Files
jiaowu-test/src/pages/ResumeInterviewPage/index.jsx
KQL 4e0e96e6b8 UI优化更新:面试模拟、简历面试、项目库、求职策略等多个页面改进
主要更新:
- 面试模拟页:移除上滑查看评价,添加渐进式评分(72→81→89)
- 简历面试页:添加岗位头像、标签背景、面试题加粗等视觉优化
- 项目库页:添加"我完成的项目库"板块,增加hover效果
- 求职策略详情页:优化圆柱体和矩形对齐,添加CSV岗位数据,调整批次文字位置
- 企业岗位列表页:添加返回按钮功能
- 全局:统一岗位级别术语(普通岗/技术骨干岗/储备干部岗)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 20:46:03 +08:00

362 lines
12 KiB
JavaScript
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 { 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";
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";
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);
const [showLeftBtn, setShowLeftBtn] = useState(false);
const [showRightBtn, setShowRightBtn] = useState(true);
const sectionsRef = useRef({});
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
};
};
// 导航到指定行业段落
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]);
// 监听数据变化,更新滚动按钮状态
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'
});
};
// 添加鼠标滚轮横向滚动功能
useEffect(() => {
// 延迟执行以确保DOM已经渲染
const timer = setTimeout(() => {
const navigation = navRef.current;
if (!navigation) return;
const handleWheel = (e) => {
// 检查是否鼠标在导航栏上
if (navigation.contains(e.target)) {
// 阻止默认垂直滚动
e.preventDefault();
e.stopPropagation();
// 转换为横向滚动
const delta = e.deltaY || e.deltaX;
navigation.scrollLeft += delta;
// 更新按钮状态
checkScrollButtons();
}
};
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]);
return (
<div className="resume-interview-page">
{loading ? (
<Spin size={80} className="resume-interview-spin" />
) : pageData ? (
<>
<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>
<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">
{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>
);
})}
</ul>
<ul className="resumes-list">
{item.questions.map((question) => (
<li
key={question.id}
className="resume-item"
onClick={() =>
handleQuestionClick({ ...item, questions: question.subQuestions || [question] })
}
>
<img
src={QuestionIcon}
alt="question"
className="question-icon"
/>
<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;