- 创建InterviewStatusDropdown组件,显示面试状态动画 - 集成Lottie动画播放器,加载对应状态的动画文件 - 点击面试状态按钮后弹出下拉栏,展示动画和状态说明 - 添加11种不同面试状态的动画映射和描述文字 - 下拉栏支持点击遮罩层关闭 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
import { useState, useEffect } from "react";
|
||
import { useSelector } from "react-redux";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { Spin, Empty } from "@arco-design/web-react";
|
||
// import { mapJobList, mapInterviewList } from "@/utils/dataMapper"; // 不再需要映射器,mock数据已经是前端格式
|
||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||
import toast from "@/components/Toast";
|
||
import JobList from "./components/JobList";
|
||
import InterviewStatusDropdown from "./components/InterviewStatusDropdown";
|
||
import {
|
||
getCompanyJobsPageData,
|
||
getJobsList,
|
||
getInterviewsList,
|
||
} from "@/services";
|
||
|
||
import "./index.css";
|
||
const PAGE_SIZE = 10;
|
||
|
||
const CompanyJobsPage = () => {
|
||
const studentInfo = useSelector((state) => state.student.studentInfo);
|
||
const [jobs, setJobs] = useState([]);
|
||
const [jobsListPage, setJobsListPage] = useState(1);
|
||
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 [dropdownOpen, setDropdownOpen] = useState(false);
|
||
const [selectedInterview, setSelectedInterview] = useState(null);
|
||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||
const navigate = useNavigate();
|
||
|
||
// 初始化页面数据 - 使用聚合接口
|
||
useEffect(() => {
|
||
const fetchInitialData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const res = await getCompanyJobsPageData({
|
||
studentId: studentInfo?.id,
|
||
});
|
||
|
||
if (res?.success) {
|
||
// 设置岗位数据
|
||
if (res.data?.jobs) {
|
||
// Mock数据已经是前端格式,直接使用不需要映射
|
||
const jobs = res.data.jobs.list || [];
|
||
setJobs(jobs);
|
||
setJobsListHasMore(res.data.jobs.hasMore);
|
||
if (jobs.length > 0) {
|
||
setJobsListPage(2); // 下次从第2页开始
|
||
}
|
||
}
|
||
|
||
// 设置面试数据
|
||
if (res.data?.interviews) {
|
||
// Mock数据已经是前端格式,直接使用不需要映射
|
||
const interviews = res.data.interviews.list || [];
|
||
setInterviews(interviews);
|
||
setInterviewsHasMore(res.data.interviews.hasMore);
|
||
if (interviews.length > 0) {
|
||
setInterviewsPage(2); // 下次从第2页开始
|
||
}
|
||
}
|
||
|
||
setInitialDataLoaded(true);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch initial page data:", error);
|
||
// 如果聚合接口失败,回退到原来的方式
|
||
setInitialDataLoaded(true);
|
||
// 显示错误信息给用户
|
||
if (toast && toast.error) {
|
||
toast.error("加载数据失败,请刷新重试");
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchInitialData();
|
||
}, [studentInfo?.id]);
|
||
|
||
// 获取面试信息 - 用于分页加载更多
|
||
const fetchInterviewsData = async () => {
|
||
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
|
||
if (!initialDataLoaded || (interviewsPage === 1 && interviews.length > 0)) {
|
||
return;
|
||
}
|
||
|
||
if (studentInfo?.id) {
|
||
const res = await getInterviewsList({
|
||
page: interviewsPage,
|
||
pageSize: PAGE_SIZE,
|
||
studentId: studentInfo?.id,
|
||
status: "SCHEDULED",
|
||
});
|
||
if (res.success) {
|
||
// Mock数据已经是前端格式,直接使用
|
||
const interviews = res.data || [];
|
||
setInterviews((prevList) => {
|
||
// 去重处理:过滤掉已存在的数据
|
||
const existingIds = new Set(
|
||
prevList.map((interview) => interview.id)
|
||
);
|
||
const newInterviews = interviews.filter(
|
||
(interview) => !existingIds.has(interview.id)
|
||
);
|
||
|
||
const newList = [...prevList, ...newInterviews];
|
||
if (res.total <= newList?.length) {
|
||
setInterviewsHasMore(false);
|
||
} else {
|
||
setInterviewsPage((prevPage) => prevPage + 1);
|
||
}
|
||
return newList;
|
||
});
|
||
} else {
|
||
if (interviewsPage === 1) {
|
||
setInterviews([]);
|
||
}
|
||
toast.error(res.message);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理面试状态点击
|
||
const handleStatusClick = (e, item) => {
|
||
e.stopPropagation();
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
setDropdownPosition({
|
||
top: rect.bottom + 5,
|
||
left: rect.left
|
||
});
|
||
setSelectedInterview(item);
|
||
setDropdownOpen(true);
|
||
};
|
||
|
||
// 关闭下拉栏
|
||
const handleCloseDropdown = () => {
|
||
setDropdownOpen(false);
|
||
setSelectedInterview(null);
|
||
};
|
||
|
||
// 获取企业内推岗位 - 用于分页加载更多
|
||
const fetchJobsList = async () => {
|
||
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
|
||
if (!initialDataLoaded || (jobsListPage === 1 && jobs.length > 0)) {
|
||
return;
|
||
}
|
||
|
||
// 防止重复请求
|
||
if (jobsListPage === 1 && jobs.length === 0) {
|
||
return; // 初始数据应该通过聚合接口加载
|
||
}
|
||
|
||
try {
|
||
const res = await getJobsList({
|
||
page: jobsListPage,
|
||
pageSize: PAGE_SIZE,
|
||
isActive: true,
|
||
});
|
||
|
||
if (res?.success) {
|
||
// Mock数据已经是前端格式,直接使用
|
||
const jobs = res.data;
|
||
setJobs((prevList) => {
|
||
// 去重处理:过滤掉已存在的数据
|
||
const existingIds = new Set(prevList.map((job) => job.id));
|
||
const newJobs = jobs.filter((job) => !existingIds.has(job.id));
|
||
|
||
const newList = [...prevList, ...newJobs];
|
||
if (res.total <= newList?.length) {
|
||
setJobsListHasMore(false);
|
||
} else {
|
||
setJobsListPage((prevPage) => prevPage + 1);
|
||
}
|
||
return newList;
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch data:", error);
|
||
if (jobsListPage === 1) {
|
||
setJobs([]);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleJobWrapperClick = () => {
|
||
navigate("/company-jobs-list");
|
||
};
|
||
|
||
return (
|
||
<div className="company-jobs-page-wrapper">
|
||
<div className="company-jobs-page">
|
||
{loading ? (
|
||
<Spin size={80} className="company-jobs-page-spin" />
|
||
) : (
|
||
<>
|
||
<div
|
||
className="company-jobs-page-left"
|
||
>
|
||
<div className="company-jobs-page-header">
|
||
<p className="company-jobs-page-title">
|
||
<img
|
||
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5w4Kcw4H.png"
|
||
alt="icon"
|
||
style={{
|
||
width: '24px',
|
||
height: '24px',
|
||
marginRight: '8px',
|
||
verticalAlign: 'middle'
|
||
}}
|
||
/>
|
||
企业内推岗位库
|
||
</p>
|
||
<button
|
||
className="view-all-jobs-btn"
|
||
onClick={handleJobWrapperClick}
|
||
>
|
||
查看全部岗位
|
||
</button>
|
||
</div>
|
||
<InfiniteScroll
|
||
loadMore={fetchJobsList}
|
||
hasMore={jobsListHasMore}
|
||
className="company-jobs-page-left-list-wrapper"
|
||
>
|
||
<JobList data={jobs} />
|
||
</InfiniteScroll>
|
||
</div>
|
||
<div className="company-jobs-page-interview-wrapper">
|
||
<div
|
||
className="company-jobs-page-interview"
|
||
>
|
||
<p className="company-jobs-page-title">
|
||
<img
|
||
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5wqNngw9.png"
|
||
alt="icon"
|
||
style={{
|
||
width: '24px',
|
||
height: '24px',
|
||
marginRight: '8px',
|
||
verticalAlign: 'middle'
|
||
}}
|
||
/>
|
||
岗位面试状态
|
||
</p>
|
||
<InfiniteScroll
|
||
loadMore={fetchInterviewsData}
|
||
hasMore={interviewsHasMore}
|
||
empty={interviews.length === 0}
|
||
className="company-jobs-page-interview-list"
|
||
>
|
||
{interviews.map((item) => (
|
||
<li
|
||
className="company-jobs-page-interview-item"
|
||
key={item.id}
|
||
>
|
||
<div className="company-jobs-page-interview-item-info">
|
||
<p className="company-jobs-page-interview-item-info-position">
|
||
{item.position}
|
||
</p>
|
||
{item.job?.tags?.length > 0 ? (
|
||
<ul className="company-jobs-page-interview-item-info-tags">
|
||
{item.job.tags.map((tag) => (
|
||
<li
|
||
className="company-jobs-page-interview-item-info-tag"
|
||
key={tag}
|
||
>
|
||
{tag}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : null}
|
||
<span className="company-jobs-page-interview-item-info-salary">
|
||
{item.job?.salary || "面议"}
|
||
</span>
|
||
</div>
|
||
<div className="company-jobs-page-interview-item-btn-wrapper">
|
||
<span>{item.interviewTime}</span>
|
||
<div
|
||
className={`company-jobs-page-interview-item-btn ${
|
||
item.status !== "COMPLETED" &&
|
||
"company-jobs-page-interview-item-btn-active"
|
||
}`}
|
||
onClick={(e) => handleStatusClick(e, item)}
|
||
style={{ cursor: 'pointer' }}
|
||
>
|
||
{item.statusText}
|
||
</div>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</InfiniteScroll>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
{/* 面试状态下拉栏 */}
|
||
<InterviewStatusDropdown
|
||
status={selectedInterview?.status}
|
||
statusText={selectedInterview?.statusText}
|
||
isOpen={dropdownOpen}
|
||
onClose={handleCloseDropdown}
|
||
position={dropdownPosition}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CompanyJobsPage;
|