feat: 多项功能优化和bug修复

- 修复mockData.js语法错误,恢复项目正常运行
- 优化求职策略详情页拖拽功能,修复重复bug和保存逻辑
- 更新岗位级别名称:初级→普通岗,中级→技术骨干岗,高级→储备干部岗
- 更新个人档案学习时长:我的273小时(100%),班级平均231小时
- 面试模拟页面添加视频锁定界面和背景图片
- 简历面试页面更新模板数据,添加更多岗位简历模板
- React版本降级至18.3.1解决兼容性问题
This commit is contained in:
KQL
2025-09-06 10:06:19 +08:00
parent 4e0e96e6b8
commit 00e8cebfe3
19 changed files with 1344 additions and 394 deletions

View File

@@ -112,7 +112,71 @@ export default ({ selectedItem = "求职面试初体验" }) => {
</div>
<div className="interview-rating-video">
{isLockedItem() ? (
<img src="/线下面试模拟锁定.png" alt="线下面试模拟锁定" style={{width: "100%", height: "100%", objectFit: "cover"}} />
<div className="locked-video-container" style={{
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden'
}}>
{/* 背景图片 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage: `url(${
selectedItem === "第一次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第一次线下面试模拟.jpg"
: selectedItem === "第二次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第二次线下面试模拟.jpg"
: "/src/assets/images/InterviewSimulationPage/第三次线下面试模拟.jpg"
})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(20px)',
transform: 'scale(1.1)'
}} />
{/* 半透明遮罩 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.4)'
}} />
{/* 锁图标和文字 */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px'
}}>
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuVOrz2GnJdK.png"
alt="lock"
style={{ width: '280px', height: '280px' }}
/>
<span style={{
color: '#fff',
fontSize: '16px',
fontWeight: '500',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: '8px 16px',
borderRadius: '4px'
}}>
DEMO演示非学员无查看权限
</span>
</div>
</div>
) : (
<video src={getVideoUrl()} controls></video>
)}

View File

@@ -2,6 +2,17 @@
width: 100%;
height: 100%;
position: relative;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
* {
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}
.target-position-content {
width: 914px;
@@ -95,15 +106,19 @@
align-items: center;
margin-right: 15px;
flex-shrink: 0;
cursor: pointer;
cursor: grab;
transition: all 0.3s ease;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
&:active {
cursor: grabbing;
}
&:hover {
transform: translateY(-3px);
.student-avatar {
box-shadow: 0 6px 16px rgba(44, 127, 255, 0.25);
}
}
/* 仅在悬停岗位名称时显示气泡 */
@@ -124,11 +139,15 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 2px solid #ffffff;
transition: all 0.3s ease;
pointer-events: auto;
img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
}

View File

