feat: 实现日历课程点击跳转到直播间功能

- 添加日历课程详情弹窗的点击跳转功能
- 公共课直播间和课程直播间支持URL参数自动选中课程
- 优化岗位详情页面样式,复用简洁卡片样式
- 为岗位详情标题添加图标
- 调整不同类型课程的跳转逻辑

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-09-11 14:14:45 +08:00
parent 60bd9bb142
commit 561d5c286d
107 changed files with 101383 additions and 478 deletions

View File

@@ -47,7 +47,7 @@
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
margin: 10px 0;
position: relative;
.divider-line {
@@ -67,8 +67,46 @@
background-color: #fff;
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 4px;
}
}
/* 可点击的分割线样式 */
.course-divider.clickable {
cursor: pointer;
transition: all 0.3s ease;
}
.course-divider.clickable:hover .divider-text {
color: #4080ff;
}
.course-divider.clickable:hover .divider-line {
border-color: #4080ff;
}
/* 分类容器缓动效果 */
.course-category-wrapper {
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.course-category-wrapper.expanded {
max-height: 2000px;
opacity: 1;
transform: scaleY(1);
margin-bottom: 0;
}
.course-category-wrapper.collapsed {
max-height: 0;
opacity: 0;
transform: scaleY(0);
margin-bottom: 0;
}
/* 自定义折叠面板元素 */
.course-list-item {

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { Collapse, Timeline, Spin } from "@arco-design/web-react";
import { IconDown, IconRight } from "@arco-design/web-react/icon";
import { getCourseLiveList } from "@/services/courseLive";
import "./index.css";
@@ -11,6 +12,12 @@ const CourseList = ({ className = "", onCourseClick }) => {
const [verticalCourseList, setVerticalCourseList] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedCourseId, setSelectedCourseId] = useState(null);
// 控制各分类的展开/收缩状态,默认全部展开
const [categoryExpanded, setCategoryExpanded] = useState({
'compound': true, // 复合能力课
'vertical': true // 垂直提升课
});
useEffect(() => {
fetchCourseList();
@@ -133,15 +140,21 @@ const CourseList = ({ className = "", onCourseClick }) => {
>
{/* 复合能力课分割线 */}
{compoundCourseList.length > 0 && (
<div className="course-divider">
<div
className="course-divider clickable"
onClick={() => setCategoryExpanded(prev => ({ ...prev, compound: !prev.compound }))}
>
<span className="divider-line"></span>
<span className="divider-text">复合能力课</span>
<span className="divider-text">
复合能力课
</span>
<span className="divider-line"></span>
</div>
)}
{/* 复合能力课部分 */}
{compoundCourseList.map((unit, index) => (
<div className={`course-category-wrapper ${!categoryExpanded.compound ? 'collapsed' : 'expanded'}`}>
{compoundCourseList.map((unit, index) => (
<CollapseItem
key={unit.unitId}
header={unit.unitName}
@@ -183,18 +196,25 @@ const CourseList = ({ className = "", onCourseClick }) => {
</Timeline>
</CollapseItem>
))}
</div>
{/* 垂直能力课分割线 */}
{verticalCourseList.length > 0 && (
<div className="course-divider">
<div
className="course-divider clickable"
onClick={() => setCategoryExpanded(prev => ({ ...prev, vertical: !prev.vertical }))}
>
<span className="divider-line"></span>
<span className="divider-text">垂直能力课</span>
<span className="divider-text">
垂直能力课
</span>
<span className="divider-line"></span>
</div>
)}
{/* 垂直能力课部分 */}
{verticalCourseList.map((unit, index) => {
<div className={`course-category-wrapper ${!categoryExpanded.vertical ? 'collapsed' : 'expanded'}`}>
{verticalCourseList.map((unit, index) => {
// 检查单元是否包含可试看课程
const hasPreviewCourse = unit.courses.some(course => course.canPreview);
@@ -274,6 +294,7 @@ const CourseList = ({ className = "", onCourseClick }) => {
</CollapseItem>
);
})}
</div>
</Collapse>
</div>
</div>

View File

@@ -197,7 +197,7 @@ export default ({ className = "", isLock = false, selectedCourse, teacherData, u
{displayCourse?.current && <div className="living-icon" />}
</div>
<span className="teacher-name">{currentTeacher?.name || ''}老师</span>
<span className="teacher-tag">{displayCourse?.unitName || unitName}</span>
<span className="teacher-tag">{displayCourse?.courseName || courseName || ''}</span>
<div className="living-data">
<div className="living-data-item">
<span>开始</span>

View File

@@ -43,6 +43,74 @@
width: 100%;
}
/*/* 分割线样式 */
.course-list-wrapper .course-list-content .course-list .course-divider {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin: 10px 0;
position: relative;
}
.course-list-wrapper .course-list-content .course-list .course-divider .divider-line {
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, #e5e6eb 20%, #e5e6eb 80%, transparent);
border-style: dashed;
border-width: 1px 0 0 0;
border-color: #e5e6eb;
}
.course-list-wrapper .course-list-content .course-list .course-divider .divider-text {
padding: 0 16px;
font-size: 14px;
font-weight: 500;
color: #86909c;
background-color: #fff;
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 4px;
}
/* 可点击的分割线样式 */
.course-list-wrapper .course-list-content .course-list .course-divider.clickable {
cursor: pointer;
transition: all 0.3s ease;
}
.course-list-wrapper .course-list-content .course-list .course-divider.clickable:hover .divider-text {
color: #4080ff;
}
.course-list-wrapper .course-list-content .course-list .course-divider.clickable:hover .divider-line {
border-color: #4080ff;
}
/* 分类容器缓动效果 */
.course-list-wrapper .course-list-content .course-list .course-category-wrapper {
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.course-list-wrapper .course-list-content .course-list .course-category-wrapper.expanded {
max-height: 2000px;
opacity: 1;
transform: scaleY(1);
margin-bottom: 0;
}
.course-list-wrapper .course-list-content .course-list .course-category-wrapper.collapsed {
max-height: 0;
opacity: 0;
transform: scaleY(0);
margin-bottom: 0;
}
/* 自定义折叠面板元素 */
.course-list-wrapper .course-list-content .course-list .course-list-item {
width: 272px;

View File

@@ -1,14 +1,10 @@
import { useState, useEffect } 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 unitPosters = {
"终生学习系统": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuW7gMz6sRee.jpg", // AI课 -> sRee
"企业高管公开课": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuW7gMz6CiPN.jpg", // 公共课 -> CiPN
"营销能力课": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuW7gMz6zwRv.jpg" // 营销课 -> zwRv
};
// 单元海报数据将从服务器返回的数据中获取
const TimelineItem = Timeline.Item;
const CollapseItem = Collapse.Item;
@@ -17,6 +13,13 @@ const PublicCourseList = ({ className = "", onCourseClick }) => {
const [courseLiveList, setCourseLiveList] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedCourseId, setSelectedCourseId] = useState(null);
// 控制各分类的展开/收缩状态,默认全部展开
const [categoryExpanded, setCategoryExpanded] = useState({
'ai': true, // 终生学习系统
'public': true, // 企业高管公开课
'marketing': true // 营销能力课
});
useEffect(() => {
fetchCourseList();
@@ -95,54 +98,143 @@ const PublicCourseList = ({ className = "", onCourseClick }) => {
expandIconPosition="right"
defaultActiveKey={[]}
>
{courseLiveList.map((unit, index) => (
<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"
{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 }))}
>
<div
className={`time-line-item ${getCourseStatus(course)} ${selectedCourseId === course.courseId ? 'selected' : ''}`}
onClick={() => {
setSelectedCourseId(course.courseId);
// 调试日志
console.log('点击课程单元:', unit.unitName);
console.log('匹配的海报URL:', unitPosters[unit.unitName]);
onCourseClick && onCourseClick({
...course,
unitName: unit.unitName,
unitPoster: unitPosters[unit.unitName] || unitPosters["终生学习系统"] || "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>
))}
<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>