feat: 🎸 优化了各个页面的加载

This commit is contained in:
2025-08-26 11:40:10 +08:00
parent 4d92976911
commit 24ffe99f24
7 changed files with 209 additions and 198 deletions

View File

@@ -92,7 +92,7 @@ const InfiniteScroll = ({
{/* 滚动加载指示器 */} {/* 滚动加载指示器 */}
{loading && hasMore && ( {loading && hasMore && (
<div className="loading-container"> <div className="loading-container">
<Spin size="small" /> <Spin size={20} />
<span className="loading-text">加载中...</span> <span className="loading-text">加载中...</span>
</div> </div>
)} )}

View File

@@ -11,6 +11,10 @@
align-items: center; align-items: center;
position: relative; position: relative;
.company-jobs-page-spin {
margin: 200px 500px;
}
.company-jobs-page-title { .company-jobs-page-title {
width: 100%; width: 100%;
height: 42px; height: 42px;

View File

@@ -1,11 +1,16 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Spin, Empty } from "@arco-design/web-react";
import { mapJobList, mapInterviewList } from "@/utils/dataMapper"; import { mapJobList, mapInterviewList } from "@/utils/dataMapper";
import InfiniteScroll from "@/components/InfiniteScroll"; import InfiniteScroll from "@/components/InfiniteScroll";
import toast from "@/components/Toast"; import toast from "@/components/Toast";
import JobList from "./components/JobList"; import JobList from "./components/JobList";
import { getCompanyJobsPageData, getJobsList, getInterviewsList } from "@/services"; import {
getCompanyJobsPageData,
getJobsList,
getInterviewsList,
} from "@/services";
import "./index.css"; import "./index.css";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -31,7 +36,7 @@ const CompanyJobsPage = () => {
const res = await getCompanyJobsPageData({ const res = await getCompanyJobsPageData({
studentId: studentInfo?.id, studentId: studentInfo?.id,
}); });
if (res?.success) { if (res?.success) {
// 设置岗位数据 // 设置岗位数据
if (res.data?.jobs) { if (res.data?.jobs) {
@@ -42,32 +47,34 @@ const CompanyJobsPage = () => {
setJobsListPage(2); // 下次从第2页开始 setJobsListPage(2); // 下次从第2页开始
} }
} }
// 设置面试数据 // 设置面试数据
if (res.data?.interviews && studentInfo?.id) { if (res.data?.interviews && studentInfo?.id) {
const mappedInterviews = mapInterviewList(res.data.interviews.list || []); const mappedInterviews = mapInterviewList(
res.data.interviews.list || []
);
setInterviews(mappedInterviews); setInterviews(mappedInterviews);
setInterviewsHasMore(res.data.interviews.hasMore); setInterviewsHasMore(res.data.interviews.hasMore);
if (mappedInterviews.length > 0) { if (mappedInterviews.length > 0) {
setInterviewsPage(2); // 下次从第2页开始 setInterviewsPage(2); // 下次从第2页开始
} }
} }
setInitialDataLoaded(true); setInitialDataLoaded(true);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch initial page data:', error); console.error("Failed to fetch initial page data:", error);
// 如果聚合接口失败,回退到原来的方式 // 如果聚合接口失败,回退到原来的方式
setInitialDataLoaded(true); setInitialDataLoaded(true);
// 显示错误信息给用户 // 显示错误信息给用户
if (toast && toast.error) { if (toast && toast.error) {
toast.error('加载数据失败,请刷新重试'); toast.error("加载数据失败,请刷新重试");
} }
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
fetchInitialData(); fetchInitialData();
}, [studentInfo?.id]); }, [studentInfo?.id]);
@@ -77,7 +84,7 @@ const CompanyJobsPage = () => {
if (!initialDataLoaded || (interviewsPage === 1 && interviews.length > 0)) { if (!initialDataLoaded || (interviewsPage === 1 && interviews.length > 0)) {
return; return;
} }
if (studentInfo?.id) { if (studentInfo?.id) {
const res = await getInterviewsList({ const res = await getInterviewsList({
page: interviewsPage, page: interviewsPage,
@@ -89,9 +96,13 @@ const CompanyJobsPage = () => {
const mappedInterviews = mapInterviewList(res.data || []); const mappedInterviews = mapInterviewList(res.data || []);
setInterviews((prevList) => { setInterviews((prevList) => {
// 去重处理:过滤掉已存在的数据 // 去重处理:过滤掉已存在的数据
const existingIds = new Set(prevList.map(interview => interview.id)); const existingIds = new Set(
const newInterviews = mappedInterviews.filter(interview => !existingIds.has(interview.id)); prevList.map((interview) => interview.id)
);
const newInterviews = mappedInterviews.filter(
(interview) => !existingIds.has(interview.id)
);
const newList = [...prevList, ...newInterviews]; const newList = [...prevList, ...newInterviews];
if (res.total <= newList?.length) { if (res.total <= newList?.length) {
setInterviewsHasMore(false); setInterviewsHasMore(false);
@@ -115,12 +126,12 @@ const CompanyJobsPage = () => {
if (!initialDataLoaded || (jobsListPage === 1 && jobs.length > 0)) { if (!initialDataLoaded || (jobsListPage === 1 && jobs.length > 0)) {
return; return;
} }
// 防止重复请求 // 防止重复请求
if (jobsListPage === 1 && jobs.length === 0) { if (jobsListPage === 1 && jobs.length === 0) {
return; // 初始数据应该通过聚合接口加载 return; // 初始数据应该通过聚合接口加载
} }
try { try {
const res = await getJobsList({ const res = await getJobsList({
page: jobsListPage, page: jobsListPage,
@@ -132,9 +143,9 @@ const CompanyJobsPage = () => {
const mappedJobs = mapJobList(res.data); const mappedJobs = mapJobList(res.data);
setJobs((prevList) => { setJobs((prevList) => {
// 去重处理:过滤掉已存在的数据 // 去重处理:过滤掉已存在的数据
const existingIds = new Set(prevList.map(job => job.id)); const existingIds = new Set(prevList.map((job) => job.id));
const newJobs = mappedJobs.filter(job => !existingIds.has(job.id)); const newJobs = mappedJobs.filter((job) => !existingIds.has(job.id));
const newList = [...prevList, ...newJobs]; const newList = [...prevList, ...newJobs];
if (res.total <= newList?.length) { if (res.total <= newList?.length) {
setJobsListHasMore(false); setJobsListHasMore(false);
@@ -156,118 +167,117 @@ const CompanyJobsPage = () => {
navigate("/company-jobs-list"); 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 ( return (
<div className="company-jobs-page-wrapper"> <div className="company-jobs-page-wrapper">
<div className="company-jobs-page"> <div className="company-jobs-page">
<div className="company-jobs-page-left" onClick={handleJobWrapperClick}> {loading ? (
<p className="company-jobs-page-title">企业内推岗位库</p> <Spin size={80} className="company-jobs-page-spin" />
<InfiniteScroll ) : (
loadMore={fetchJobsList} <>
hasMore={jobsListHasMore} <div
className="company-jobs-page-left-list-wrapper" className="company-jobs-page-left"
> onClick={handleJobWrapperClick}
<JobList data={jobs} backgroundColor="#F7F8FA" />
</InfiniteScroll>
</div>
<div className="company-jobs-page-interview-wrapper">
<div
className={`${
isExpand
? "company-jobs-page-interview"
: "company-jobs-page-interview company-jobs-page-interview-expand"
}`}
>
<p className="company-jobs-page-title">内推岗位面试</p>
<InfiniteScroll
loadMore={fetchInterviewsData}
hasMore={interviewsHasMore}
empty={interviews.length === 0}
className="company-jobs-page-interview-list"
> >
{interviews.map((item) => ( <p className="company-jobs-page-title">企业内推岗位库</p>
<li className="company-jobs-page-interview-item" key={item.id}> <InfiniteScroll
<div className="company-jobs-page-interview-item-info"> loadMore={fetchJobsList}
<p className="company-jobs-page-interview-item-info-position"> hasMore={jobsListHasMore}
{item.position} className="company-jobs-page-left-list-wrapper"
</p> >
{item.job?.tags?.length > 0 ? ( <JobList data={jobs} backgroundColor="#F7F8FA" />
<ul className="company-jobs-page-interview-item-info-tags"> </InfiniteScroll>
{item.job.tags.map((tag) => ( </div>
<li <div className="company-jobs-page-interview-wrapper">
className="company-jobs-page-interview-item-info-tag" <div
key={tag} className={`${
> isExpand
{tag} ? "company-jobs-page-interview"
</li> : "company-jobs-page-interview company-jobs-page-interview-expand"
))} }`}
</ul> >
) : null} <p className="company-jobs-page-title">内推岗位面试</p>
<span className="company-jobs-page-interview-item-info-salary"> <InfiniteScroll
{item.job?.salary || "面议"} loadMore={fetchInterviewsData}
</span> hasMore={interviewsHasMore}
</div> empty={interviews.length === 0}
<div className="company-jobs-page-interview-item-btn-wrapper"> className="company-jobs-page-interview-list"
<span>{item.interviewTime}</span> >
<div {interviews.map((item) => (
className={`company-jobs-page-interview-item-btn ${ <li
item.status !== "COMPLETED" && className="company-jobs-page-interview-item"
"company-jobs-page-interview-item-btn-active" key={item.id}
}`}
> >
{item.statusText} <div className="company-jobs-page-interview-item-info">
</div> <p className="company-jobs-page-interview-item-info-position">
</div> {item.position}
</li> </p>
))} {item.job?.tags?.length > 0 ? (
</InfiniteScroll> <ul className="company-jobs-page-interview-item-info-tags">
</div> {item.job.tags.map((tag) => (
</div> <li
<div className="company-jobs-page-interview-item-info-tag"
className={ key={tag}
isExpand >
? "company-jobs-page-process-wrapper-expand" {tag}
: "company-jobs-page-process-wrapper-close" </li>
} ))}
> </ul>
<div ) : null}
className="company-jobs-page-process-wrapper-title" <span className="company-jobs-page-interview-item-info-salary">
onClick={() => setIsExpand(!isExpand)} {item.job?.salary || "面议"}
> </span>
岗位陪跑流程 </div>
</div> <div className="company-jobs-page-interview-item-btn-wrapper">
<div className="company-jobs-page-process-content"> <span>{item.interviewTime}</span>
<div className="company-jobs-page-process-item-icon icon1"> <div
<p>内推岗位简历投递</p> className={`company-jobs-page-interview-item-btn ${
item.status !== "COMPLETED" &&
"company-jobs-page-interview-item-btn-active"
}`}
>
{item.statusText}
</div>
</div>
</li>
))}
</InfiniteScroll>
</div>
</div> </div>
<div className="company-jobs-page-process-item-round-dot icon2" /> <div
<div className="company-jobs-page-process-item-icon icon3"> className={
<p>岗位简历接收</p> isExpand
? "company-jobs-page-process-wrapper-expand"
: "company-jobs-page-process-wrapper-close"
}
>
<div
className="company-jobs-page-process-wrapper-title"
onClick={() => setIsExpand(!isExpand)}
>
岗位陪跑流程
</div>
<div className="company-jobs-page-process-content">
<div className="company-jobs-page-process-item-icon icon1">
<p>内推岗位简历投递</p>
</div>
<div className="company-jobs-page-process-item-round-dot icon2" />
<div className="company-jobs-page-process-item-icon icon3">
<p>岗位简历接收</p>
</div>
<div className="company-jobs-page-process-item-icon icon4">
<p>面试时间地点确定</p>
</div>
<div className="company-jobs-page-process-item-icon icon5">
<p>参与企业面试</p>
</div>
<div className="company-jobs-page-process-item-round-dot icon6" />
<div className="company-jobs-page-process-item-icon icon7">
<p>企业offer发送</p>
</div>
</div>
</div> </div>
<div className="company-jobs-page-process-item-icon icon4"> </>
<p>面试时间地点确定</p> )}
</div>
<div className="company-jobs-page-process-item-icon icon5">
<p>参与企业面试</p>
</div>
<div className="company-jobs-page-process-item-round-dot icon6" />
<div className="company-jobs-page-process-item-icon icon7">
<p>企业offer发送</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -3,7 +3,12 @@
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: flex;
.resume-interview-spin,
.empty-data {
margin: auto;
}
.resume-interview-navigation { .resume-interview-navigation {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@@ -1,4 +1,5 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import { Spin, Empty } from "@arco-design/web-react";
import toast from "@/components/Toast"; import toast from "@/components/Toast";
import InterviewQuestionsModal from "./components/InterviewQuestionsModal"; import InterviewQuestionsModal from "./components/InterviewQuestionsModal";
import ResumeInfoModal from "@/pages/CompanyJobsPage/components/ResumeInfoModal"; import ResumeInfoModal from "@/pages/CompanyJobsPage/components/ResumeInfoModal";
@@ -110,79 +111,74 @@ const ResumeInterviewPage = () => {
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, [pageData?.industries]); }, [pageData?.industries]);
if (loading) {
return (
<div className="resume-interview-page">
<div className="loading">加载中...</div>
</div>
);
}
if (!pageData) {
return (
<div className="resume-interview-page">
<div className="error">加载失败请刷新重试</div>
</div>
);
}
return ( return (
<div className="resume-interview-page"> <div className="resume-interview-page">
<ul className="resume-interview-navigation"> {loading ? (
<div className="navigation-tabs"> <Spin size={80} className="resume-interview-spin" />
{pageData.industries.map((industry) => ( ) : pageData ? (
<li <>
key={industry.id} <ul className="resume-interview-navigation">
className={`resume-interview-navigation-item ${ <div className="navigation-tabs">
activeIndustry === industry.id ? "active" : "" {pageData.industries.map((industry) => (
}`} <li
onClick={() => handleNavClick(industry.id)} key={industry.id}
> className={`resume-interview-navigation-item ${
{industry.name} activeIndustry === industry.id ? "active" : ""
</li> }`}
))} onClick={() => handleNavClick(industry.id)}
</div> >
</ul> {industry.name}
<ul className="resume-interview-content-wrapper"> </li>
{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) => (
<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>
<span>详情 &gt;</span>
</div>
</li>
))}
</ul>
<ul className="resumes-list">
{item.questions.map((question) => (
<li
key={question.id}
className="resume-item"
onClick={() => handleQuestionClick({ ...item, question })}
>
<p>{question.question}</p>
</li>
))}
</ul>
</div> </div>
</li> </ul>
))} <ul className="resume-interview-content-wrapper">
</ul> {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) => (
<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>
<span>详情 &gt;</span>
</div>
</li>
))}
</ul>
<ul className="resumes-list">
{item.questions.map((question) => (
<li
key={question.id}
className="resume-item"
onClick={() =>
handleQuestionClick({ ...item, question })
}
>
<p>{question.question}</p>
</li>
))}
</ul>
</div>
</li>
))}
</ul>
</>
) : (
<Empty description="暂无数据" className="empty-data" />
)}
<InterviewQuestionsModal <InterviewQuestionsModal
visible={interviewModalVisible} visible={interviewModalVisible}
onClose={handleCloseInterviewModal} onClose={handleCloseInterviewModal}

View File

@@ -5,7 +5,6 @@ export async function getLoginStudentProgress() {
return request({ return request({
url: `/api/students/me/progress`, url: `/api/students/me/progress`,
method: "GET", method: "GET",
namespace: "profileLoading",
}); });
} }
@@ -14,7 +13,7 @@ export async function getDashboardStatistics() {
return request({ return request({
url: `/api/dashboard/stats`, url: `/api/dashboard/stats`,
method: "GET", method: "GET",
namespace: "dashboardLoading", namespace: "globalLoading",
}); });
} }
@@ -24,7 +23,6 @@ export async function getClassRank(params = {}) {
url: `/api/rankings/class`, url: `/api/rankings/class`,
method: "GET", method: "GET",
params: params, params: params,
namespace: "profileLoading",
}); });
} }
@@ -33,7 +31,6 @@ export async function getMyRanking() {
return request({ return request({
url: `/api/rankings/my-ranking`, url: `/api/rankings/my-ranking`,
method: "GET", method: "GET",
namespace: "profileLoading",
}); });
} }
@@ -42,6 +39,5 @@ export async function getProfileOverview() {
return request({ return request({
url: `/api/profile/overview`, url: `/api/profile/overview`,
method: "GET", method: "GET",
namespace: "profileLoading",
}); });
} }