@@ -1,13 +1,211 @@
import { useEffect, useRef } from "react";
import { Avatar } from "@arco-design/web-react";
import { useEffect, useRef, useState } from "react";
import { Modal, Message } from "@arco-design/web-react";
import { useNavigate, useLocation } from "react-router-dom";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
horizontalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import Locked from "@/components/Locked";
import jobLevelData from "@/data/joblevel.json";
import "./index.css";
// 可排序的岗位组件
const SortablePosition = ({ id, position, getPositionAvatar }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
userSelect: 'none',
WebkitUserSelect: 'none',
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="avatar-wrapper"
>
<div className="student-avatar">
<img
alt="avatar"
src={getPositionAvatar(position)}
draggable={false}
style={{ userSelect: 'none', WebkitUserSelect: 'none', pointerEvents: 'none' }}
/>
</div>
<span className="student-name" style={{ userSelect: 'none', WebkitUserSelect: 'none' }}>{position}</span>
<div className="position-tooltip">{position}</div>
</div>
);
};
export default ({ locked = false }) => {
const navigate = useNavigate();
const location = useLocation();
const batch1Ref = useRef(null);
const batch2Ref = useRef(null);
const batch3Ref = useRef(null);
const [hasChanges, setHasChanges] = useState(false);
const [showSaveModal, setShowSaveModal] = useState(false);
const [pendingNavigation, setPendingNavigation] = useState(null);
const [activeId, setActiveId] = useState(null);
// 处理拖拽开始
const handleDragStart = (event) => {
const { active } = event;
setActiveId(active.id);
};
// 处理拖拽结束
const handleDragEnd = (event) => {
const { active, over } = event;
setActiveId(null);
if (!over) return;
const activePosition = active.id.split('-').slice(1).join('-');
const activeBatch = active.id.split('-')[0];
// 确定目标批次
let targetBatch = over.id.split('-')[0];
let targetPosition = over.id.split('-').slice(1).join('-');
// 如果目标和源相同,不做任何操作
if (active.id === over.id) return;
setBatchPositions((prev) => {
const newPositions = { ...prev };
// 如果是同一批次内的移动
if (activeBatch === targetBatch) {
const batch = [...newPositions[activeBatch]];
const activeIndex = batch.indexOf(activePosition);
const overIndex = batch.indexOf(targetPosition);
if (activeIndex !== -1 && overIndex !== -1 && activeIndex !== overIndex) {
// 移除原位置
batch.splice(activeIndex, 1);
// 计算新位置索引
const newOverIndex = activeIndex < overIndex ? overIndex - 1 : overIndex;
// 插入到新位置
batch.splice(newOverIndex, 0, activePosition);
newPositions[activeBatch] = batch;
}
} else {
// 跨批次移动
// 从原批次删除
const sourceBatch = [...newPositions[activeBatch]];
const activeIndex = sourceBatch.indexOf(activePosition);
if (activeIndex !== -1) {
sourceBatch.splice(activeIndex, 1);
newPositions[activeBatch] = sourceBatch;
}
// 添加到目标批次
const targetBatchArray = [...newPositions[targetBatch]];
const overIndex = targetBatchArray.indexOf(targetPosition);
if (overIndex !== -1) {
// 插入到特定位置
targetBatchArray.splice(overIndex, 0, activePosition);
} else {
// 如果目标批次为空或找不到目标位置,添加到末尾
targetBatchArray.push(activePosition);
}
newPositions[targetBatch] = targetBatchArray;
}
return newPositions;
});
setHasChanges(true);
};
// 处理拖拽到其他批次 - 仅用于预览,不实际移动
const handleDragOver = (event) => {
// 空函数 - 我们只在dragEnd时处理实际的移动
};
// 监听路由变化
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasChanges) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasChanges]);
// 拦截导航 - 监听所有可能的页面切换
useEffect(() => {
if (!hasChanges) return;
const handleNavigation = (e) => {
// 如果点击的是弹窗内的元素,不拦截
if (e.target.closest('.arco-modal')) {
return;
}
// 检查是否是链接点击
const link = e.target.closest('a') || (e.target.tagName === 'A' ? e.target : null);
const button = e.target.closest('button') || (e.target.tagName === 'BUTTON' ? e.target : null);
// 检查是否是导航相关的元素
if (link || (button && (button.textContent?.includes('返回') || button.onclick))) {
e.preventDefault();
e.stopPropagation();
setShowSaveModal(true);
if (link) {
setPendingNavigation(link.href);
}
}
};
// 监听点击事件(捕获阶段)
document.addEventListener('click', handleNavigation, true);
// 监听浏览器后退/前进
const handlePopState = (e) => {
if (hasChanges) {
e.preventDefault();
setShowSaveModal(true);
}
};
window.addEventListener('popstate', handlePopState);
return () => {
document.removeEventListener('click', handleNavigation, true);
window.removeEventListener('popstate', handlePopState);
};
}, [hasChanges]);
useEffect(() => {
// 添加鼠标滚轮事件监听,实现横向滚动
@@ -49,7 +247,7 @@ export default ({ locked = false }) => {
};
// 定义三个批次的岗位数据
const batchPositions = {
const initialBatchPositions = {
batch1: [
"二次元周边店店员",
"会展执行助理",
@@ -107,66 +305,308 @@ export default ({ locked = false }) => {
]
};
const [batchPositions, setBatchPositions] = useState(initialBatchPositions);
// 拖拽传感器设置
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// 获取所有岗位ID
const getAllPositionIds = () => {
const ids = [];
Object.entries(batchPositions).forEach(([batch, positions]) => {
positions.forEach(position => {
ids.push(`${batch}-${position}`);
});
});
return ids;
};
return (
<div className="target-position-wrapper">
<div className="target-position-content">
<div className="batch-icon">
<span>第一批次</span>
<span>第二批次</span>
<span>第三批次</span>
</div>
<div className="batch-content batch-content1" ref={batch1Ref}>
{batchPositions.batch1.map((position, index) => (
<div key={index} className="avatar-wrapper">
<div className="student-avatar">
<img
alt="avatar"
src={getPositionAvatar(position)}
/>
</div>
<span className="student-name">{position}</span>
{/* 完整名称的气泡提示 */}
<div className="position-tooltip">{position}</div>
<div style={{ userSelect: 'none', WebkitUserSelect: 'none', MozUserSelect: 'none', msUserSelect: 'none' }}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
>
<div className="target-position-wrapper">
<div className="target-position-content">
<div className="batch-icon">
<span>第一批次</span>
<span>第二批次</span>
<span>第三批次</span>
</div>
))}
</div>
<div className="batch-content batch-content2" ref={batch2Ref}>
{batchPositions.batch2.map((position, index) => (
<div key={index} className="avatar-wrapper">
<div className="student-avatar">
<img
alt="avatar"
src={getPositionAvatar(position)}
/>
</div>
<span className="student-name">{position}</span>
{/* 完整名称的气泡提示 */}
<div className="position-tooltip">{position}</div>
{/* 第一批次 */}
<div className="batch-content batch-content1" ref={batch1Ref}>
<SortableContext
items={batchPositions.batch1.map(p => `batch1-${p}`)}
strategy={horizontalListSortingStrategy}
>
{batchPositions.batch1.map((position) => (
<SortablePosition
key={`batch1-${position}`}
id={`batch1-${position}`}
position={position}
getPositionAvatar={getPositionAvatar}
/>
))}
</SortableContext>
</div>
))}
</div>
<div className="batch-content batch-content3" ref={batch3Ref}>
{batchPositions.batch3.map((position, index) => (
<div key={index} className="avatar-wrapper">
<div className="student-avatar">
<img
alt="avatar"
src={getPositionAvatar(position)}
/>
</div>
<span className="student-name">{position}</span>
{/* 完整名称的气泡提示 */}
<div className="position-tooltip">{position}</div>
{/* 第二批次 */}
<div className="batch-content batch-content2" ref={batch2Ref}>
<SortableContext
items={batchPositions.batch2.map(p => `batch2-${p}`)}
strategy={horizontalListSortingStrategy}
>
{batchPositions.batch2.map((position) => (
<SortablePosition
key={`batch2-${position}`}
id={`batch2-${position}`}
position={position}
getPositionAvatar={getPositionAvatar}
/>
))}
</SortableContext>
</div>
{/* 第三批次 */}
<div className="batch-content batch-content3" ref={batch3Ref}>
<SortableContext
items={batchPositions.batch3.map(p => `batch3-${p}`)}
strategy={horizontalListSortingStrategy}
>
{batchPositions.batch3.map((position) => (
<SortablePosition
key={`batch3-${position}`}
id={`batch3-${position}`}
position={position}
getPositionAvatar={getPositionAvatar}
/>
))}
</SortableContext>
</div>
))}
</div>
{locked && (
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" />
)}
</div>
{locked && (
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" />
)}
</div>
</div>
{/* 拖拽覆盖层 */}
<DragOverlay>
{activeId ? (
<div
style={{
cursor: 'grabbing',
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'none'
}}
>
<div
style={{
position: 'relative',
width: '64px',
height: '64px',
borderRadius: '50%',
overflow: 'hidden',
backgroundColor: '#ffffff',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
border: '2px solid #ffffff'
}}
>
<img
alt="avatar"
src={getPositionAvatar(activeId.split('-').slice(1).join('-'))}
draggable={false}
style={{
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'none',
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/>
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
{/* 保存提示模态框 */}
<Modal
title={
<div style={{
fontSize: '18px',
fontWeight: '600',
color: '#1d2129',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm1 15h-2v-2h2v2zm0-4h-2V5h2v6z" fill="#2c7fff"/>
</svg>
保存更改
</div>
}
visible={showSaveModal}
onCancel={() => {
setShowSaveModal(false);
setPendingNavigation(null);
// 只关闭弹窗保留hasChanges状态下次点击返回还会弹出
}}
footer={[
<button
key="cancel"
className="arco-btn arco-btn-secondary"
onClick={() => {
setShowSaveModal(false);
setHasChanges(false);
setPendingNavigation(null);
navigate('/job-strategy'); // 返回定制求职策略页面
}}
style={{
padding: '8px 20px',
fontSize: '14px',
fontWeight: '500',
borderRadius: '6px',
border: '1px solid #e5e6eb',
backgroundColor: '#ffffff',
color: '#4e5969',
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f7f8fa';
e.currentTarget.style.borderColor = '#c9cdd4';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#ffffff';
e.currentTarget.style.borderColor = '#e5e6eb';
}}
>
放弃更改
</button>,
<div
key="save-wrapper"
style={{
position: 'relative',
display: 'inline-block'
}}
onMouseEnter={(e) => {
const tooltip = document.createElement('div');
tooltip.className = 'save-tooltip';
tooltip.textContent = '非导师和学生本人无修改权限';
tooltip.style.cssText = `
position: absolute;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #1d2129 0%, #2e3440 100%);
color: #ffffff;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.3s ease;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(5px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
`;
document.head.appendChild(style);
// 添加小箭头
const arrow = document.createElement('div');
arrow.style.cssText = `
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 6px 6px 0 6px;
border-color: #2e3440 transparent transparent transparent;
`;
tooltip.appendChild(arrow);
e.currentTarget.appendChild(tooltip);
}}
onMouseLeave={(e) => {
const tooltip = e.currentTarget.querySelector('.save-tooltip');
if (tooltip) {
tooltip.remove();
}
const style = document.querySelector('style');
if (style && style.textContent.includes('fadeIn')) {
style.remove();
}
}}
>
<button
className="arco-btn arco-btn-primary"
disabled
style={{
padding: '8px 24px',
fontSize: '14px',
fontWeight: '500',
borderRadius: '6px',
backgroundColor: '#e5e6eb',
color: '#86909c',
cursor: 'not-allowed',
border: 'none',
opacity: '0.6',
transition: 'all 0.3s ease'
}}
>
保存更改
</button>
</div>
]}
style={{
borderRadius: '12px'
}}
>
<div style={{
display: 'flex',
alignItems: 'flex-start',
gap: '12px'
}}>
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ marginTop: '2px', flexShrink: 0 }}>
<path d="M9 13h2v2H9v-2zm0-8h2v6H9V5zm.99-5C4.47 0 0 4.48 0 10s4.47 10 9.99 10C15.52 20 20 15.52 20 10S15.52 0 9.99 0zM10 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="#ff7d00"/>
</svg>
<div>
<p style={{ margin: 0, fontWeight: '500', color: '#1d2129', marginBottom: '8px' }}>
您对岗位顺序进行了修改
</p>
<p style={{ margin: 0, fontSize: '13px', color: '#86909c' }}>
离开此页面前是否要保存您的更改未保存的更改将会丢失
</p>
</div>
</div>
</Modal>
</div>
);
};
};

View File

@@ -3,6 +3,31 @@
height: 100%;
box-sizing: border-box;
padding: 20px;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
/* 禁止所有图片的拖拽和选中 */
img {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
pointer-events: none;
}
/* 禁止所有文本选中 */
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* 返回按钮样式 */
.back-button-wrapper {
@@ -21,6 +46,7 @@
border: 1px solid #e5e6eb;
border-radius: 6px;
cursor: pointer;
pointer-events: auto;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;

View File

@@ -16,7 +16,15 @@ const JobStrategyDetailPage = () => {
};
return (
<div className="job-strategy-detail-page">
<div
className="job-strategy-detail-page"
style={{
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none'
}}
>
<div className="job-strategy-detail-wrapper">
{/* 返回按钮 */}
<div className="back-button-wrapper">

View File

@@ -21,7 +21,7 @@ export const staticPageData = {
{
id: "homestay_2",
title: "民宿客房管家",
level: "中级",
level: "技术骨干岗",
department: "民宿经营",
type: "全职",
experience: "1-3年",
@@ -65,7 +65,7 @@ export const staticPageData = {
{
id: "hotel_1",
title: "酒店餐饮主管",
level: "中级",
level: "技术骨干岗",
department: "酒店经营",
type: "全职",
experience: "3-5年",
@@ -151,7 +151,7 @@ export const staticPageData = {
},
{
position: "民宿客房管家",
level: "中级",
level: "技术骨干岗",
studentInfo: {
educational_experience: ["酒店管理专业 本科", "XX大学 2019-2023"],
project_experience: [
@@ -216,7 +216,7 @@ export const staticPageData = {
"酒店经营": [
{
position: "酒店餐饮主管",
level: "中级",
level: "技术骨干岗",
studentInfo: {
educational_experience: ["酒店管理专业 本科", "XX大学 2018-2022"],
project_experience: [