forked from Gitlink/forgeplus-react
Merge branch 'gitlink_competitions' of https://gitlink.org.cn/Gitlink/forgeplus-react into gitlink_competitions
This commit is contained in:
commit
435a199c2b
File diff suppressed because it is too large
Load Diff
|
|
@ -45,6 +45,7 @@
|
|||
"echarts-for-react": "2.0.16",
|
||||
"echarts-wordcloud": "1.1.3",
|
||||
"flv.js": "1.5.0",
|
||||
"gsap": "^3.14.2",
|
||||
"helmet": "3.23.3",
|
||||
"hls.js": "^1.1.1",
|
||||
"html2pdf.js": "^0.10.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import SpeTitle from '../components/Title'
|
||||
import styles from './index.less'
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { gsap } from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { ScrollToPlugin } from 'gsap/ScrollToPlugin'
|
||||
import icon from '../img/schedule/icon.webp'
|
||||
import right from '../img/schedule/right.webp'
|
||||
import { timelineDir, ruleList } from '../info'
|
||||
|
|
@ -8,6 +11,8 @@ import { getDocDetail, getDocList } from '../api'
|
|||
import { history } from 'umi'
|
||||
import { Base64 } from 'js-base64'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||
|
||||
function Schedule(props) {
|
||||
|
||||
const [selectedRule, setSelectedRule] = useState(ruleList[1].id)
|
||||
|
|
@ -23,10 +28,13 @@ function Schedule(props) {
|
|||
const scheduleRefs = useRef({})
|
||||
const periodRefs = useRef({})
|
||||
const periodListRef = useRef(null)
|
||||
const autoPlayTimerRef = useRef(null)
|
||||
const progressTimerRef = useRef(null)
|
||||
const startTimeRef = useRef(null)
|
||||
const elapsedTimeRef = useRef(0)
|
||||
const scheduleDetailRef = useRef(null)
|
||||
const scheduleLeftRef = useRef(null)
|
||||
const scheduleListRef = useRef(null)
|
||||
const scrollTriggerRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const ruleInfo = {}
|
||||
|
|
@ -52,35 +60,34 @@ function Schedule(props) {
|
|||
const sortedRows = res.rows.reverse();
|
||||
setPeriodList(sortedRows);
|
||||
|
||||
// 为每个文章调用getDocDetail接口
|
||||
const scheduleData = [];
|
||||
const periodMap = new Map(sortedRows.map((period, index) => [period.id, { ...period, order: index }]));
|
||||
|
||||
const scheduleDataMap = new Map();
|
||||
sortedRows.forEach(period => {
|
||||
scheduleDataMap.set(period.id, []);
|
||||
});
|
||||
|
||||
let processedCount = 0;
|
||||
|
||||
sortedRows.forEach((period, periodIndex) => {
|
||||
sortedRows.forEach(period => {
|
||||
getDocDetail(period.id).then(detailRes => {
|
||||
if (detailRes.data && detailRes.data.content) {
|
||||
// Base64解码content字段
|
||||
const decodedContent = Base64.decode(detailRes.data.content);
|
||||
|
||||
// 使用DOMParser解析HTML内容
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(decodedContent, 'text/html');
|
||||
const paragraphs = doc.querySelectorAll('p');
|
||||
|
||||
let scheduleId = scheduleData.length + 1;
|
||||
let scheduleId = 1;
|
||||
|
||||
paragraphs.forEach((p) => {
|
||||
const text = p.textContent.trim();
|
||||
if (text) {
|
||||
// 按空格分割,第一个词作为日期,其余作为内容
|
||||
const parts = text.split(/\s+/);
|
||||
if (parts.length > 0) {
|
||||
const date = parts[0];
|
||||
const content = parts.slice(1).join(' ');
|
||||
|
||||
// 创建赛程项
|
||||
scheduleData.push({
|
||||
id: scheduleId++,
|
||||
scheduleDataMap.get(period.id).push({
|
||||
periodId: period.id,
|
||||
date: date,
|
||||
content: content,
|
||||
|
|
@ -93,10 +100,21 @@ function Schedule(props) {
|
|||
|
||||
processedCount++;
|
||||
|
||||
// 当所有文章都处理完成后,更新scheduleList
|
||||
if (processedCount === sortedRows.length) {
|
||||
// 更新scheduleList状态
|
||||
setScheduleList(scheduleData);
|
||||
const finalScheduleData = [];
|
||||
sortedRows.forEach(period => {
|
||||
const periodSchedules = scheduleDataMap.get(period.id);
|
||||
if (periodSchedules && periodSchedules.length > 0) {
|
||||
periodSchedules.forEach(schedule => {
|
||||
finalScheduleData.push({
|
||||
...schedule,
|
||||
globalId: finalScheduleData.length + 1
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setScheduleList(finalScheduleData);
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
|
|
@ -104,7 +122,20 @@ function Schedule(props) {
|
|||
processedCount++;
|
||||
|
||||
if (processedCount === sortedRows.length) {
|
||||
setScheduleList(scheduleData);
|
||||
const finalScheduleData = [];
|
||||
sortedRows.forEach(period => {
|
||||
const periodSchedules = scheduleDataMap.get(period.id);
|
||||
if (periodSchedules && periodSchedules.length > 0) {
|
||||
periodSchedules.forEach(schedule => {
|
||||
finalScheduleData.push({
|
||||
...schedule,
|
||||
globalId: finalScheduleData.length + 1
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setScheduleList(finalScheduleData);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -165,6 +196,86 @@ function Schedule(props) {
|
|||
};
|
||||
}, [ruleList, isHovered]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scheduleLeftRef.current) return;
|
||||
|
||||
// 使用 GSAP ScrollTrigger 只 pin 左侧区域
|
||||
scrollTriggerRef.current = ScrollTrigger.create({
|
||||
trigger: scheduleLeftRef.current,
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
pin: true,
|
||||
pinSpacing: false,
|
||||
anticipatePin: 1
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (scrollTriggerRef.current) {
|
||||
scrollTriggerRef.current.kill();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 使用 ref 来跟踪当前的 selectedPeriod,避免循环触发
|
||||
const selectedPeriodRef = useRef(selectedPeriod);
|
||||
useEffect(() => {
|
||||
selectedPeriodRef.current = selectedPeriod;
|
||||
}, [selectedPeriod]);
|
||||
|
||||
// 使用 GSAP ScrollTrigger 根据右侧视口中第一个 schedule_item 自动激活对应 period
|
||||
useEffect(() => {
|
||||
if (!scheduleList.length || !scheduleRefs.current) return;
|
||||
|
||||
const triggers = [];
|
||||
|
||||
// 为每个 period 的第一个 schedule_item 创建 ScrollTrigger
|
||||
const periodFirstSchedules = new Map();
|
||||
scheduleList.forEach(schedule => {
|
||||
if (!periodFirstSchedules.has(schedule.periodId)) {
|
||||
periodFirstSchedules.set(schedule.periodId, schedule);
|
||||
}
|
||||
});
|
||||
|
||||
periodFirstSchedules.forEach((schedule, periodId) => {
|
||||
const element = scheduleRefs.current[schedule.globalId];
|
||||
if (element) {
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: "top 20%",
|
||||
end: "top 0",
|
||||
onEnter: () => {
|
||||
if (selectedPeriodRef.current !== periodId) {
|
||||
setSelectedPeriod(periodId);
|
||||
|
||||
// 更新左侧激活条位置
|
||||
if (periodRefs.current[periodId] && periodListRef.current) {
|
||||
const periodElement = periodRefs.current[periodId];
|
||||
const offsetTop = periodElement.offsetTop;
|
||||
periodListRef.current.style.setProperty('--active-top', `${offsetTop}px`);
|
||||
}
|
||||
}
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (selectedPeriodRef.current !== periodId) {
|
||||
setSelectedPeriod(periodId);
|
||||
|
||||
// 更新左侧激活条位置
|
||||
if (periodRefs.current[periodId] && periodListRef.current) {
|
||||
const periodElement = periodRefs.current[periodId];
|
||||
const offsetTop = periodElement.offsetTop;
|
||||
periodListRef.current.style.setProperty('--active-top', `${offsetTop}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
triggers.push(trigger);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
triggers.forEach(trigger => trigger.kill());
|
||||
};
|
||||
}, [scheduleList]);
|
||||
|
||||
const handleRuleChange = (ruleId) => {
|
||||
elapsedTimeRef.current = 0;
|
||||
|
|
@ -182,25 +293,21 @@ function Schedule(props) {
|
|||
const element = periodRefs.current[periodId]
|
||||
const offsetTop = element.offsetTop
|
||||
periodListRef.current.style.setProperty('--active-top', `${offsetTop}px`)
|
||||
|
||||
// 滚动到对应时期项
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动到对应赛程项,添加200px余量
|
||||
// 使用 GSAP 滚动到对应赛程项(现在右侧随页面一起滚动)
|
||||
if (scheduleList && scheduleList.length > 0) {
|
||||
const firstScheduleOfPeriod = scheduleList.find(schedule => schedule.periodId === periodId)
|
||||
if (firstScheduleOfPeriod && scheduleRefs.current[firstScheduleOfPeriod.id]) {
|
||||
const element = scheduleRefs.current[firstScheduleOfPeriod.id]
|
||||
const container = element.parentElement
|
||||
const offsetTop = element.getBoundingClientRect().top - container.getBoundingClientRect().top
|
||||
|
||||
container.scrollTo({
|
||||
top: offsetTop + container.scrollTop,
|
||||
behavior: 'smooth'
|
||||
if (firstScheduleOfPeriod && scheduleRefs.current[firstScheduleOfPeriod.globalId]) {
|
||||
const element = scheduleRefs.current[firstScheduleOfPeriod.globalId]
|
||||
|
||||
gsap.to(window, {
|
||||
scrollTo: {
|
||||
y: element,
|
||||
offsetY: 100
|
||||
},
|
||||
duration: 0.5,
|
||||
ease: "power2.inOut"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -214,7 +321,7 @@ function Schedule(props) {
|
|||
ruleList.map(e => {
|
||||
return <div className={`${ styles.rules_tab_item } ${ e.id === selectedRule ? styles.rules_tab_selected : '' }`} onClick={() => { handleRuleChange(e.id) }}>
|
||||
<span>{ e.name }</span>
|
||||
<span>2026第八届开源创新大赛</span>
|
||||
<span>2026 第八届开源创新大赛</span>
|
||||
<span>Competition Rules</span>
|
||||
</div>
|
||||
})
|
||||
|
|
@ -238,8 +345,8 @@ function Schedule(props) {
|
|||
</div>
|
||||
|
||||
<SpeTitle className={styles.title2} first={'详细赛程'} last={'XXXX'} english={'four tracks'}></SpeTitle>
|
||||
<div className={styles.schedule_detail}>
|
||||
<div className={styles.schedule_left}>
|
||||
<div ref={scheduleDetailRef} className={styles.schedule_detail}>
|
||||
<div ref={scheduleLeftRef} className={styles.schedule_left}>
|
||||
<p className={styles.period_date}>03.30 — 08.16</p>
|
||||
<div className={styles.period_list} ref={periodListRef}>
|
||||
{periodList.map(period => (
|
||||
|
|
@ -259,11 +366,11 @@ function Schedule(props) {
|
|||
</div>
|
||||
<div className={styles.schedule_right}>
|
||||
<p className={styles.schedule_title}>具体赛程</p>
|
||||
<div className={styles.schedule_list}>
|
||||
<div ref={scheduleListRef} className={styles.schedule_list}>
|
||||
{scheduleList.map(schedule => (
|
||||
<div
|
||||
key={schedule.id}
|
||||
ref={el => scheduleRefs.current[schedule.id] = el}
|
||||
key={schedule.globalId}
|
||||
ref={el => scheduleRefs.current[schedule.globalId] = el}
|
||||
className={styles.schedule_item}
|
||||
>
|
||||
<div className={styles.schedule_date}>{schedule.date}</div>
|
||||
|
|
@ -279,4 +386,4 @@ function Schedule(props) {
|
|||
</div>
|
||||
}
|
||||
|
||||
export default Schedule
|
||||
export default Schedule
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
background-position: top, bottom;
|
||||
background-size: 100% auto, 100% auto;
|
||||
.title {
|
||||
padding-top: 40px;
|
||||
padding-top: 160px;
|
||||
}
|
||||
.title2 {
|
||||
padding-top: 100px;
|
||||
|
|
@ -288,9 +288,6 @@
|
|||
max-width: 1105px;
|
||||
|
||||
.schedule_list {
|
||||
max-height: 700px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 100px;
|
||||
|
||||
|
|
@ -343,17 +340,24 @@
|
|||
flex: 1;
|
||||
.schedule_content {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-family: alibabaPuhuiM;
|
||||
color:#091221;
|
||||
font-size:20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 800px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.schedule_status {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
width: 150px;
|
||||
line-height:normal;
|
||||
font-family: alibabaReg;
|
||||
color:#434769;
|
||||
font-size:16px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +365,7 @@
|
|||
font-family: alibabaPuhuiM;
|
||||
color: #091221;
|
||||
font-size: 16px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
|
|||
Loading…
Reference in New Issue