Files
jiaowu-test/src/pages/JobStrategyDetailPage/components/TargetPosition/index.jsx.backup_20251018_092718
KQL 63f8cf2e7d chore: 迁移项目到新仓库并整理代码
- 更新多个组件的功能优化
- 整理简历映射数据
- 优化视频播放和面试模拟相关组件
- 更新就业策略和公司职位页面

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 18:42:25 +08:00

654 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};