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>

View File

@@ -648,7 +648,7 @@
"任职要求": "1.具备丰富的酒店/民宿客房行业管理经验客房主管履历2年及以上有高端酒店/民宿领域领域工作经验者可优先录用。\n2.具备良好的沟通能力和团队管理能。\n3.对工作细致细心有较强的责任心和服务意识。\n4.能有效处理突发情况,保证客房服务的连续性。\n5.与其他部门协同工作,完成民宿接待工作",
"公司介绍": "米拉夏朵成立于2018年以“缔造城市微度假理想生活”为使命深耕长三角精品度假产业。现运营上海迪士尼/海昌海洋公园周边的五大主题民宿筹建中的安吉竹海度假村即将成为浙北文旅新地标。旗下项目连续斩获「中国十大必睡星宿」「上海市五星级乡村民宿」「长三角精品民宿TOP10」等20+行业殊荣。\n\n【品牌实力】\n√ 精准布局流量入口依托上海两大千万级流量景区打造“15分钟度假生活圈”\n√ 沉浸式场景设计每座建筑融合在地文化与童话美学客房平均溢价率超行业35%\n\n【团队基因】\n90后海归主理人领衔集结资深酒店管理精英、新锐空间设计师、文旅策划专家组成的跨领域战队。扁平化管理模式下每位成员都是价值创造者。\n\n【加入我们你将获得】\n• 参与长三角文旅产业升级的黄金机遇,接触民宿与度假村双赛道运营;\n• 合伙人孵化计划,优秀者可获门店股权激励及项目分红;\n• 年度带薪度假体验特权5天+专属福利包);\n• 迪士尼门票、安吉茶园采茶体验等员工权益;\n• 定期行业培训与跨界交流机会(如设计、文旅、电商模块)。\n\n【寻找这样的你】\n如果你不仅是执行者更是创造者既能用数据驱动运营又能用情怀打磨体验渴望在诗与远方中实现商业价值欢迎加入米拉夏朵我们将为你提供百万级项目操盘机会与行业顶配资源共同书写中国度假产业的新篇章。\n\n—— 让每个日常,都有抵达远方的可能 ——",
"岗位标签": "实习",
"岗位相关标签": "专业相关岗位",
"岗位相关标签": "专业相关岗位",
"福利标签": [
"五险一金",
"带薪年假",

View File

@@ -660,7 +660,7 @@
"任职要求": "1. 具备丰富的酒店/民宿客房行业管理经验客房主管履历2年及以上有高端酒店/民宿领域领域工作经验者可优先录用。\n2. 具备良好的沟通能力和团队管理能。\n3. 对工作细致细心有较强的责任心和服务意识。\n4. 能有效处理突发情况,保证客房服务的连续性。\n5. 与其他部门协同工作,完成民宿接待工作。",
"公司介绍": "米拉夏朵成立于2018年以“缔造城市微度假理想生活”为使命深耕长三角精品度假产业。现运营上海迪士尼/海昌海洋公园周边的五大主题民宿筹建中的安吉竹海度假村即将成为浙北文旅新地标。旗下项目连续斩获「中国十大必睡星宿」「上海市五星级乡村民宿」「长三角精品民宿TOP10」等20+行业殊荣。\n\n【品牌实力】\n√ 精准布局流量入口依托上海两大千万级流量景区打造“15分钟度假生活圈”\n√ 沉浸式场景设计每座建筑融合在地文化与童话美学客房平均溢价率超行业35%\n\n【团队基因】\n90后海归主理人领衔集结资深酒店管理精英、新锐空间设计师、文旅策划专家组成的跨领域战队。扁平化管理模式下每位成员都是价值创造者。\n\n【加入我们你将获得】\n• 参与长三角文旅产业升级的黄金机遇,接触民宿与度假村双赛道运营;\n• 合伙人孵化计划,优秀者可获门店股权激励及项目分红;\n• 年度带薪度假体验特权5天+专属福利包);\n• 迪士尼门票、安吉茶园采茶体验等员工权益;\n• 定期行业培训与跨界交流机会(如设计、文旅、电商模块)。\n\n【寻找这样的你】\n如果你不仅是执行者更是创造者既能用数据驱动运营又能用情怀打磨体验渴望在诗与远方中实现商业价值欢迎加入米拉夏朵我们将为你提供百万级项目操盘机会与行业顶配资源共同书写中国度假产业的新篇章。\n\n—— 让每个日常,都有抵达远方的可能 ——",
"岗位标签": "实习",
"岗位相关标签": "专业相关岗位",
"岗位相关标签": "专业相关岗位",
"福利标签": [
"五险一金",
"带薪年假",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
{
"code": 200,
"message": "操作成功",
"data": {
"high": {
"name": "储备干部岗",
"list": [
{
"record_id": "recuzDi0lXYCA0",
"position_name": "民宿管家",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2im65lI.jpeg"
},
{
"record_id": "recuzDi0lXza7O",
"position_name": "民宿客房管家",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imoVv6.jpeg"
},
{
"record_id": "recuzVoIzaXUqc",
"position_name": "民宿运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imlAwW.jpeg"
},
{
"record_id": "recuzDi0lXJQ5u",
"position_name": "酒店餐饮主管",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imTKuC.jpeg"
},
{
"record_id": "recuzVoIzaMhKk",
"position_name": "酒店运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imUf1N.jpeg"
},
{
"record_id": "recuzVoIzakyIc",
"position_name": "客房经理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPGOiZ8J13S.jpeg"
},
{
"record_id": "recuzVoIzaZMmW",
"position_name": "酒店大堂副理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPGOiZ85boC.jpeg"
},
{
"record_id": "recuzVoIza7JA5",
"position_name": "餐厅运营经理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imDPFP.jpeg"
},
{
"record_id": "recuzDi0lXi4bv",
"position_name": "品牌公关",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2im9Sra.jpeg"
},
{
"record_id": "recuzDi0lXkX5u",
"position_name": "艺人经纪人",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuQIuAUMPgcx.jpeg"
},
{
"record_id": "recuzDi0lXn5dv",
"position_name": "演出执行经理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuQIuHUH07Ia.jpeg"
}
]
},
"middle": {
"name": "技术骨干岗",
"list": [
{
"record_id": "recuFZJhP8rNao",
"position_name": "赛事经纪",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPGOiZ8uICB.jpeg"
},
{
"record_id": "recuzDi0lXtaB3",
"position_name": "景区运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPGOiZ80asf.jpeg"
},
{
"record_id": "recuIZG3KIM5C5",
"position_name": "活动策划师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imXS9c.jpeg"
},
{
"record_id": "recuIZG7I3fy6w",
"position_name": "活动执行",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imyYok.jpeg"
},
{
"record_id": "recuzDi0lXAT6U",
"position_name": "场馆运营人员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imt2TC.jpeg"
},
{
"record_id": "recuRgmhftZcHa",
"position_name": "露营地运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imRFOD.jpeg"
},
{
"record_id": "recuFZJhP8hm76",
"position_name": "宠物店店长",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imzXzv.jpeg"
},
{
"record_id": "recuzJ1ZVuwnPv",
"position_name": "宠物营养师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imdfAp.jpeg"
},
{
"record_id": "recuIZKwO9ixGh",
"position_name": "SEO专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2iml2IU.jpeg"
},
{
"record_id": "recuzDi0lX7glV",
"position_name": "SEM专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imnfVv.jpeg"
},
{
"record_id": "recuzDi0lXyBDV",
"position_name": "文创产品设计师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imizTB.jpeg"
},
{
"record_id": "recuRgmlK4Pj7L",
"position_name": "文创产品策划师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imjSEA.jpeg"
},
{
"record_id": "recuFZJhP8hP1X",
"position_name": "会展策划师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imOdyu.jpeg"
},
{
"record_id": "recuzVoIzaxqAd",
"position_name": "会展营销",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imYg13.jpeg"
},
{
"record_id": "recuFZJhP8yJi4",
"position_name": "漫展策划师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2im9Ecg.jpeg"
},
{
"record_id": "recuRgmO7ra6GB",
"position_name": "品牌策划运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imPi00.jpeg"
},
{
"record_id": "recuzDi0lXQCWG",
"position_name": "品牌推广专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imTJYD.jpeg"
},
{
"record_id": "recuzDi0lXedcK",
"position_name": "社群运营",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imvheR.jpeg"
},
{
"record_id": "recuzVoIzaHWP9",
"position_name": "ip运营",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imDp1Z.jpeg"
},
{
"record_id": "recuFc94HfoZHy",
"position_name": "赛事礼仪",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2im6YlW.jpeg"
},
{
"record_id": "recuFZJ4YLutP7",
"position_name": "赛事编辑",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2im1JEV.jpeg"
},
{
"record_id": "recuzDi0lXh3P4",
"position_name": "新媒体运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imbZSu.jpeg"
},
{
"record_id": "recuFZJhP80n5d",
"position_name": "二次元周边店店长",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imWiVD.jpeg"
},
{
"record_id": "recuzDi0lXzexM",
"position_name": "旅游规划师",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2im0xT7.jpeg"
},
{
"record_id": "recuzDi0lXXv4U",
"position_name": "旅游计调专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imqBs9.jpeg"
},
{
"record_id": "recuTPacKm3d50",
"position_name": "IP运营总监助理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuTL5CPsNiYe.jpeg"
}
]
},
"ordinary": {
"name": "普通岗",
"list": [
{
"record_id": "recuFZJhP8plco",
"position_name": "二次元周边选品专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imqaAh.jpeg"
},
{
"record_id": "recuRgloZcfNcO",
"position_name": "二次元周边店店员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFP8c21CSJ.jpeg"
},
{
"record_id": "recuRgmXAXrTA8",
"position_name": "会展执行助理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imZ1Bt.jpeg"
},
{
"record_id": "recuzDi0lXFOur",
"position_name": "会展讲解员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imXoJH.jpeg"
},
{
"record_id": "recuRgmV8M0qkD",
"position_name": "商业会展执行专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imjOOd.jpeg"
},
{
"record_id": "recuzVoIzafaiF",
"position_name": "直播中控",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imcqPK.jpeg"
},
{
"record_id": "recuzVoIzazez6",
"position_name": "直播助理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imof01.jpeg"
},
{
"record_id": "recuzDi0lXsd2r",
"position_name": "网络运营专员",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuPFY2imy8Dg.jpeg"
},
{
"record_id": "recuTPacKmTFN8",
"position_name": "文创产品设计师助理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuTL5CPsiABJ.jpeg"
},
{
"record_id": "recuTPacKmIYRA",
"position_name": "品牌公关管培生",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuTL5CPsaZas.jpeg"
},
{
"record_id": "recuTPacKm0jSg",
"position_name": "文旅运营总监助理",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuTL5CPswT5x.jpeg"
},
{
"record_id": "recuTPacKmzBcS",
"position_name": "文旅项目投资拓展管培生",
"img": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/butler_position_avatar/recuTL5CPs5hKs.jpeg"
}
]
}
}
}

View File

@@ -2701,7 +2701,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",
@@ -8714,7 +8714,7 @@ mockData.profileOverview = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1544,7 +1544,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1621,7 +1621,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1437,7 +1437,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1425,7 +1425,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1480,7 +1480,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1694,7 +1694,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1904,7 +1904,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

File diff suppressed because it is too large Load Diff

View File

@@ -2431,7 +2431,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1159,7 +1159,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1159,7 +1159,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -2701,7 +2701,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

View File

@@ -1149,7 +1149,7 @@ export const mockData = {
interviews: [
{
id: 1,
company: "求职面试初体验",
company: "面试初体验",
position: "自我介绍练习",
date: "2023-09-15",
time: "14:30",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,220 +1,265 @@
import { useEffect } from "react";
import Portal from "@/components/Portal";
import { IconCalendarClock, IconCheck, IconClockCircle } from "@arco-design/web-react/icon";
const EventDetailModal = ({ isOpen, event, onClose }) => {
// ESC键关闭模态框
useEffect(() => {
const handleEscKey = (e) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
};
if (isOpen) {
document.addEventListener("keydown", handleEscKey);
}
return () => {
document.removeEventListener("keydown", handleEscKey);
};
}, [isOpen, onClose]);
// 如果未打开或无事件数据,不渲染
if (!isOpen || !event) {
return null;
}
// 事件类型映射
const eventTypeMap = {
'compound-skill': '复合技能课',
'vertical-skill': '垂直技能课',
'public-course': '公开课',
'one-on-one': '1v1规划',
'interview': '线下面试模拟',
'class': '课',
'meeting': '会议',
'lab': '实验',
'exam': '考试',
};
// 事件类型颜色映射
const eventTypeColorMap = {
'compound-skill': { bg: '#667eea', light: '#e0e7ff' },
'vertical-skill': { bg: '#22c55e', light: '#dcfce7' },
'public-course': { bg: '#f59e0b', light: '#fef3c7' },
'one-on-one': { bg: '#ec4899', light: '#fce7f3' },
'interview': { bg: '#3b82f6', light: '#dbeafe' },
'class': { bg: '#667eea', light: '#e0e7ff' },
'meeting': { bg: '#f093fb', light: '#fae8ff' },
'lab': { bg: '#fa709a', light: '#fce7f3' },
'exam': { bg: '#ff6b6b', light: '#fee2e2' },
};
// 处理遮罩层点击
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
// 处理关闭按钮点击
const handleCloseClick = () => {
onClose();
};
// 格式化时间显示
const formatTime = (timeStr) => {
if (!timeStr) return '';
// 处理多种格式: "2024-09-18 09:00:00" 或 "2024-09-18 09:00" 或 "09:00:00" 或 "09:00"
const parts = timeStr.toString().split(' ');
let time = '';
if (parts.length > 1) {
// 格式如 "2024-09-18 09:00:00" 或 "2024-09-18 09:00"
time = parts[1];
} else {
// 格式如 "09:00:00" 或 "09:00"
time = parts[0];
}
// 确保时间存在且格式正确
if (!time || time === 'undefined') {
return '00:00';
}
// 只显示小时和分钟 (前5个字符)
return time.substring(0, 5);
};
// 格式化日期
const formatDate = (date) => {
if (!date) return '';
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
return date.toLocaleDateString('zh-CN', options);
};
// 判断是否为多事件模式(点击日期时显示当天所有事件)
const isMultiEventMode = event.events && Array.isArray(event.events);
const events = isMultiEventMode ? event.events : [event];
const displayDate = isMultiEventMode ? formatDate(event.date) : '';
// 获取事件状态 - 所有事项都显示为已完成
const getEventStatus = (eventItem) => {
return { text: '已完成', icon: <IconCheck />, color: '#52c41a' };
};
return (
<Portal className="event-detail-portal">
<div className="event-detail-overlay" onClick={handleOverlayClick}>
<div className="event-detail-modal-new">
{/* 模态框头部 */}
<div className="event-detail-header-new">
<h3 className="event-detail-title-new">
{isMultiEventMode ? '日程详情' : '事件详情'}
</h3>
<button
className="event-detail-close-new"
onClick={handleCloseClick}
type="button"
aria-label="关闭"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
</button>
</div>
{/* 日期显示(多事件模式) */}
{isMultiEventMode && (
<div className="event-detail-date-header">
<IconCalendarClock style={{ fontSize: 20, color: '#3b82f6' }} />
<span>{displayDate}</span>
</div>
)}
{/* 模态框内容 */}
<div className="event-detail-content-new">
{events.length === 0 ? (
<div className="event-empty-state">
<IconCalendarClock style={{ fontSize: 48, color: '#c3c5c9', marginBottom: 16 }} />
<p style={{ fontSize: 16, color: '#86909c', margin: 0 }}>当日无事项</p>
</div>
) : (
<div className="event-list-container">
{events.map((eventItem, index) => {
const status = getEventStatus(eventItem);
const typeColor = eventTypeColorMap[eventItem.type] || { bg: '#667eea', light: '#e0e7ff' };
return (
<div key={eventItem.id || index} className="event-card-new">
<div className="event-card-header">
<div className="event-type-indicator" style={{ backgroundColor: typeColor.bg }}></div>
<div className="event-card-title">
<h4>{eventItem.title}</h4>
<span className="event-type-tag" style={{
backgroundColor: typeColor.light,
color: typeColor.bg
}}>
{eventTypeMap[eventItem.type] || eventItem.type}
</span>
</div>
</div>
<div className="event-card-body">
<div className="event-info-row">
<IconClockCircle style={{ fontSize: 16, color: '#8c8c8c' }} />
<span className="event-time">
{formatTime(eventItem.startTime)} - {formatTime(eventItem.endTime)}
</span>
</div>
<div className="event-info-row">
<div className="event-status" style={{ color: status.color }}>
{status.icon}
<span>{status.text}</span>
</div>
</div>
{/* 企业高管公开课添加线下参与标签 */}
{eventItem.type === 'public-course' && (
<div className="event-info-row" style={{ marginTop: '8px' }}>
<div style={{
padding: '4px 12px',
backgroundColor: '#f0f9ff',
color: '#0ea5e9',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
gap: '4px'
}}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
可报名线下参与
</div>
</div>
)}
{eventItem.description && (
<div className="event-description">
{eventItem.description}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</Portal>
);
};
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Portal from "@/components/Portal";
import { IconCalendarClock, IconCheck, IconClockCircle } from "@arco-design/web-react/icon";
const EventDetailModal = ({ isOpen, event, onClose }) => {
const navigate = useNavigate();
// ESC键关闭模态框
useEffect(() => {
const handleEscKey = (e) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
};
if (isOpen) {
document.addEventListener("keydown", handleEscKey);
}
return () => {
document.removeEventListener("keydown", handleEscKey);
};
}, [isOpen, onClose]);
// 如果未打开或无事件数据,不渲染
if (!isOpen || !event) {
return null;
}
// 事件类型映射
const eventTypeMap = {
'compound-skill': '复合技能课',
'vertical-skill': '垂直技能课',
'public-course': '公开课',
'one-on-one': '1v1规划',
'interview': '线下面试模拟',
'class': '课程',
'meeting': '会议',
'lab': '实验',
'exam': '考试',
};
// 事件类型颜色映射
const eventTypeColorMap = {
'compound-skill': { bg: '#667eea', light: '#e0e7ff' },
'vertical-skill': { bg: '#22c55e', light: '#dcfce7' },
'public-course': { bg: '#f59e0b', light: '#fef3c7' },
'one-on-one': { bg: '#ec4899', light: '#fce7f3' },
'interview': { bg: '#3b82f6', light: '#dbeafe' },
'class': { bg: '#667eea', light: '#e0e7ff' },
'meeting': { bg: '#f093fb', light: '#fae8ff' },
'lab': { bg: '#fa709a', light: '#fce7f3' },
'exam': { bg: '#ff6b6b', light: '#fee2e2' },
};
// 处理遮罩层点击
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
// 处理关闭按钮点击
const handleCloseClick = () => {
onClose();
};
// 格式化时间显示
const formatTime = (timeStr) => {
if (!timeStr) return '';
// 处理多种格式: "2024-09-18 09:00:00" 或 "2024-09-18 09:00" 或 "09:00:00" 或 "09:00"
const parts = timeStr.toString().split(' ');
let time = '';
if (parts.length > 1) {
// 格式如 "2024-09-18 09:00:00" 或 "2024-09-18 09:00"
time = parts[1];
} else {
// 格式如 "09:00:00" 或 "09:00"
time = parts[0];
}
// 确保时间存在且格式正确
if (!time || time === 'undefined') {
return '00:00';
}
// 只显示小时和分钟 (前5个字符)
return time.substring(0, 5);
};
// 格式化日期
const formatDate = (date) => {
if (!date) return '';
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
return date.toLocaleDateString('zh-CN', options);
};
// 判断是否为多事件模式(点击日期时显示当天所有事件)
const isMultiEventMode = event.events && Array.isArray(event.events);
const events = isMultiEventMode ? event.events : [event];
const displayDate = isMultiEventMode ? formatDate(event.date) : '';
// 获取事件状态 - 所有事项都显示为已完成
const getEventStatus = (eventItem) => {
return { text: '已完成', icon: <IconCheck />, color: '#52c41a' };
};
// 处理课程点击 - 跳转到对应的课程页面
const handleCourseClick = (eventItem) => {
// 构建URL参数
const params = new URLSearchParams();
if (eventItem.id) {
params.append('courseId', eventItem.id);
}
if (eventItem.title) {
params.append('courseTitle', eventItem.title);
}
// 根据课程类型跳转到不同页面
switch(eventItem.type) {
case 'compound-skill':
case 'vertical-skill':
// 复合技能课/垂直技能课 - 跳转到课程直播间
navigate(`/live?${params.toString()}`);
break;
case 'public-course':
// 公开课包括AI课、企业高管公开课、营销课 - 跳转到公共课直播间
navigate(`/public-courses?${params.toString()}`);
break;
case 'one-on-one':
// 1v1规划 - 跳转到定制求职策略页面
navigate('/job-strategy');
break;
case 'interview':
// 线下面试模拟 - 不跳转,仅关闭弹窗
break;
default:
// 其他类型暂不跳转
break;
}
// 关闭弹窗
onClose();
};
return (
<Portal className="event-detail-portal">
<div className="event-detail-overlay" onClick={handleOverlayClick}>
<div className="event-detail-modal-new">
{/* 模态框头部 */}
<div className="event-detail-header-new">
<h3 className="event-detail-title-new">
{isMultiEventMode ? '日程详情' : '课程详情'}
</h3>
<button
className="event-detail-close-new"
onClick={handleCloseClick}
type="button"
aria-label="关闭"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
</button>
</div>
{/* 日期显示(多事件模式) */}
{isMultiEventMode && (
<div className="event-detail-date-header">
<IconCalendarClock style={{ fontSize: 20, color: '#3b82f6' }} />
<span>{displayDate}</span>
</div>
)}
{/* 模态框内容 */}
<div className="event-detail-content-new">
{events.length === 0 ? (
<div className="event-empty-state">
<IconCalendarClock style={{ fontSize: 48, color: '#c3c5c9', marginBottom: 16 }} />
<p style={{ fontSize: 16, color: '#86909c', margin: 0 }}>当日无事项</p>
</div>
) : (
<div className="event-list-container">
{events.map((eventItem, index) => {
const status = getEventStatus(eventItem);
const typeColor = eventTypeColorMap[eventItem.type] || { bg: '#667eea', light: '#e0e7ff' };
return (
<div
key={eventItem.id || index}
className={`event-card-new ${eventItem.type !== 'interview' ? 'clickable-course' : ''}`}
onClick={eventItem.type !== 'interview' ? () => handleCourseClick(eventItem) : undefined}
style={{ cursor: eventItem.type !== 'interview' ? 'pointer' : 'default' }}
>
<div className="event-card-header">
<div className="event-type-indicator" style={{ backgroundColor: typeColor.bg }}></div>
<div className="event-card-title">
<h4>{eventItem.title}</h4>
<span className="event-type-tag" style={{
backgroundColor: typeColor.light,
color: typeColor.bg
}}>
{eventTypeMap[eventItem.type] || eventItem.type}
</span>
</div>
</div>
<div className="event-card-body">
<div className="event-info-row">
<IconClockCircle style={{ fontSize: 16, color: '#8c8c8c' }} />
<span className="event-time">
{formatTime(eventItem.startTime)} - {formatTime(eventItem.endTime)}
</span>
</div>
<div className="event-info-row">
<div className="event-status" style={{ color: status.color }}>
{status.icon}
<span>{status.text}</span>
</div>
</div>
{/* 企业高管公开课添加线下参与标签 */}
{eventItem.type === 'public-course' && (
<div className="event-info-row" style={{ marginTop: '8px' }}>
<div style={{
padding: '4px 12px',
backgroundColor: '#f0f9ff',
color: '#0ea5e9',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '500',
display: 'inline-flex',
alignItems: 'center',
gap: '4px'
}}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
可报名线下参与
</div>
</div>
)}
{eventItem.description && (
<div className="event-description">
{eventItem.description}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</Portal>
);
};
export default EventDetailModal;

View File

@@ -662,6 +662,19 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* 可点击的课程卡片样式 */
.event-card-new.clickable-course {
cursor: pointer;
transition: all 0.2s ease;
}
.event-card-new.clickable-course:hover {
background: #f3f4f6;
border-color: #4080ff;
box-shadow: 0 4px 12px rgba(64, 128, 255, 0.15);
transform: translateY(-2px);
}
/* 事件卡片头部 */
.event-card-header {
display: flex;

View File

@@ -277,25 +277,11 @@
.job-info-modal-content-position-info-companyInfo {
width: 100%;
box-sizing: border-box;
padding: 20px;
border-radius: 12px;
background: linear-gradient(135deg, #ffffff 0%, #f7f8fa 100%);
margin: 8px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(102, 126, 234, 0.1);
position: relative;
overflow: visible;
min-height: auto;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
padding: 16px;
border-radius: 8px;
background-color: #fff;
margin: 10px 0;
border: 1px solid #e5e6eb;
> p {
width: 100%;
@@ -304,47 +290,42 @@
.description-title,
.requirements-title,
.companyInfo-title {
font-size: 16px;
font-size: 18px;
font-weight: 600;
line-height: 24px;
line-height: 28px;
color: #1d2129;
margin-bottom: 16px;
padding-left: 12px;
position: relative;
margin-bottom: 12px;
display: flex;
align-items: center;
&:before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
.title-icon {
width: 20px;
height: 20px;
margin-right: 8px;
object-fit: contain;
}
}
.description-content {
font-size: 14px;
font-weight: 400;
line-height: 22px;
line-height: 24px;
color: #4e5969;
text-align: left;
.description-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
margin-bottom: 8px;
text-align: left;
.description-number {
display: inline-block;
min-width: 24px;
min-width: 20px;
font-size: 14px;
font-weight: 600;
color: #667eea;
margin-right: 8px;
font-weight: 500;
color: #1d2129;
margin-right: 6px;
text-align: left;
}
@@ -352,7 +333,7 @@
flex: 1;
font-size: 14px;
font-weight: 400;
line-height: 22px;
line-height: 24px;
color: #4e5969;
text-align: left;
}
@@ -368,16 +349,9 @@
font-weight: 400;
line-height: 24px;
color: #4e5969;
.company-paragraph {
margin-bottom: 12px;
text-indent: 2em;
text-align: justify;
&:last-child {
margin-bottom: 0;
}
}
text-align: left;
white-space: pre-wrap;
word-break: break-word;
}
.requirements-content {
width: 100%;
@@ -386,16 +360,16 @@
.requirements-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
margin-bottom: 8px;
text-align: left;
.requirement-number {
display: inline-block;
min-width: 24px;
min-width: 20px;
font-size: 14px;
font-weight: 600;
color: #667eea;
margin-right: 8px;
font-weight: 500;
color: #1d2129;
margin-right: 6px;
text-align: left;
}
@@ -403,7 +377,7 @@
flex: 1;
font-size: 14px;
font-weight: 400;
line-height: 22px;
line-height: 24px;
color: #4e5969;
text-align: left;
}

View File

@@ -269,7 +269,10 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
)}
{data?.details?.description && (
<div className="job-info-modal-content-position-info-description">
<p className="description-title">岗位描述</p>
<p className="description-title">
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
岗位描述
</p>
<div className="description-content">
{data?.details?.description.split(/\d+\.\s*/).filter(item => item.trim()).map((item, index) => (
<div key={index} className="description-item">
@@ -282,7 +285,10 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
)}
{(data?.details?.requirements?.length > 0 || data?.details?.requirementsText) && (
<div className="job-info-modal-content-position-info-requirements">
<p className="requirements-title">岗位要求</p>
<p className="requirements-title">
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
岗位要求
</p>
<div className="requirements-content">
{data?.details?.requirements ? (
data?.details?.requirements?.map((item, index) => (
@@ -304,7 +310,10 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
)}
{data?.details?.companyInfo && (
<div className="job-info-modal-content-position-info-companyInfo">
<p className="companyInfo-title">公司介绍</p>
<p className="companyInfo-title">
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
公司介绍
</p>
<div className="companyInfo-content">
{data?.details?.companyInfo.split('\n').map((paragraph, index) => (
<p key={index} className="company-paragraph">

View File

@@ -38,7 +38,12 @@ const Dashboard = () => {
// 根据选中日期筛选任务
const getTasksForDate = (date) => {
if (!dashboardData?.tasks?.allTasks) return [];
console.log("getTasksForDate called with date:", date);
console.log("dashboardData.tasks:", dashboardData?.tasks);
if (!dashboardData?.tasks?.allTasks) {
console.log("No allTasks found in dashboardData");
return [];
}
// Check if date is valid before calling getTime
if (!date || isNaN(date.getTime())) {
console.warn("Invalid date provided to getTasksForDate:", date);

View File

@@ -59,7 +59,7 @@ const ExpertSupportPage = () => {
{
id: 1,
type: "user",
content: "您好,我在使用TypeScript时遇到了一些泛型定义的问题",
content: "您好,我遇到了一些问题",
time: "2025-01-08 16:45",
avatar: "👤",
},
@@ -67,10 +67,10 @@ const ExpertSupportPage = () => {
id: 2,
type: "expert",
content:
"您好我是王开发专家很高兴为您解答TypeScript相关问题。请详细描述一下您遇到的具体问题。",
"你好我是多多ai智能问答小助手有什么问题就来问我吧",
time: "2025-01-08 16:46",
avatar: "👨‍💻",
expertName: "王开发专家",
expertName: "多多机器人",
},
{
id: 3,

View File

@@ -41,10 +41,30 @@
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
flex-wrap: nowrap;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f2f3f5;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #c9cdd4;
border-radius: 3px;
&:hover {
background: #86909c;
}
}
.unit-nav-item {
padding: 5px 14px;

View File

@@ -10,6 +10,7 @@ const HomeworkPage = () => {
const { homework } = mockData;
const scrollContainerRef = useRef(null);
const verticalScrollContainerRef = useRef(null);
const unitNavRefs = useRef({});
const [showIframe, setShowIframe] = useState(false);
const [selectedUnits, setSelectedUnits] = useState({
1: "全部", // 复合能力课的选中单元
@@ -27,7 +28,9 @@ const HomeworkPage = () => {
// 如果显示iframe不初始化滚动
if (showIframe) return;
const containers = [scrollContainerRef.current, verticalScrollContainerRef.current].filter(Boolean);
// 收集所有需要横向滚动的容器
const unitNavContainers = Object.values(unitNavRefs.current).filter(Boolean);
const containers = [scrollContainerRef.current, verticalScrollContainerRef.current, ...unitNavContainers].filter(Boolean);
if (containers.length === 0) return;
const animationIds = new Map();
@@ -177,7 +180,10 @@ const HomeworkPage = () => {
<p className="homework-page-content-list-title">{item.name}</p>
</div>
{item.units && (
<div className="homework-page-unit-nav">
<div
className="homework-page-unit-nav"
ref={el => unitNavRefs.current[item.id] = el}
>
<span
className={`unit-nav-item ${selectedUnits[item.id] === "全部" ? "active" : ""}`}
onClick={() => setSelectedUnits({...selectedUnits, [item.id]: "全部"})}

View File

@@ -4,11 +4,11 @@ import ScoreChart from "../ScoreChart";
import RadarChart from "../RadarChart";
import "./index.css";
export default ({ selectedItem = "求职面试初体验" }) => {
export default ({ selectedItem = "面试初体验" }) => {
// 根据选中项目获取对应的视频URL
const getVideoUrl = () => {
switch(selectedItem) {
case "求职面试初体验":
case "面试初体验":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_simulation/3years_ago.mov";
case "未来的自己":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_simulation/3years_later.mov";
@@ -146,7 +146,7 @@ export default ({ selectedItem = "求职面试初体验" }) => {
};
} else {
return {
totalScore: 14, // 根据JSON计算的真实总分 (求职面试初体验)
totalScore: 14, // 根据JSON计算的真实总分 (面试初体验)
professionalScore: 7, // (2+1+1+1+1+1)/6*10*0.6 = 7
performanceScore: 7, // (2+1+2+2)/4*10*0.4 = 7
radarData: [2, 1, 1, 1, 1, 1], // 六项专业能力指标来自JSON
@@ -177,7 +177,7 @@ export default ({ selectedItem = "求职面试初体验" }) => {
// 判断是否应该显示评价内容
const shouldShowEvaluation = () => {
return selectedItem === "求职面试初体验" ||
return selectedItem === "面试初体验" ||
selectedItem === "未来的自己" ||
selectedItem === "第一次线下面试模拟" ||
selectedItem === "第二次线下面试模拟" ||
@@ -331,9 +331,10 @@ export default ({ selectedItem = "求职面试初体验" }) => {
{ name: "仪表与职场礼仪", max: 10 },
{ name: "时间管理与条理性", max: 10 },
]}
lineClolr="#FFE4D9"
areaColor="#FFD4C1"
areaBorderColor="#FFB89A"
lineClolr="#E8F5E9"
areaColor="#C8E6C9"
areaBorderColor="#66BB6A"
isGreenTheme={true}
/>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import "./index.css";
const TimelineItem = Timeline.Item;
export default ({ onItemSelect }) => {
const [selectedItem, setSelectedItem] = useState("求职面试初体验");
const [selectedItem, setSelectedItem] = useState("面试初体验");
const handleItemClick = (itemName) => {
setSelectedItem(itemName);
@@ -29,16 +29,16 @@ export default ({ onItemSelect }) => {
<TimelineItem
lineType="dashed"
dot={
<div className={`time-line-dot-icon ${selectedItem === "求职面试初体验" ? "time-line-dot-icon-active" : ""}`} />
<div className={`time-line-dot-icon ${selectedItem === "面试初体验" ? "time-line-dot-icon-active" : ""}`} />
}
>
<div
className={`time-line-item ${selectedItem === "求职面试初体验" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("求职面试初体验")}
className={`time-line-item ${selectedItem === "面试初体验" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("面试初体验")}
style={{ cursor: "pointer" }}
>
<p>
求职面试初体验
面试初体验
<span style={{
marginLeft: '8px',
padding: '2px 8px',

View File

@@ -4,7 +4,7 @@ import InterviewRating from "./components/InterviewRating";
import "./index.css";
const InterviewSimulationPage = () => {
const [selectedItem, setSelectedItem] = useState("求职面试初体验");
const [selectedItem, setSelectedItem] = useState("面试初体验");
return (
<div className="interview-simulation-page">

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { Modal, Message } from "@arco-design/web-react";
import { Modal, Message, Tooltip } from "@arco-design/web-react";
import { useNavigate, useLocation } from "react-router-dom";
import {
DndContext,
@@ -344,9 +344,51 @@ export default ({ locked = false }) => {
<div className="target-position-wrapper">
<div className="target-position-content">
<div className="batch-icon">
<span>第一批次</span>
<span>批次</span>
<span>第三批次</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
批次
<Tooltip content="通过培训后能直接上岗的岗位,入职成功率最高。">
<span style={{ fontSize: '12px',
color: '#ffffff',
backgroundColor: '#4080ff',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold' }}>?</span>
</Tooltip>
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
第二批次
<Tooltip content="需积累一定工作经验后可争取的晋升岗位方向。">
<span style={{ fontSize: '12px',
color: '#ffffff',
backgroundColor: '#4080ff',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold' }}>?</span>
</Tooltip>
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
第三批次
<Tooltip content="需长期经验和能力沉淀,可作为学员的终极职业目标。">
<span style={{ fontSize: '12px',
color: '#ffffff',
backgroundColor: '#4080ff',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold' }}>?</span>
</Tooltip>
</span>
</div>
{/* 第一批次 */}

View File

@@ -41,7 +41,7 @@ const JobStrategyDetailPage = () => {
onClick={() => setActiveItem("1")}
>
<span className="nav-icon target-icon"></span>
<span className="nav-text">优先目标岗位</span>
<span className="nav-text">目标岗位优先级</span>
</div>
<div
className={`nav-item ${activeItem === "2" ? "item-active" : ""}`}

View File

@@ -7,7 +7,7 @@ const JobStrategyPage = () => {
<div className="job-strategy-page">
<CoursesVideoPlayer
isLock
backgroundImage="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuW8VeXQDVI2.jpg"
backgroundImage="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuWolCCOlOCz.jpg"
/>
<LiveSummary showBtn />
</div>

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import CoursesVideoPlayer from "@/components/CoursesVideoPlayer";
import CourseList from "@/components/CourseList";
import { mockData } from "@/data/mockData";
@@ -6,6 +7,29 @@ import "./index.css";
const LivePage = () => {
const [selectedCourse, setSelectedCourse] = useState(null);
const [searchParams] = useSearchParams();
// 检查URL参数如果有courseId或courseTitle则自动打开对应课程
useEffect(() => {
const courseId = searchParams.get('courseId');
const courseTitle = searchParams.get('courseTitle');
if (courseId || courseTitle) {
// 查找对应的课程
const allCourses = [
...(mockData.compoundSkillCourses || []),
...(mockData.verticalSkillCourses || [])
];
const targetCourse = allCourses.find(course =>
course.id === courseId || course.title === courseTitle
);
if (targetCourse) {
setSelectedCourse(targetCourse);
}
}
}, [searchParams]);
const handleCourseClick = (course) => {
setSelectedCourse(course);

View File

@@ -18,24 +18,6 @@ const Portfolio = () => {
return (
<div className="user-portfolio-page">
<button
onClick={handleRefresh}
style={{
position: "absolute",
top: "10px",
right: "10px",
zIndex: 1000,
padding: "8px 16px",
backgroundColor: "#3491fa",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "14px"
}}
>
刷新页面
</button>
{iframeSrc && (
<iframe
key={iframeSrc} // 使用key强制重新渲染iframe

View File

@@ -126,6 +126,7 @@
width: 210px;
height: 82px;
background-color: #f7f8fa;
transition: all 0.3s ease;
border-radius: 8px;
padding: 16px;
box-sizing: border-box;
@@ -137,6 +138,13 @@
cursor: pointer;
flex-shrink: 0;
&:hover {
background-color: #f2f3f5;
border-color: #4080ff;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
> span {
font-size: 12px;
line-height: 20px;

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import Modal from "@/components/Modal";
import PDFICON from "@/assets/images/Common/pdf_icon.png";
import FileIcon from "@/components/FileIcon";
@@ -7,6 +8,7 @@ import ImagePreviewModal from "../ImagePreviewModal";
import "./index.css";
export default ({ visible, onClose, data }) => {
const navigate = useNavigate();
const [previewVisible, setPreviewVisible] = useState(false);
const [previewIndex, setPreviewIndex] = useState(0);
@@ -14,6 +16,14 @@ export default ({ visible, onClose, data }) => {
onClose();
};
// 处理岗位点击,跳转到简历与面试题页面
const handlePositionClick = (positionName) => {
// 关闭当前模态框
onClose();
// 跳转到简历与面试题页面,并传递岗位名称参数
navigate('/resume-interview', { state: { selectedPosition: positionName } });
};
// 处理图片点击预览
const handleImageClick = (index) => {
setPreviewIndex(index);
@@ -91,7 +101,12 @@ export default ({ visible, onClose, data }) => {
<p className="project-cases-modal-item-title">适用岗位</p>
<ul className="project-cases-modal-horizontal-list">
{data?.applicablePositions?.map((pos, index) => (
<li key={index} className="high-count-list-item">
<li
key={index}
className="high-count-list-item"
onClick={() => handlePositionClick(pos.position)}
style={{ cursor: 'pointer' }}
>
<span className={pos.level === '普通岗' ? 'low' : pos.level === '技术骨干岗' ? 'medium' : 'high'}>
{pos.level}
</span>
@@ -108,23 +123,72 @@ export default ({ visible, onClose, data }) => {
{/* 对应单元 */}
<li className="project-cases-modal-item">
<p className="project-cases-modal-item-title">对应单元</p>
<ul className="project-cases-modal-horizontal-list">
{data?.units?.map((unit, index) => (
<li key={index} className="class-list-item">
<div className="class-list-item-title">
<i />
<span>{unit}</span>
</div>
</li>
)) || (
<li className="class-list-item">
<div className="class-list-item-title">
<i />
<span>暂无对应单元信息</span>
</div>
</li>
)}
</ul>
{/* 复合能力课 */}
<div className="unit-category-section">
<p className="unit-category-subtitle">复合能力课</p>
<ul className="project-cases-modal-horizontal-list">
{data?.units?.filter(unit =>
unit.includes('商业活动') ||
unit.includes('营销') ||
unit.includes('品牌') ||
unit.includes('项目全周期')
).map((unit, index) => (
<li key={`compound-${index}`} className="class-list-item">
<div className="class-list-item-title">
<i />
<span>{unit}</span>
</div>
</li>
)) || null}
{(!data?.units || data?.units?.filter(unit =>
unit.includes('商业活动') ||
unit.includes('营销') ||
unit.includes('品牌') ||
unit.includes('项目全周期')
).length === 0) && (
<li className="class-list-item">
<div className="class-list-item-title">
<i />
<span>暂无复合能力课信息</span>
</div>
</li>
)}
</ul>
</div>
{/* 垂直能力课 */}
<div className="unit-category-section">
<p className="unit-category-subtitle">垂直能力课</p>
<ul className="project-cases-modal-horizontal-list">
{data?.units?.filter(unit =>
!unit.includes('商业活动') &&
!unit.includes('营销') &&
!unit.includes('品牌') &&
!unit.includes('项目全周期')
).map((unit, index) => (
<li key={`vertical-${index}`} className="class-list-item">
<div className="class-list-item-title">
<i />
<span>{unit}</span>
</div>
</li>
)) || null}
{(!data?.units || data?.units?.filter(unit =>
!unit.includes('商业活动') &&
!unit.includes('营销') &&
!unit.includes('品牌') &&
!unit.includes('项目全周期')
).length === 0) && (
<li className="class-list-item">
<div className="class-list-item-title">
<i />
<span>暂无垂直能力课信息</span>
</div>
</li>
)}
</ul>
</div>
</li>
{/* 项目整体流程介绍 - Markdown格式 */}
<li className="project-cases-modal-item">

View File

@@ -0,0 +1,301 @@
/*/* 全局样式控制弹窗容器 */
.upload-modal.arco-modal-wrapper {
.arco-modal {
width: 720px !important;
max-width: 90vw;
}
}
/* 强制控制 Arco Modal 外层容器为横向矩形 */
body {
[data-focus-lock-disabled="false"] {
.arco-modal-wrapper {
.arco-modal {
width: 960px !important;
max-width: 90vw;
height: auto !important;
}
}
}
}
/*/* 针对上传弹窗的特定样式 */
.arco-modal-wrapper:has(.upload-modal-content) {
.arco-modal {
width: 720px !important;
height: auto !important;
max-width: 90vw;
}
}
.upload-modal {
.arco-modal {
/* 横向矩形布局 - 16:9 比例 */
width: 720px !important;
.arco-modal-content {
height: 420px;
max-height: 420px;
display: flex;
flex-direction: column;
}
}
.arco-modal-header {
border-bottom: 1px solid #e5e6eb;
padding: 20px 24px;
flex-shrink: 0;
}
.arco-modal-title {
font-size: 18px;
font-weight: 600;
color: #1d2129;
}
.arco-modal-body {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.upload-modal-content {
height: 100%;
display: flex;
flex-direction: column;
.upload-header {
padding: 24px 24px 20px;
background: linear-gradient(180deg, #f7f8fa 0%, #ffffff 100%);
h3 {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 8px;
}
p {
font-size: 14px;
color: #86909c;
}
}
.upload-area {
margin: 20px 24px;
flex: 1;
max-height: 150px;
min-height: 120px;
border: 2px dashed #e5e6eb;
border-radius: 12px;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
background: linear-gradient(to bottom, #fafafa, #f5f5f5);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg,
transparent 48%,
rgba(22, 93, 255, 0.03) 49%,
rgba(22, 93, 255, 0.03) 51%,
transparent 52%
);
background-size: 20px 20px;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover {
border-color: #165dff;
background-color: #f0f7ff;
&::before {
opacity: 1;
}
.upload-icon {
color: #165dff;
}
}
&.dragging {
border-color: #165dff;
background-color: #e8f4ff;
&::before {
opacity: 1;
}
}
&.has-file {
border-color: #00b42a;
background-color: #ffffff;
border-style: solid;
}
.upload-placeholder {
width: 100%;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
.upload-icon {
font-size: 48px;
color: #c9cdd4;
}
.upload-text {
font-size: 16px;
color: #4e5969;
font-weight: 500;
margin: 0;
}
.upload-hint {
font-size: 14px;
color: #86909c;
margin: 0;
}
}
.uploaded-file-display {
width: 100%;
padding: 15px 24px;
display: flex;
align-items: center;
gap: 16px;
.file-icon-wrapper {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.file-icon {
font-size: 24px;
color: #ffffff;
}
}
.file-info {
flex: 1;
min-width: 0;
.file-name {
font-size: 15px;
color: #1d2129;
font-weight: 500;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
.file-size {
color: #86909c;
}
.file-status {
color: #00b42a;
padding: 2px 8px;
background: #f0ffef;
border-radius: 4px;
font-size: 12px;
}
}
}
.remove-file-btn {
width: 32px;
height: 32px;
border: none;
background: #f2f3f5;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #86909c;
&:hover {
background: #ff4d4f;
color: #ffffff;
}
svg {
font-size: 16px;
}
}
}
}
}
.arco-modal-footer {
border-top: 1px solid #e5e6eb;
padding: 16px 24px;
flex-shrink: 0;
background: linear-gradient(to bottom, #ffffff, #fafafa);
.arco-btn {
min-width: 100px;
height: 36px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
transition: all 0.3s ease;
&.arco-btn-default {
background: #ffffff;
border: 1px solid #e5e6eb;
color: #4e5969;
&:hover {
background: #f7f8fa;
border-color: #c9cdd4;
color: #1d2129;
}
}
&.arco-btn-primary {
background: linear-gradient(135deg, #165dff 0%, #3c7eff 100%);
border: none;
color: #ffffff;
box-shadow: 0 2px 8px rgba(22, 93, 255, 0.2);
&:hover {
background: linear-gradient(135deg, #1450db 0%, #3673ff 100%);
box-shadow: 0 4px 12px rgba(22, 93, 255, 0.3);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(22, 93, 255, 0.2);
}
}
}
}
}

View File

@@ -0,0 +1,161 @@
import { useState, useRef } from "react";
import { Modal } from "@arco-design/web-react";
import { IconUpload, IconFile, IconDelete } from "@arco-design/web-react/icon";
import toast from "@/components/Toast";
import "./index.css";
const UploadModal = ({ visible, onClose }) => {
const [isDragging, setIsDragging] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
const fileInputRef = useRef(null);
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect(files[0]);
}
};
const handleFileSelect = (file) => {
// 验证文件类型
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'text/markdown',
'text/x-markdown'
];
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.md', '.markdown'];
const fileExtension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) {
toast.error("请上传 PDF、Word、Markdown 或 TXT 格式的文件");
return;
}
// 验证文件大小限制为10MB
if (file.size > 10 * 1024 * 1024) {
toast.error("文件大小不能超过10MB");
return;
}
setUploadedFile(file);
toast.success(`文件 ${file.name} 已选择`);
};
const handleFileInputChange = (e) => {
const file = e.target.files[0];
if (file) {
handleFileSelect(file);
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleSubmit = () => {
if (!uploadedFile) {
toast.error("请先选择要上传的文件");
return;
}
// 这里可以添加实际的上传逻辑
toast.success("文件上传成功");
setUploadedFile(null);
onClose();
};
const handleCancel = () => {
setUploadedFile(null);
onClose();
};
return (
<Modal
title={false}
visible={visible}
onOk={handleSubmit}
onCancel={handleCancel}
okText="确认上传"
cancelText="取消"
style={{ width: '720px' }}
width={720}
className="upload-modal"
width={960}
maskClosable={false}
>
<div className="upload-modal-content">
<div className="upload-header">
<h3>选择文件上传</h3>
<p>请选择您的项目文档进行上传支持批量上传多个文件</p>
</div>
<div
className={`upload-area ${isDragging ? 'dragging' : ''} ${uploadedFile ? 'has-file' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleUploadClick}
>
<input
ref={fileInputRef}
type="file"
accept=".pdf,.doc,.docx,.txt,.md,.markdown"
onChange={handleFileInputChange}
style={{ display: 'none' }}
/>
{uploadedFile ? (
<div className="uploaded-file-display">
<div className="file-icon-wrapper">
<IconFile className="file-icon" />
</div>
<div className="file-info">
<p className="file-name">{uploadedFile.name}</p>
<p className="file-meta">
<span className="file-size">
{(uploadedFile.size / 1024 / 1024).toFixed(2)} MB
</span>
<span className="file-status">准备上传</span>
</p>
</div>
<button
className="remove-file-btn"
onClick={(e) => {
e.stopPropagation();
setUploadedFile(null);
}}
>
<IconDelete />
</button>
</div>
) : (
<div className="upload-placeholder">
<IconUpload className="upload-icon" />
<p className="upload-text">点击选择文件或将文件拖拽至此处</p>
<p className="upload-hint">支持 PDFWordMarkdownTXT 格式单个文件不超过 10MB</p>
</div>
)}
</div>
</div>
</Modal>
);
};
export default UploadModal;

View File

@@ -14,18 +14,50 @@
padding: 20px;
overflow: hidden;
.project-library-title {
/* 项目库头部样式 */
.project-library-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 20px;
border-bottom: 1px solid #e5e6eb;
padding-bottom: 10px;
.upload-button {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background-color: #f7f8fa;
border: 1px solid #e5e6eb;
border-radius: 6px;
color: #86909c;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
svg {
font-size: 16px;
}
&:hover {
background-color: #e8f4ff;
border-color: #165dff;
color: #165dff;
}
}
}
.project-library-title {
width: auto;
height: 42px;
font-size: 20px;
font-weight: 600;
line-height: 30px;
margin-bottom: 20px;
color: #1d2129;
flex-shrink: 0;
position: relative;
border-bottom: 1px solid #e5e6eb;
padding-bottom: 10px;
&::after {
content: "";
@@ -40,6 +72,37 @@
}
}
/* 项目分类导航栏样式 */
.project-category-nav {
display: flex;
gap: 20px;
margin-bottom: 20px;
border-bottom: 1px solid #e5e6eb;
padding-bottom: 15px;
.category-item {
padding: 8px 16px;
border-radius: 20px;
background-color: #f7f8fa;
color: #86909c;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
white-space: nowrap;
&:hover {
background-color: #e8f4ff;
color: #165dff;
}
&.active {
background-color: #165dff;
color: #ffffff;
}
}
}
.project-library-search-area {
width: 100%;
height: 36px;
@@ -157,8 +220,8 @@
}
}
}
.my-project-library {
.my-project-library {
margin-top: 20px;
.project-library-empty {
@@ -237,7 +300,33 @@
color: #1d4ed8 !important;
}
}
}
/* 全局控制上传弹窗的外层容器为横向矩形 */
:global([data-focus-lock-disabled="false"]) {
:global(.arco-modal-wrapper) {
:global(.arco-modal) {
width: 720px !important;
max-width: 90vw !important;
}
}
}
/* 确保上传弹窗是横向矩形 */
:global(.arco-modal-wrapper) {
&:has(.upload-modal-content) {
:global(.arco-modal) {
width: 720px !important;
max-width: 90vw !important;
height: auto !important;
:global(.arco-modal-content) {
height: 420px !important;
max-height: 420px !important;
}
}
}
}
}
}
}

View File

@@ -1,9 +1,11 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { Tooltip } from "@arco-design/web-react";
import toast from "@/components/Toast";
import InfiniteScroll from "@/components/InfiniteScroll";
import ProjectCasesModal from "./components/ProjectCasesModal";
import UploadModal from "./components/UploadModal";
import { getProjectsList, getProjectsdetail } from "@/services/projectLibrary";
import { IconUpload } from "@arco-design/web-react/icon";
// 我的项目库数据
const myProjectsData = [
{
@@ -91,7 +93,7 @@ import "./index.css";
const PAGE_SIZE = 10;
const ProjectLibrary = () => {
const ProjectLibraryPage = () => {
// 处理我的项目数据
const processMyProjects = () => {
const projects = [];
@@ -201,8 +203,19 @@ const ProjectLibrary = () => {
useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [selectedCategory, setSelectedCategory] = useState("全部");
const [uploadModalVisible, setUploadModalVisible] = useState(false);
// 项目分类基于数据中的direction字段
const categories = ["全部", "项目经营管理", "商业活动策划", "文化服务"];
// 根据分类过滤项目
const filteredProjects = useMemo(() => {
if (selectedCategory === "全部") {
return projectList;
}
return projectList.filter(project => project.direction === selectedCategory);
}, [selectedCategory, projectList]);
const handleProjectClick = async (item) => {
// 如果是我的普通项目,不允许点击
@@ -261,13 +274,26 @@ const ProjectLibrary = () => {
<div className="project-library-page">
<div className="project-library-wrapper">
<p className="project-library-title">文旅班级项目库</p>
{/* 项目分类导航栏 */}
<div className="project-category-nav">
{categories.map((category) => (
<span
key={category}
className={`category-item ${selectedCategory === category ? 'active' : ''}`}
onClick={() => setSelectedCategory(category)}
>
{category}
</span>
))}
</div>
<InfiniteScroll
loadMore={fetchProjects}
hasMore={hasMore}
empty={projectList.length === 0}
className="project-library-list"
>
{projectList.map((item) => (
{filteredProjects.map((item) => (
<li className="project-library-item" key={item.id}>
<p className="project-library-item-title">{item.description}</p>
<div>
@@ -281,7 +307,16 @@ const ProjectLibrary = () => {
{/* 我的项目库板块 */}
<div className="project-library-wrapper my-project-library">
<p className="project-library-title">我完成的项目库</p>
<div className="project-library-header">
<p className="project-library-title">我完成的项目库</p>
<button
className="upload-button"
onClick={() => setUploadModalVisible(true)}
>
<IconUpload />
<span>上传</span>
</button>
</div>
<div className="project-library-list">
{/* 可点击的特殊项目 */}
{clickableProjects.map((item) => (
@@ -322,8 +357,13 @@ const ProjectLibrary = () => {
visible={projectCasesModalVisible}
onClose={handleCloseModal}
/>
<UploadModal
visible={uploadModalVisible}
onClose={() => setUploadModalVisible(false)}
/>
</div>
);
};
export default ProjectLibrary;
export default ProjectLibraryPage;

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import CoursesVideoPlayer from "@/components/CoursesVideoPlayer";
import PublicCourseList from "@/components/PublicCourseList";
import { mockData } from "@/data/mockData";
@@ -7,6 +8,25 @@ import "./index.css";
const PublicCourses = () => {
// 默认不选中任何课程,显示黑屏状态
const [selectedCourse, setSelectedCourse] = useState(null);
const [searchParams] = useSearchParams();
// 检查URL参数如果有courseId则自动打开对应课程
useEffect(() => {
const courseId = searchParams.get('courseId');
const courseTitle = searchParams.get('courseTitle');
if (courseId || courseTitle) {
// 查找对应的课程
const publicCourses = mockData.publicCourses || [];
const targetCourse = publicCourses.find(course =>
course.id === courseId || course.title === courseTitle
);
if (targetCourse) {
setSelectedCourse(targetCourse);
}
}
}, [searchParams]);
const handleCourseClick = (course) => {
setSelectedCourse(course);

View File

@@ -1,4 +1,5 @@
import { useRef, useState, useEffect } from "react";
import { useLocation } from "react-router-dom";
import { Spin, Empty } from "@arco-design/web-react";
import toast from "@/components/Toast";
import InterviewQuestionsModal from "./components/InterviewQuestionsModal";
@@ -14,6 +15,7 @@ import QuestionIcon from "@/assets/images/ResumeInterviewPage/question_icon2.png
import "./index.css";
const ResumeInterviewPage = () => {
const location = useLocation();
const [activeIndustry, setActiveIndustry] = useState("frontend");
const [interviewModalVisible, setInterviewModalVisible] = useState(false);
const [resumeModalVisible, setResumeModalVisible] = useState(false);
@@ -117,6 +119,7 @@ const ResumeInterviewPage = () => {
title: position.title,
content: selectedTemplate?.content || selectedTemplate?.oldContent || null,
studentResume: pageData.myResume,
selectedTemplate: selectedTemplate, // 添加整个模板数据包括studentInfo
};
setResumeModalData(resumeData);
@@ -161,6 +164,31 @@ const ResumeInterviewPage = () => {
fetchPageData();
}, []);
// 处理从项目库页面传递过来的岗位参数
useEffect(() => {
if (location.state?.selectedPosition && pageData) {
const positionName = location.state.selectedPosition;
// 遍历所有行业查找对应的岗位
for (const industry of pageData.industries) {
const position = industry.positions?.find(p => p.title === positionName);
if (position) {
// 找到岗位后,自动打开简历详情
handlePositionClick(position, industry);
// 滚动到对应的行业区域
setActiveIndustry(industry.id);
setTimeout(() => {
sectionsRef.current[industry.id]?.scrollIntoView({
behavior: "smooth",
block: "start",
});
}, 100);
break;
}
}
}
}, [location.state, pageData]);
// 监听滚动位置更新导航状态
useEffect(() => {
if (!pageData?.industries) return;

View File

@@ -67,6 +67,14 @@ export default [
key: "course",
showMenu: true,
routes: [
{
path: "/career-tree",
name: "就业管家知识树",
element: <CareerTreePage />,
default: "recuUY59K6SkoO",
active: "recuUY59l98AZg",
showMenuItem: true,
},
{
path: "/public-courses",
name: "公共课直播间",
@@ -83,22 +91,29 @@ export default [
active: "recuUY5abx7Arx",
showMenuItem: true,
},
{
path: "/career-tree",
name: "就业管家知识树",
element: <CareerTreePage />,
default: "recuUY59K6SkoO",
active: "recuUY59l98AZg",
showMenuItem: true,
},
{
path: "/homework",
name: "课作业",
name: "课作业",
element: <HomeworkPage />,
default: "recuUY5bpDGGa2",
active: "recuUY5aZHbzCC",
showMenuItem: true,
},
{
path: "/expert-support",
name: "专家支持中心",
element: <ExpertSupportPage />,
default: "recuUY5hQnbcBY",
active: "recuUY5hsxnmk6",
showMenuItem: true,
},
],
},
{
name: "求职",
key: "job",
showMenu: true,
routes: [
{
path: "/job-strategy",
name: "定制求职策略",
@@ -121,21 +136,6 @@ export default [
active: "recuUY5fQqg5MD",
showMenuItem: true,
},
],
},
{
name: "资源",
key: "resource",
showMenu: true,
routes: [
{
path: "/expert-support",
name: "专家支持中心",
element: <ExpertSupportPage />,
default: "recuUY5hQnbcBY",
active: "recuUY5hsxnmk6",
showMenuItem: true,
},
{
path: "/company-jobs",
name: "企业内推岗位",
@@ -150,9 +150,16 @@ export default [
element: <CompanyJobsListPage />,
showMenu: false,
},
],
},
{
name: "资源",
key: "resource",
showMenu: true,
routes: [
{
path: "/resume-interview",
name: "我的简历与面试",
name: "简历与面试",
element: <ResumeInterviewPage />,
default: "recuUY5dTT3xbx",
active: "recuUY5dvQEYlx",