接口调整

This commit is contained in:
2025-08-25 23:50:22 +08:00
parent 11b529c502
commit ba4098e1d8
9 changed files with 911 additions and 117 deletions

View File

@@ -15,17 +15,19 @@ const CompanyJobsPage = () => {
const [isExpand, setIsExpand] = useState(false); // 是否展开
const [jobs, setJobs] = useState([]);
const [jobsListPage, setJobsListPage] = useState(1);
const [joblistHasMore, setJobsListHasMore] = useState(true);
const [jobsListHasMore, setJobsListHasMore] = useState(true);
const [interviews, setInterviews] = useState([]);
const [interviewsPage, setInterviewsPage] = useState(1);
const [interviewsHasMore, setInterviewsHasMore] = useState(true);
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
// 初始化页面数据 - 使用聚合接口
useEffect(() => {
const fetchInitialData = async () => {
try {
setLoading(true);
const res = await getCompanyJobsPageData({
studentId: studentInfo?.id,
});
@@ -35,7 +37,7 @@ const CompanyJobsPage = () => {
if (res.data?.jobs) {
const mappedJobs = mapJobList(res.data.jobs.list || []);
setJobs(mappedJobs);
setJoblistHasMore(res.data.jobs.hasMore);
setJobsListHasMore(res.data.jobs.hasMore);
if (mappedJobs.length > 0) {
setJobsListPage(2); // 下次从第2页开始
}
@@ -57,6 +59,12 @@ const CompanyJobsPage = () => {
console.error('Failed to fetch initial page data:', error);
// 如果聚合接口失败,回退到原来的方式
setInitialDataLoaded(true);
// 显示错误信息给用户
if (toast && toast.error) {
toast.error('加载数据失败,请刷新重试');
}
} finally {
setLoading(false);
}
};
@@ -80,8 +88,12 @@ const CompanyJobsPage = () => {
if (res.success) {
const mappedInterviews = mapInterviewList(res.data || []);
setInterviews((prevList) => {
const newList = [...prevList, ...mappedInterviews];
if (res.total === newList?.length) {
// 去重处理:过滤掉已存在的数据
const existingIds = new Set(prevList.map(interview => interview.id));
const newInterviews = mappedInterviews.filter(interview => !existingIds.has(interview.id));
const newList = [...prevList, ...newInterviews];
if (res.total <= newList?.length) {
setInterviewsHasMore(false);
} else {
setInterviewsPage((prevPage) => prevPage + 1);
@@ -104,6 +116,11 @@ const CompanyJobsPage = () => {
return;
}
// 防止重复请求
if (jobsListPage === 1 && jobs.length === 0) {
return; // 初始数据应该通过聚合接口加载
}
try {
const res = await getJobsList({
page: jobsListPage,
@@ -114,8 +131,12 @@ const CompanyJobsPage = () => {
if (res?.success) {
const mappedJobs = mapJobList(res.data);
setJobs((prevList) => {
const newList = [...prevList, ...mappedJobs];
if (res.total === newList?.length) {
// 去重处理:过滤掉已存在的数据
const existingIds = new Set(prevList.map(job => job.id));
const newJobs = mappedJobs.filter(job => !existingIds.has(job.id));
const newList = [...prevList, ...newJobs];
if (res.total <= newList?.length) {
setJobsListHasMore(false);
} else {
setJobsListPage((prevPage) => prevPage + 1);
@@ -135,6 +156,19 @@ const CompanyJobsPage = () => {
navigate("/company-jobs-list");
};
if (loading && jobs.length === 0 && interviews.length === 0) {
return (
<div className="company-jobs-page-wrapper" style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px'
}}>
<p>正在加载数据...</p>
</div>
);
}
return (
<div className="company-jobs-page-wrapper">
<div className="company-jobs-page">
@@ -142,7 +176,7 @@ const CompanyJobsPage = () => {
<p className="company-jobs-page-title">企业内推岗位库</p>
<InfiniteScroll
loadMore={fetchJobsList}
hasMore={joblistHasMore}
hasMore={jobsListHasMore}
className="company-jobs-page-left-list-wrapper"
>
<JobList data={jobs} backgroundColor="#F7F8FA" />

View File

@@ -62,7 +62,7 @@ const EchartsProgress = ({
},
data: [
{
value: 75,
value: percent,
detail: {
valueAnimation: true,
offsetCenter: ["0%", "0%"],

View File

@@ -7,98 +7,31 @@ export default ({ visible, onClose, data }) => {
onClose();
};
if (!data) return null;
const { question } = data;
return (
<Modal visible={visible} onClose={handleCloseModal}>
<div className="interview-questions-modal">
<i className="close-icon" onClick={handleCloseModal} />
<p className="interview-questions-modal-title">PLC岗位群面试题</p>
<p className="interview-questions-modal-title">{data.name}面试题</p>
<p className="interview-questions-modal-subtitle">
在1V1定制求职策略阶段企业HR会为您讲解面试题
</p>
<ul className="interview-questions-modal-list">
<li className="interview-questions-modal-item">
<p className="interview-questions-modal-question">
<span>问题1:</span>xxxxxxxxxxxxxxxxxx
<span>问题:</span>{question.question}
</p>
<p className="interview-questions-modal-answer">
<span>解答</span>
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
</p>
</li>
<li className="interview-questions-modal-item">
<p className="interview-questions-modal-question">
<span>问题1:</span>xxxxxxxxxxxxxxxxxx
</p>
<p className="interview-questions-modal-answer">
<span>解答</span>
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
</p>
</li>
<li className="interview-questions-modal-item">
<p className="interview-questions-modal-question">
<span>问题1:</span>xxxxxxxxxxxxxxxxxx
</p>
<p className="interview-questions-modal-answer">
<span>解答</span>
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
</p>
</li>
<li className="interview-questions-modal-item">
<p className="interview-questions-modal-question">
<span>问题1:</span>xxxxxxxxxxxxxxxxxx
</p>
<p className="interview-questions-modal-answer">
<span>解答</span>
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
</p>
</li>
<li className="interview-questions-modal-item">
<p className="interview-questions-modal-question">
<span>问题1:</span>xxxxxxxxxxxxxxxxxx
</p>
<p className="interview-questions-modal-answer">
<span>解答</span>
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
负责室内平面设计项目的创意和实施2.
与团队合作确保设计质量和项目进度 3.
持续关注设计趋势提升设计技能 1. 具备出色的沟通能力和团队协作精神
2. 能够高效完成任务对工作细节有高度关注 3.
具有不断学习和适应新挑战的能力 加分项有以下行业经验建筑设计
这是一个{question.difficulty.toLowerCase()}难度的{question.category}相关问题
针对这类问题建议从以下几个方面来回答
1. 理解问题的核心概念和背景
2. 结合实际项目经验进行说明
3. 展示对相关技术的深入理解
4. 提及可能的优化或改进方案
</p>
</li>
</ul>

View File

@@ -0,0 +1,421 @@
.resume-modal {
width: 900px;
max-width: 95vw;
max-height: 90vh;
overflow: hidden;
background: #fff;
border-radius: 12px;
display: flex;
flex-direction: column;
position: relative;
}
.resume-modal .close-icon {
position: absolute;
top: 20px;
right: 20px;
width: 24px;
height: 24px;
cursor: pointer;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3C/svg%3E") no-repeat center;
background-size: contain;
transition: opacity 0.3s;
z-index: 10;
}
.resume-modal .close-icon:hover {
opacity: 0.7;
}
.resume-header {
padding: 30px;
border-bottom: 1px solid #e5e5e5;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.resume-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.resume-subtitle {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
.tabs-container {
border-bottom: 1px solid #e5e5e5;
background: #f8f9fa;
}
.tabs {
display: flex;
padding: 0 30px;
}
.tab {
padding: 16px 24px;
cursor: pointer;
border-bottom: 3px solid transparent;
font-weight: 500;
color: #666;
transition: all 0.3s;
position: relative;
}
.tab:hover {
color: #1890ff;
background: rgba(24, 144, 255, 0.05);
}
.tab.active {
color: #1890ff;
border-bottom-color: #1890ff;
background: #fff;
}
.tab-content {
flex: 1;
overflow-y: auto;
padding: 30px;
}
.tab-header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e5e5;
}
.tab-header h3 {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.tab-header p {
color: #666;
font-size: 14px;
margin: 0;
}
.header-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.edit-toggle {
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.edit-toggle:hover {
background: #40a9ff;
}
.edit-toggle.editing {
background: #52c41a;
}
.resume-content {
max-height: 500px;
overflow-y: auto;
}
.personal-info {
margin-bottom: 25px;
}
.personal-info h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 12px;
position: relative;
}
.personal-info h3::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: #1890ff;
border-radius: 2px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.info-item {
display: flex;
font-size: 14px;
}
.info-item label {
font-weight: 500;
color: #333;
width: 60px;
flex-shrink: 0;
}
.info-item span {
color: #666;
}
.section {
margin-bottom: 25px;
}
.section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 12px;
position: relative;
}
.section h3::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: #1890ff;
border-radius: 2px;
}
.section p {
font-size: 14px;
line-height: 1.6;
color: #666;
margin-bottom: 10px;
}
.education-item,
.experience-item,
.project-item {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.education-header,
.experience-header,
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.education-header strong,
.experience-header strong,
.project-header strong {
font-weight: 600;
color: #333;
}
.date,
.role {
font-size: 12px;
color: #999;
background: #fff;
padding: 2px 8px;
border-radius: 4px;
}
.education-details {
font-size: 14px;
color: #666;
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.skill-tag {
display: inline-block;
padding: 4px 12px;
background: #e6f7ff;
color: #1890ff;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
}
.project-link {
display: inline-block;
margin-top: 8px;
padding: 4px 12px;
background: #1890ff;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 12px;
transition: all 0.3s;
}
.project-link:hover {
background: #40a9ff;
}
.awards-list {
margin: 0;
padding-left: 20px;
}
.awards-list li {
font-size: 14px;
line-height: 1.6;
color: #666;
margin-bottom: 5px;
}
.no-content {
text-align: center;
padding: 40px;
color: #999;
font-size: 16px;
}
.no-personal-resume {
text-align: center;
padding: 60px 40px;
color: #666;
}
.no-personal-resume p {
font-size: 16px;
margin-bottom: 20px;
}
.create-btn {
padding: 12px 24px;
background: #1890ff;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.create-btn:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
.resume-edit {
display: flex;
flex-direction: column;
height: 500px;
}
.edit-notice {
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 6px;
padding: 12px;
margin-bottom: 15px;
}
.edit-notice p {
color: #d48806;
margin: 0;
font-size: 14px;
}
.content-editor {
flex: 1;
width: 100%;
padding: 15px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
resize: none;
background: #fafafa;
}
.content-editor:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.edit-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
justify-content: flex-end;
}
.save-btn,
.cancel-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.save-btn {
background: #52c41a;
color: white;
}
.save-btn:hover {
background: #73d13d;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.cancel-btn:hover {
background: #e6e6e6;
}
.development-notice {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
.notice-content {
text-align: center;
color: #86909c;
}
.notice-content h4 {
font-size: 18px;
margin: 0;
}

View File

@@ -0,0 +1,259 @@
import { useState, useEffect } from "react";
import Modal from "@/components/Modal";
import "./index.css";
const ResumeModal = ({ visible, onClose, data }) => {
const [activeTab, setActiveTab] = useState("original");
const [editMode, setEditMode] = useState(false);
const [editContent, setEditContent] = useState(null);
useEffect(() => {
if (data && data.selectedTemplate) {
setEditContent(data.selectedTemplate.content);
}
}, [data]);
const handleCloseModal = () => {
setActiveTab("original");
setEditMode(false);
setEditContent(null);
onClose();
};
if (!data) return null;
const { selectedTemplate, studentResume } = data;
const handleSaveResume = async () => {
// TODO: 调用API保存简历
console.log("保存简历:", editContent);
setEditMode(false);
// 这里应该调用后端API保存简历
};
const handleEditToggle = () => {
if (editMode) {
// 退出编辑模式,询问是否保存
if (JSON.stringify(editContent) !== JSON.stringify(selectedTemplate.content)) {
if (confirm("是否保存修改?")) {
handleSaveResume();
} else {
setEditContent(selectedTemplate.content);
}
}
}
setEditMode(!editMode);
};
const renderResumeContent = (content) => {
if (!content) return <div className="no-content">暂无简历内容</div>;
return (
<div className="resume-content">
<div className="personal-info">
<h3>个人信息</h3>
<div className="info-grid">
<div className="info-item">
<label>姓名</label>
<span>{content.personalInfo?.name || '未填写'}</span>
</div>
<div className="info-item">
<label>电话</label>
<span>{content.personalInfo?.phone || '未填写'}</span>
</div>
<div className="info-item">
<label>邮箱</label>
<span>{content.personalInfo?.email || '未填写'}</span>
</div>
<div className="info-item">
<label>学校</label>
<span>{content.personalInfo?.school || '未填写'}</span>
</div>
</div>
</div>
<div className="section">
<h3>求职意向</h3>
<p>{content.objective || '未填写'}</p>
</div>
<div className="section">
<h3>教育背景</h3>
{content.education && content.education.length > 0 ? (
content.education.map((edu, index) => (
<div key={index} className="education-item">
<div className="education-header">
<strong>{edu.school}</strong>
<span className="date">{edu.startDate} - {edu.endDate}</span>
</div>
<div className="education-details">
<span>{edu.major} | {edu.degree}</span>
{edu.description && <p>{edu.description}</p>}
</div>
</div>
))
) : (
<p>暂无教育背景</p>
)}
</div>
<div className="section">
<h3>工作经历</h3>
{content.experience && content.experience.length > 0 ? (
content.experience.map((exp, index) => (
<div key={index} className="experience-item">
<div className="experience-header">
<strong>{exp.company} - {exp.position}</strong>
<span className="date">{exp.startDate} - {exp.endDate}</span>
</div>
<p>{exp.description}</p>
</div>
))
) : (
<p>暂无工作经历</p>
)}
</div>
<div className="section">
<h3>专业技能</h3>
{content.skills && content.skills.length > 0 ? (
<div className="skills-list">
{content.skills.map((skill, index) => (
<span key={index} className="skill-tag">{skill}</span>
))}
</div>
) : (
<p>暂无专业技能</p>
)}
</div>
<div className="section">
<h3>项目经历</h3>
{content.projects && content.projects.length > 0 ? (
content.projects.map((project, index) => (
<div key={index} className="project-item">
<div className="project-header">
<strong>{project.name}</strong>
<span className="role">担任{project.role}</span>
</div>
<p>{project.description}</p>
{project.link && (
<a href={project.link} target="_blank" rel="noopener noreferrer" className="project-link">
项目链接
</a>
)}
</div>
))
) : (
<p>暂无项目经历</p>
)}
</div>
{content.awards && content.awards.length > 0 && (
<div className="section">
<h3>获奖情况</h3>
<ul className="awards-list">
{content.awards.map((award, index) => (
<li key={index}>{award}</li>
))}
</ul>
</div>
)}
</div>
);
};
const renderEditableContent = (content) => {
// 简化的编辑界面,实际项目中可能需要更复杂的表单
return (
<div className="resume-edit">
<div className="edit-notice">
<p>📝 编辑模式您可以基于系统模板修改个人信息</p>
</div>
<textarea
className="content-editor"
value={JSON.stringify(editContent, null, 2)}
onChange={(e) => {
try {
const newContent = JSON.parse(e.target.value);
setEditContent(newContent);
} catch (error) {
// 忽略JSON解析错误保持当前内容
}
}}
placeholder="请输入JSON格式的简历内容"
/>
<div className="edit-buttons">
<button className="save-btn" onClick={handleSaveResume}>
保存修改
</button>
<button className="cancel-btn" onClick={() => setEditMode(false)}>
取消编辑
</button>
</div>
</div>
);
};
return (
<Modal visible={visible} onClose={handleCloseModal}>
<div className="resume-modal">
<i className="close-icon" onClick={handleCloseModal} />
<div className="resume-header">
<h2 className="resume-title">
{selectedTemplate?.name || '简历模板'}
</h2>
<p className="resume-subtitle">
{selectedTemplate?.description || '基于系统模板的简历展示'}
</p>
</div>
<div className="tabs-container">
<div className="tabs">
<div
className={`tab ${activeTab === 'original' ? 'active' : ''}`}
onClick={() => setActiveTab('original')}
>
原始版
</div>
<div
className={`tab ${activeTab === 'personal' ? 'active' : ''}`}
onClick={() => {
alert('个人修改版功能开发中,敬请期待!');
}}
>
个人修改版
</div>
</div>
</div>
<div className="tab-content">
{activeTab === 'original' && (
<div className="original-tab">
<div className="tab-header">
<h3>原始版</h3>
</div>
{renderResumeContent(selectedTemplate?.content)}
</div>
)}
{activeTab === 'personal' && (
<div className="personal-tab">
<div className="tab-header">
<h3>个人修改版</h3>
</div>
<div className="development-notice">
<div className="notice-content">
<h4>开发中</h4>
</div>
</div>
</div>
)}
</div>
</div>
</Modal>
);
};
export default ResumeModal;

View File

@@ -11,7 +11,7 @@
height: 56px;
background-color: #ffffff;
display: flex;
justify-content: flex-start;
justify-content: space-between;
align-items: center;
overflow-x: auto;
box-sizing: border-box;
@@ -23,6 +23,11 @@
background-color: #f2f3f5;
}
.navigation-tabs {
display: flex;
align-items: center;
}
.resume-interview-navigation-item {
margin-right: 20px;
width: 101px;
@@ -36,6 +41,63 @@
font-weight: 600;
text-align: center;
}
.search-container {
position: relative;
display: flex;
align-items: center;
}
.search-input {
width: 200px;
height: 32px;
padding: 0 35px 0 12px;
border: 1px solid #e5e8ed;
border-radius: 16px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
&::placeholder {
color: #86909c;
}
&:focus {
border-color: #2c7aff;
box-shadow: 0 0 0 2px rgba(44, 122, 255, 0.1);
}
}
.search-icon {
position: absolute;
right: 10px;
width: 16px;
height: 16px;
background-color: #86909c;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") center/contain no-repeat;
pointer-events: none;
}
.clear-button {
position: absolute;
right: 8px;
width: 20px;
height: 20px;
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #86909c;
font-size: 14px;
opacity: 0.7;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
}
.resume-interview-content-wrapper {
@@ -201,6 +263,16 @@
}
}
}
.no-results {
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
color: #86909c;
font-size: 14px;
}
}
.resumes-list {

View File

@@ -1,15 +1,19 @@
import { useRef, useState, useEffect } from "react";
import InterviewQuestionsModal from "./components/InterviewQuestionsModal";
import ResumeModal from "./components/ResumeModal";
import { getPageData } from "@/services/resumeInterview";
import "./index.css";
const ResumeInterviewPage = () => {
const [activeIndustry, setActiveIndustry] = useState("frontend");
const [interviewModalVisible, setInterviewModalVisible] = useState(false);
const [resumeModalVisible, setResumeModalVisible] = useState(false);
const [modalData, setModalData] = useState(undefined);
const [interviewModalData, setInterviewModalData] = useState(undefined);
const [resumeModalData, setResumeModalData] = useState(undefined);
const [pageData, setPageData] = useState(null);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const sectionsRef = useRef({});
// 导航到指定行业段落
@@ -21,15 +25,50 @@ const ResumeInterviewPage = () => {
});
};
// 钩子题目点击处理
const handleHookQuestionClick = (item) => {
setModalData(item);
// 面试题点击处理
const handleQuestionClick = (item) => {
setInterviewModalData(item);
setInterviewModalVisible(true);
};
// 职位点击处理
const handlePositionClick = (position, industry) => {
// Find resume templates for this industry
const templates = pageData.resumeTemplates[industry.name] || [];
const selectedTemplate = templates.find(t => t.level === position.level) || templates[0];
setResumeModalData({
selectedTemplate,
studentResume: pageData.myResume
});
setResumeModalVisible(true);
};
const handleCloseModal = () => {
const handleCloseInterviewModal = () => {
setInterviewModalVisible(false);
setInterviewModalData(undefined);
};
const handleCloseResumeModal = () => {
setResumeModalVisible(false);
setModalData(undefined);
setResumeModalData(undefined);
};
// Search functionality
const handleSearchChange = (e) => {
setSearchTerm(e.target.value);
};
const handleClearSearch = () => {
setSearchTerm("");
};
// Filter positions based on search term
const filterPositions = (positions) => {
if (!searchTerm.trim()) return positions;
return positions.filter(position =>
position.name.toLowerCase().includes(searchTerm.toLowerCase())
);
};
// 获取页面数据
@@ -98,6 +137,7 @@ const ResumeInterviewPage = () => {
return (
<div className="resume-interview-page">
<ul className="resume-interview-navigation">
<div className="navigation-tabs">
{pageData.industries.map((industry) => (
<li
key={industry.id}
@@ -109,6 +149,22 @@ const ResumeInterviewPage = () => {
{industry.name}
</li>
))}
</div>
<div className="search-container">
<input
type="text"
className="search-input"
placeholder="搜索职位名称..."
value={searchTerm}
onChange={handleSearchChange}
/>
{searchTerm && (
<button className="clear-button" onClick={handleClearSearch}>
×
</button>
)}
{!searchTerm && <div className="search-icon" />}
</div>
</ul>
<ul className="resume-interview-content-wrapper">
{pageData.industries.map((item) => (
@@ -121,8 +177,12 @@ const ResumeInterviewPage = () => {
<p className="item-subtitle">简历与面试题</p>
<div className="item-content-wrapper">
<ul className="jobs-list">
{item.positions.map((position) => (
<li className="job-item job-item-change" key={position.id}>
{filterPositions(item.positions).map((position) => (
<li
className="job-item job-item-change"
key={position.id}
onClick={() => handlePositionClick(position, item)}
>
<span>{position.level}</span>
<div className="job-name">
<p>{position.name}</p>
@@ -130,13 +190,18 @@ const ResumeInterviewPage = () => {
</div>
</li>
))}
{searchTerm && filterPositions(item.positions).length === 0 && (
<li className="no-results">
<p>未找到匹配的职位</p>
</li>
)}
</ul>
<ul className="resumes-list">
{item.questions.map((question) => (
<li
key={question.id}
className="resume-item"
onClick={() => handleHookQuestionClick({ ...item, question })}
onClick={() => handleQuestionClick({ ...item, question })}
>
<p>{question.question}</p>
</li>
@@ -147,9 +212,14 @@ const ResumeInterviewPage = () => {
))}
</ul>
<InterviewQuestionsModal
visible={interviewModalVisible}
onClose={handleCloseInterviewModal}
data={interviewModalData}
/>
<ResumeModal
visible={resumeModalVisible}
onClose={handleCloseModal}
data={modalData}
onClose={handleCloseResumeModal}
data={resumeModalData}
/>
</div>
);

View File

@@ -199,9 +199,14 @@ export const mapInterview = (backendData) => {
feedback: backendData.feedback,
result: backendData.result,
student: backendData.student ? mapStudent(backendData.student) : null,
job: backendData.job ? mapJob(backendData.job) : null,
company: backendData.job?.company?.name || "",
position: backendData.job?.title || "",
job: backendData.job ? mapJob(backendData.job) : {
// Provide default job object when no job relation exists
salary: "面议",
tags: [],
company: { name: backendData.company || "" }
},
company: backendData.company || backendData.job?.company?.name || "",
position: backendData.position || backendData.job?.title || "",
// Map status for frontend
statusText: mapInterviewStatus(backendData.status, backendData.result),
};