chore: 迁移项目到新仓库并整理代码

- 更新多个组件的功能优化
- 整理简历映射数据
- 优化视频播放和面试模拟相关组件
- 更新就业策略和公司职位页面

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-10-24 18:42:25 +08:00
parent 1b964b3886
commit 63f8cf2e7d
43 changed files with 3937 additions and 792 deletions

View File

@@ -0,0 +1,654 @@
import { useEffect, useRef, useState } from "react";
import { Modal, Message, Tooltip } 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(() => {
// 添加鼠标滚轮事件监听,实现横向滚动
const handleWheel = (e, ref) => {
if (ref.current && ref.current.contains(e.target)) {
e.preventDefault();
ref.current.scrollLeft += e.deltaY;
}
};
const batch1El = batch1Ref.current;
const batch2El = batch2Ref.current;
const batch3El = batch3Ref.current;
const wheel1Handler = (e) => handleWheel(e, batch1Ref);
const wheel2Handler = (e) => handleWheel(e, batch2Ref);
const wheel3Handler = (e) => handleWheel(e, batch3Ref);
if (batch1El) batch1El.addEventListener('wheel', wheel1Handler, { passive: false });
if (batch2El) batch2El.addEventListener('wheel', wheel2Handler, { passive: false });
if (batch3El) batch3El.addEventListener('wheel', wheel3Handler, { passive: false });
return () => {
if (batch1El) batch1El.removeEventListener('wheel', wheel1Handler);
if (batch2El) batch2El.removeEventListener('wheel', wheel2Handler);
if (batch3El) batch3El.removeEventListener('wheel', wheel3Handler);
};
}, []);
// 根据岗位名称获取头像
const getPositionAvatar = (positionName) => {
const jobData = jobLevelData.data;
for (const [key, levelData] of Object.entries(jobData)) {
const found = levelData.list.find(item => item.position_name === positionName);
if (found) {
return found.img;
}
}
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuUpSO4gUtJz.png"; // 默认头像
};
// 定义三个批次的岗位数据
const initialBatchPositions = {
batch1: [
"二次元周边店店员",
"会展执行助理",
"会展讲解员",
"会展营销",
"商业会展执行专员",
"景区运营专员",
"文旅运营总监助理",
"品牌策划运营专员",
"品牌推广专员",
"ip运营",
"文创产品设计师助理",
"新媒体运营专员",
"网络运营专员",
"社群运营",
"直播助理"
],
batch2: [
"宠物店店长",
"宠物营养师",
"二次元周边选品专员",
"二次元周边店店长",
"会展策划师",
"漫展策划师",
"活动执行",
"活动策划师",
"酒店运营专员",
"餐厅运营经理",
"露营地运营专员",
"旅游规划师",
"文旅项目投资拓展管培生",
"民宿管家",
"民宿客房管家",
"民宿运营专员",
"品牌公关",
"ip运营总监助理",
"品牌公关管培生",
"直播中控",
"SEO专员",
"SEM专员",
"赛事经纪"
],
batch3: [
"酒店餐饮主管",
"客房经理",
"酒店大堂副理",
"旅游计调专员",
"文创产品策划师",
"文创产品设计师",
"赛事礼仪",
"赛事编辑",
"艺人经纪人",
"演出执行经理",
"场馆运营人员"
]
};
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 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 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>
{/* 第一批次 */}
<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 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>
{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>
);
};