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

@@ -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;