- 更新多个组件的功能优化 - 整理简历映射数据 - 优化视频播放和面试模拟相关组件 - 更新就业策略和公司职位页面 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
654 lines
21 KiB
Plaintext
654 lines
21 KiB
Plaintext
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>
|
||
);
|
||
}; |