2025-09-03 13:26:13 +08:00
|
|
|
|
import { useRef, useState, useEffect } from "react";
|
|
|
|
|
|
import { Spin, Empty } from "@arco-design/web-react";
|
|
|
|
|
|
import toast from "@/components/Toast";
|
|
|
|
|
|
import InterviewQuestionsModal from "./components/InterviewQuestionsModal";
|
|
|
|
|
|
import ResumeInfoModal from "@/pages/CompanyJobsPage/components/ResumeInfoModal";
|
|
|
|
|
|
import { getPageData } from "@/services/resumeInterview";
|
2025-09-05 20:46:03 +08:00
|
|
|
|
import jobLevelData from "@/data/joblevel.json";
|
|
|
|
|
|
import TagHigh from "@/assets/images/ResumeInterviewPage/Tag.png";
|
|
|
|
|
|
import TagMiddle from "@/assets/images/ResumeInterviewPage/Tag2.png";
|
|
|
|
|
|
import TagOrdinary from "@/assets/images/ResumeInterviewPage/Tag3.png";
|
|
|
|
|
|
import QuestionIcon from "@/assets/images/ResumeInterviewPage/question_icon2.png";
|
2025-09-03 13:26:13 +08:00
|
|
|
|
|
|
|
|
|
|
import "./index.css";
|
|
|
|
|
|
|
|
|
|
|
|
const ResumeInterviewPage = () => {
|
|
|
|
|
|
const [activeIndustry, setActiveIndustry] = useState("frontend");
|
|
|
|
|
|
const [interviewModalVisible, setInterviewModalVisible] = useState(false);
|
|
|
|
|
|
const [resumeModalVisible, setResumeModalVisible] = useState(false);
|
|
|
|
|
|
const [interviewModalData, setInterviewModalData] = useState(undefined);
|
|
|
|
|
|
const [resumeModalData, setResumeModalData] = useState(undefined);
|
|
|
|
|
|
const [pageData, setPageData] = useState(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2025-09-05 20:46:03 +08:00
|
|
|
|
const [showLeftBtn, setShowLeftBtn] = useState(false);
|
|
|
|
|
|
const [showRightBtn, setShowRightBtn] = useState(true);
|
2025-09-03 13:26:13 +08:00
|
|
|
|
const sectionsRef = useRef({});
|
2025-09-05 20:46:03 +08:00
|
|
|
|
const navRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取岗位头像和级别信息
|
|
|
|
|
|
const getPositionInfo = (positionTitle) => {
|
|
|
|
|
|
const jobData = jobLevelData.data;
|
|
|
|
|
|
let positionInfo = null;
|
|
|
|
|
|
let levelName = "";
|
|
|
|
|
|
let levelKey = "";
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有级别查找匹配的岗位
|
|
|
|
|
|
for (const [key, levelData] of Object.entries(jobData)) {
|
|
|
|
|
|
const found = levelData.list.find(item => item.position_name === positionTitle);
|
|
|
|
|
|
if (found) {
|
|
|
|
|
|
positionInfo = found;
|
|
|
|
|
|
levelName = levelData.name;
|
|
|
|
|
|
levelKey = key;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据级别返回对应的标签图片
|
|
|
|
|
|
let tagImage = "";
|
|
|
|
|
|
switch(levelKey) {
|
|
|
|
|
|
case "high":
|
|
|
|
|
|
tagImage = TagHigh;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "middle":
|
|
|
|
|
|
tagImage = TagMiddle;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "ordinary":
|
|
|
|
|
|
default:
|
|
|
|
|
|
tagImage = TagOrdinary;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
avatar: positionInfo?.img || null,
|
|
|
|
|
|
levelName: levelName || "普通岗",
|
|
|
|
|
|
tagImage: tagImage
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-09-03 13:26:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 导航到指定行业段落
|
|
|
|
|
|
const handleNavClick = (industryId) => {
|
|
|
|
|
|
setActiveIndustry(industryId);
|
|
|
|
|
|
sectionsRef.current[industryId]?.scrollIntoView({
|
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
|
block: "start",
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 面试题点击处理
|
|
|
|
|
|
const handleQuestionClick = (item) => {
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
setInterviewModalVisible(true);
|
|
|
|
|
|
setInterviewModalData(item);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.error("加载数据失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 职位点击处理
|
|
|
|
|
|
const handlePositionClick = (position, industry) => {
|
|
|
|
|
|
// Find resume templates for this industry
|
|
|
|
|
|
const templates = pageData.resumeTemplates[industry.name] || [];
|
|
|
|
|
|
// 首先根据岗位名称精确匹配
|
|
|
|
|
|
const selectedTemplate =
|
|
|
|
|
|
templates.find((t) => t.position === position.title) ||
|
|
|
|
|
|
templates.find((t) => t.level === position.level) ||
|
|
|
|
|
|
templates[0];
|
|
|
|
|
|
|
|
|
|
|
|
setResumeModalData({
|
|
|
|
|
|
selectedTemplate,
|
|
|
|
|
|
studentResume: pageData.myResume,
|
|
|
|
|
|
});
|
|
|
|
|
|
setResumeModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCloseInterviewModal = () => {
|
|
|
|
|
|
setInterviewModalVisible(false);
|
|
|
|
|
|
setInterviewModalData(undefined);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCloseResumeModal = () => {
|
|
|
|
|
|
setResumeModalVisible(false);
|
|
|
|
|
|
setResumeModalData(undefined);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const filterPositions = (positions) => {
|
|
|
|
|
|
return positions.filter((position) => position.title?.toLowerCase());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取页面数据
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchPageData = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const response = await getPageData();
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
console.log('页面数据加载成功:', response.data);
|
|
|
|
|
|
setPageData(response.data);
|
|
|
|
|
|
// 设置默认选中第一个行业
|
|
|
|
|
|
if (response.data.industries && response.data.industries.length > 0) {
|
|
|
|
|
|
setActiveIndustry(response.data.industries[0].id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Failed to fetch page data:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchPageData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 监听滚动位置更新导航状态
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!pageData?.industries) return;
|
|
|
|
|
|
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
const scrollPosition = window.scrollY + 200;
|
|
|
|
|
|
|
|
|
|
|
|
pageData.industries.forEach((industry) => {
|
|
|
|
|
|
const section = sectionsRef.current[industry.id];
|
|
|
|
|
|
if (section) {
|
|
|
|
|
|
const sectionTop = section.offsetTop;
|
|
|
|
|
|
const sectionBottom = sectionTop + section.offsetHeight;
|
|
|
|
|
|
|
|
|
|
|
|
if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) {
|
|
|
|
|
|
setActiveIndustry(industry.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("scroll", handleScroll);
|
|
|
|
|
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
|
|
|
|
}, [pageData?.industries]);
|
2025-09-05 20:46:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听数据变化,更新滚动按钮状态
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (pageData?.industries && navRef.current) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
checkScrollButtons();
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [pageData?.industries]);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查滚动按钮的显示状态
|
|
|
|
|
|
const checkScrollButtons = () => {
|
|
|
|
|
|
if (!navRef.current) return;
|
|
|
|
|
|
const { scrollLeft, scrollWidth, clientWidth } = navRef.current;
|
|
|
|
|
|
setShowLeftBtn(scrollLeft > 5); // 添加5px的容差
|
|
|
|
|
|
setShowRightBtn(scrollLeft < scrollWidth - clientWidth - 5); // 添加5px的容差,确保能滚动到最后
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 左右滚动函数
|
|
|
|
|
|
const scrollNav = (direction) => {
|
|
|
|
|
|
if (!navRef.current) return;
|
|
|
|
|
|
const scrollAmount = 200; // 每次滚动的距离
|
|
|
|
|
|
navRef.current.scrollTo({
|
|
|
|
|
|
left: navRef.current.scrollLeft + (direction === 'left' ? -scrollAmount : scrollAmount),
|
|
|
|
|
|
behavior: 'smooth'
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-09-03 13:26:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加鼠标滚轮横向滚动功能
|
|
|
|
|
|
useEffect(() => {
|
2025-09-05 20:46:03 +08:00
|
|
|
|
// 延迟执行以确保DOM已经渲染
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
|
const navigation = navRef.current;
|
|
|
|
|
|
if (!navigation) return;
|
2025-09-03 13:26:13 +08:00
|
|
|
|
|
2025-09-05 20:46:03 +08:00
|
|
|
|
const handleWheel = (e) => {
|
|
|
|
|
|
// 检查是否鼠标在导航栏上
|
|
|
|
|
|
if (navigation.contains(e.target)) {
|
|
|
|
|
|
// 阻止默认垂直滚动
|
2025-09-03 13:26:13 +08:00
|
|
|
|
e.preventDefault();
|
2025-09-05 20:46:03 +08:00
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为横向滚动
|
|
|
|
|
|
const delta = e.deltaY || e.deltaX;
|
|
|
|
|
|
navigation.scrollLeft += delta;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新按钮状态
|
|
|
|
|
|
checkScrollButtons();
|
2025-09-03 13:26:13 +08:00
|
|
|
|
}
|
2025-09-05 20:46:03 +08:00
|
|
|
|
};
|
2025-09-03 13:26:13 +08:00
|
|
|
|
|
2025-09-05 20:46:03 +08:00
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
checkScrollButtons();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 使用捕获阶段以确保事件被正确处理
|
|
|
|
|
|
window.addEventListener('wheel', handleWheel, { passive: false, capture: true });
|
|
|
|
|
|
navigation.addEventListener('scroll', handleScroll);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始检查
|
|
|
|
|
|
checkScrollButtons();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener('wheel', handleWheel, { capture: true });
|
|
|
|
|
|
navigation.removeEventListener('scroll', handleScroll);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
|
}, [pageData]);
|
2025-09-03 13:26:13 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="resume-interview-page">
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<Spin size={80} className="resume-interview-spin" />
|
|
|
|
|
|
) : pageData ? (
|
|
|
|
|
|
<>
|
2025-09-05 20:46:03 +08:00
|
|
|
|
<div className="resume-interview-navigation-wrapper">
|
|
|
|
|
|
{showLeftBtn && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="nav-scroll-btn nav-scroll-btn-left"
|
|
|
|
|
|
onClick={() => scrollNav('left')}
|
|
|
|
|
|
>
|
|
|
|
|
|
‹
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<ul className="resume-interview-navigation" ref={navRef}>
|
|
|
|
|
|
<div className="navigation-tabs">
|
|
|
|
|
|
{pageData.industries.map((industry) => (
|
|
|
|
|
|
<li
|
|
|
|
|
|
key={industry.id}
|
|
|
|
|
|
className={`resume-interview-navigation-item ${
|
|
|
|
|
|
activeIndustry === industry.id ? "active" : ""
|
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => handleNavClick(industry.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{industry.name}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
{showRightBtn && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="nav-scroll-btn nav-scroll-btn-right"
|
|
|
|
|
|
onClick={() => scrollNav('right')}
|
|
|
|
|
|
>
|
|
|
|
|
|
›
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-03 13:26:13 +08:00
|
|
|
|
<ul className="resume-interview-content-wrapper">
|
|
|
|
|
|
{pageData.industries.map((item) => (
|
|
|
|
|
|
<li
|
|
|
|
|
|
className="resume-interview-content-item-wrapper"
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
ref={(el) => (sectionsRef.current[item.id] = el)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<p className="item-title">{item.name}</p>
|
|
|
|
|
|
<p className="item-subtitle">简历与面试题</p>
|
|
|
|
|
|
<div className="item-content-wrapper">
|
|
|
|
|
|
<ul className="jobs-list">
|
2025-09-05 20:46:03 +08:00
|
|
|
|
{filterPositions(item.positions).map((position) => {
|
|
|
|
|
|
const positionInfo = getPositionInfo(position.title);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<li
|
|
|
|
|
|
className="job-item job-item-change"
|
|
|
|
|
|
key={position.id}
|
|
|
|
|
|
onClick={() => handlePositionClick(position, item)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="job-avatar-wrapper">
|
|
|
|
|
|
{positionInfo.avatar ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={positionInfo.avatar}
|
|
|
|
|
|
alt={position.title}
|
|
|
|
|
|
className="job-avatar"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="job-avatar-placeholder" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="job-info">
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={positionInfo.tagImage}
|
|
|
|
|
|
alt={positionInfo.levelName}
|
|
|
|
|
|
className="job-level-tag"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="job-name">
|
|
|
|
|
|
<p>岗位名称:{position.title}</p>
|
|
|
|
|
|
<span className="job-arrow">›</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-09-03 13:26:13 +08:00
|
|
|
|
</ul>
|
|
|
|
|
|
<ul className="resumes-list">
|
|
|
|
|
|
{item.questions.map((question) => (
|
|
|
|
|
|
<li
|
|
|
|
|
|
key={question.id}
|
|
|
|
|
|
className="resume-item"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
handleQuestionClick({ ...item, questions: question.subQuestions || [question] })
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2025-09-05 20:46:03 +08:00
|
|
|
|
<img
|
|
|
|
|
|
src={QuestionIcon}
|
|
|
|
|
|
alt="question"
|
|
|
|
|
|
className="question-icon"
|
|
|
|
|
|
/>
|
2025-09-03 13:26:13 +08:00
|
|
|
|
<p>{question.question}</p>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无数据" className="empty-data" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<InterviewQuestionsModal
|
|
|
|
|
|
visible={interviewModalVisible}
|
|
|
|
|
|
onClose={handleCloseInterviewModal}
|
|
|
|
|
|
data={interviewModalData}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ResumeInfoModal
|
|
|
|
|
|
visible={resumeModalVisible}
|
|
|
|
|
|
onClose={handleCloseResumeModal}
|
|
|
|
|
|
data={resumeModalData}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ResumeInterviewPage;
|