主要更新: - 更新所有12个产业的教务系统数据和功能 - 删除所有 node_modules 文件夹(节省3.7GB) - 删除所有 .yoyo 缓存文件夹(节省1.2GB) - 删除所有 dist 构建文件(节省55MB) 项目优化: - 项目大小从 8.1GB 减少到 3.2GB(节省60%空间) - 保留完整的源代码和配置文件 - .gitignore 已配置,防止再次提交大文件 启动脚本: - start-industry.sh/bat/ps1 脚本会自动检测并安装依赖 - 首次启动时自动运行 npm install - 支持单个或批量启动产业系统 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
355 lines
14 KiB
JavaScript
355 lines
14 KiB
JavaScript
import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
|
||
import { Collapse, Timeline, Spin } from "@arco-design/web-react";
|
||
import { IconDown, IconRight } from "@arco-design/web-react/icon";
|
||
import { getPublicCourseLiveList } from "@/services/courseLive";
|
||
import "./index.css";
|
||
|
||
// 单元海报数据将从服务器返回的数据中获取
|
||
|
||
const TimelineItem = Timeline.Item;
|
||
const CollapseItem = Collapse.Item;
|
||
|
||
const PublicCourseList = forwardRef(({ className = "", onCourseClick }, ref) => {
|
||
const [courseLiveList, setCourseLiveList] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [selectedCourseId, setSelectedCourseId] = useState(null);
|
||
const [activeKeys, setActiveKeys] = useState([]);
|
||
|
||
// 控制各分类的展开/收缩状态,默认全部展开
|
||
const [categoryExpanded, setCategoryExpanded] = useState({
|
||
'ai': true, // 终生学习系统
|
||
'public': true, // 企业高管公开课
|
||
'marketing': true // 营销能力课
|
||
});
|
||
|
||
useEffect(() => {
|
||
fetchCourseList();
|
||
}, []);
|
||
|
||
// 暴露方法给父组件调用
|
||
useImperativeHandle(ref, () => ({
|
||
selectCourse: (courseId, courseName) => {
|
||
console.log('PublicCourseList - selectCourse called:', courseId, courseName);
|
||
console.log('PublicCourseList - Current courseLiveList:', courseLiveList);
|
||
|
||
// 查找课程并触发点击
|
||
for (let i = 0; i < courseLiveList.length; i++) {
|
||
const unit = courseLiveList[i];
|
||
console.log(`Checking unit ${i}:`, unit.unitName, 'courses:', unit.courses?.length);
|
||
|
||
if (!unit.courses) {
|
||
console.log(`Unit ${unit.unitName} has no courses`);
|
||
continue;
|
||
}
|
||
|
||
const courseIndex = unit.courses.findIndex(c => {
|
||
const matches = c.courseId === courseId || c.courseName === courseName;
|
||
if (matches) {
|
||
console.log('Found matching course:', c);
|
||
}
|
||
return matches;
|
||
});
|
||
|
||
if (courseIndex !== -1) {
|
||
const course = unit.courses[courseIndex];
|
||
|
||
// 展开对应的单元 - 使用正确的索引
|
||
const activeKey = String(i + 1);
|
||
|
||
// 如果单元未展开,则添加到 activeKeys 中
|
||
setActiveKeys(prevKeys => {
|
||
if (!prevKeys.includes(activeKey)) {
|
||
console.log('Adding activeKey:', activeKey, 'to existing keys:', prevKeys);
|
||
return [...prevKeys, activeKey];
|
||
}
|
||
return prevKeys;
|
||
});
|
||
|
||
// 设置选中的课程
|
||
console.log('Setting selectedCourseId to:', course.courseId);
|
||
setSelectedCourseId(course.courseId);
|
||
|
||
// 触发点击事件
|
||
if (onCourseClick) {
|
||
console.log('Triggering onCourseClick with course:', course);
|
||
onCourseClick({
|
||
...course,
|
||
unitName: unit.unitName,
|
||
unitPoster: unit.unitPoster || "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuW7gMz6sRee.jpg"
|
||
});
|
||
}
|
||
|
||
// 滚动到对应的单元和课程位置
|
||
// 需要等待折叠面板展开动画完成
|
||
setTimeout(() => {
|
||
const unitElement = document.querySelector(`.course-list-item:nth-child(${i + 1})`);
|
||
if (unitElement) {
|
||
console.log('Scrolling to unit element');
|
||
unitElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
|
||
// 查找并滚动到具体课程
|
||
const courseElements = document.querySelectorAll('.time-line-item');
|
||
courseElements.forEach(element => {
|
||
const courseText = element.querySelector('p')?.textContent;
|
||
if (courseText === course.courseName) {
|
||
console.log('Scrolling to course element');
|
||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
});
|
||
}, 300); // 等待折叠面板展开
|
||
|
||
console.log('Course found and selected:', course.courseName);
|
||
return; // 找到后退出
|
||
}
|
||
}
|
||
|
||
console.log('Course not found:', courseId, courseName);
|
||
}
|
||
}), [courseLiveList, onCourseClick]);
|
||
|
||
const fetchCourseList = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await getPublicCourseLiveList();
|
||
if (res.success) {
|
||
const courseList = res.data || [];
|
||
setCourseLiveList(courseList);
|
||
// 不设置默认选中,保持黑屏状态
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch course list:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const getCourseStatus = (course) => {
|
||
if (course.completed) return "finish";
|
||
if (course.current) return "active";
|
||
|
||
// 判断未来课程的具体状态
|
||
if (course.upcoming) {
|
||
const courseDate = new Date(course.date);
|
||
const today = new Date();
|
||
|
||
// 重置时间部分只比较日期
|
||
courseDate.setHours(0, 0, 0, 0);
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const timeDiff = courseDate - today;
|
||
const daysDiff = Math.floor(timeDiff / (24 * 60 * 60 * 1000));
|
||
|
||
// 未来7天内的课程显示为"即将开始"
|
||
if (daysDiff > 0 && daysDiff <= 7) {
|
||
return "coming";
|
||
}
|
||
// 7天后的课程显示为"未开始"
|
||
return "not-started";
|
||
}
|
||
|
||
// 默认状态
|
||
return "pending";
|
||
};
|
||
|
||
// 获取图标类型
|
||
const getDotIcon = (course) => {
|
||
if (course.completed) return <div className="time-line-dot-icon" />;
|
||
if (course.current) return <div className="time-line-clock-icon" />;
|
||
return <div className="time-line-lock-icon" />;
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className={`${className} course-list-wrapper`}>
|
||
<p className="course-list-title">公共课程列表</p>
|
||
<div className="course-list-content">
|
||
<Spin />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`${className} course-list-wrapper`}>
|
||
<p className="course-list-title">公共课程列表</p>
|
||
<div className="course-list-content">
|
||
<Collapse
|
||
className="course-list"
|
||
bordered={false}
|
||
expandIconPosition="right"
|
||
activeKey={activeKeys}
|
||
onChange={(keys) => {
|
||
console.log('PublicCourseList Collapse onChange received:', keys, 'type:', typeof keys);
|
||
|
||
// Arco Collapse 在受控模式下,当点击时:
|
||
// - 如果是字符串,表示点击了某个面板,需要切换它的展开/收起状态
|
||
// - 如果是数组,表示新的展开状态
|
||
if (typeof keys === 'string') {
|
||
// 切换单个面板的展开/收起状态(手风琴效果:展开一个时自动收起其他)
|
||
setActiveKeys(prevKeys => {
|
||
const keyStr = String(keys);
|
||
const index = prevKeys.indexOf(keyStr);
|
||
if (index > -1) {
|
||
// 如果已展开,则收起
|
||
console.log('Closing panel:', keyStr);
|
||
return [];
|
||
} else {
|
||
// 如果已收起,则展开(并自动收起其他面板)
|
||
console.log('Opening panel:', keyStr, 'and closing others');
|
||
return [keyStr];
|
||
}
|
||
});
|
||
} else if (Array.isArray(keys)) {
|
||
// 直接设置新的展开状态
|
||
setActiveKeys(keys);
|
||
} else {
|
||
// 处理 undefined/null 的情况
|
||
setActiveKeys([]);
|
||
}
|
||
}}
|
||
>
|
||
{courseLiveList.map((unit, index) => {
|
||
// 判断当前单元属于哪个分类
|
||
const isAIUnit = [
|
||
"AI 入门与工具环境",
|
||
"RAG 与检索增强",
|
||
"AI 自动化与任务编排",
|
||
"AI 项目开发与前端交互",
|
||
"AI 大模型与核心原理",
|
||
"AI 行业应用与综合实战"
|
||
].includes(unit.unitName);
|
||
|
||
const isPublicUnit = [
|
||
"沟通与协作能力",
|
||
"问题解决与思维能力",
|
||
"职场基础与个人发展",
|
||
"职业发展与管理技能"
|
||
].includes(unit.unitName);
|
||
|
||
const isMarketingUnit = [
|
||
"必备营销技能",
|
||
"自我营销课"
|
||
].includes(unit.unitName);
|
||
|
||
// 判断是否需要显示分割线
|
||
// 终生学习系统:第一个AI单元前显示
|
||
const showAIDivider = isAIUnit && index === 0;
|
||
|
||
// 企业高管公开课:第一个企业高管单元前显示(前一个不是企业高管单元)
|
||
const showPublicDivider = isPublicUnit && index > 0 &&
|
||
!["沟通与协作能力", "问题解决与思维能力", "职场基础与个人发展", "职业发展与管理技能"]
|
||
.includes(courseLiveList[index - 1]?.unitName);
|
||
|
||
// 营销能力课:第一个营销单元前显示(前一个不是营销单元)
|
||
const showMarketingDivider = isMarketingUnit && index > 0 &&
|
||
!["必备营销技能", "自我营销课"]
|
||
.includes(courseLiveList[index - 1]?.unitName);
|
||
|
||
return (
|
||
<div key={unit.unitId}>
|
||
{/* 终生学习系统分割线 */}
|
||
{showAIDivider && (
|
||
<div
|
||
className="course-divider clickable"
|
||
onClick={() => setCategoryExpanded(prev => ({ ...prev, ai: !prev.ai }))}
|
||
>
|
||
<span className="divider-line"></span>
|
||
<span className="divider-text">
|
||
终生学习系统
|
||
</span>
|
||
<span className="divider-line"></span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 企业高管公开课分割线 */}
|
||
{showPublicDivider && (
|
||
<div
|
||
className="course-divider clickable"
|
||
onClick={() => setCategoryExpanded(prev => ({ ...prev, public: !prev.public }))}
|
||
>
|
||
<span className="divider-line"></span>
|
||
<span className="divider-text">
|
||
企业高管公开课
|
||
</span>
|
||
<span className="divider-line"></span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 营销能力课分割线 */}
|
||
{showMarketingDivider && (
|
||
<div
|
||
className="course-divider clickable"
|
||
onClick={() => setCategoryExpanded(prev => ({ ...prev, marketing: !prev.marketing }))}
|
||
>
|
||
<span className="divider-line"></span>
|
||
<span className="divider-text">
|
||
营销能力课
|
||
</span>
|
||
<span className="divider-line"></span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 根据分类的展开状态决定是否显示单元 */}
|
||
<div
|
||
className={`course-category-wrapper ${
|
||
(isAIUnit && !categoryExpanded.ai) ||
|
||
(isPublicUnit && !categoryExpanded.public) ||
|
||
(isMarketingUnit && !categoryExpanded.marketing)
|
||
? 'collapsed' : 'expanded'
|
||
}`}
|
||
>
|
||
<CollapseItem
|
||
key={unit.unitId}
|
||
header={unit.unitName}
|
||
name={String(index + 1)}
|
||
className="course-list-item"
|
||
>
|
||
<Timeline>
|
||
{unit.courses.map((course) => (
|
||
<TimelineItem
|
||
key={course.courseId}
|
||
dot={getDotIcon(course)}
|
||
lineType="dashed"
|
||
>
|
||
<div
|
||
className={`time-line-item ${getCourseStatus(course)} ${selectedCourseId === course.courseId ? 'selected' : ''}`}
|
||
onClick={() => {
|
||
setSelectedCourseId(course.courseId);
|
||
|
||
onCourseClick && onCourseClick({
|
||
...course,
|
||
unitName: unit.unitName,
|
||
unitPoster: unit.unitPoster || "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuW7gMz6sRee.jpg"
|
||
});
|
||
}}
|
||
style={{ cursor: 'pointer' }}
|
||
>
|
||
<p style={{
|
||
overflow: 'visible',
|
||
textOverflow: 'unset',
|
||
whiteSpace: 'normal',
|
||
wordBreak: 'break-word'
|
||
}}>
|
||
{course.courseName}
|
||
</p>
|
||
<div className="time-line-item-info">
|
||
<span>{course.teacherName}</span>
|
||
<span className="course-date">{course.date}</span>
|
||
</div>
|
||
</div>
|
||
</TimelineItem>
|
||
))}
|
||
</Timeline>
|
||
</CollapseItem>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</Collapse>
|
||
</div>
|
||
</div>
|
||
);
|
||
});
|
||
|
||
export default PublicCourseList; |