仪表盘接口调整

This commit is contained in:
2025-08-25 20:56:56 +08:00
parent 2c67099e95
commit 11b529c502
15 changed files with 700 additions and 208 deletions

View File

@@ -1,11 +1,11 @@
import { useState } 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 { 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 { getJobsList, getInterviewsList } from "@/services"; import { getCompanyJobsPageData, getJobsList, getInterviewsList } from "@/services";
import "./index.css"; import "./index.css";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -19,10 +19,57 @@ const CompanyJobsPage = () => {
const [interviews, setInterviews] = useState([]); const [interviews, setInterviews] = useState([]);
const [interviewsPage, setInterviewsPage] = useState(1); const [interviewsPage, setInterviewsPage] = useState(1);
const [interviewsHasMore, setInterviewsHasMore] = useState(true); const [interviewsHasMore, setInterviewsHasMore] = useState(true);
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
// 获取面试信息 // 初始化页面数据 - 使用聚合接口
useEffect(() => {
const fetchInitialData = async () => {
try {
const res = await getCompanyJobsPageData({
studentId: studentInfo?.id,
});
if (res?.success) {
// 设置岗位数据
if (res.data?.jobs) {
const mappedJobs = mapJobList(res.data.jobs.list || []);
setJobs(mappedJobs);
setJoblistHasMore(res.data.jobs.hasMore);
if (mappedJobs.length > 0) {
setJobsListPage(2); // 下次从第2页开始
}
}
// 设置面试数据
if (res.data?.interviews && studentInfo?.id) {
const mappedInterviews = mapInterviewList(res.data.interviews.list || []);
setInterviews(mappedInterviews);
setInterviewsHasMore(res.data.interviews.hasMore);
if (mappedInterviews.length > 0) {
setInterviewsPage(2); // 下次从第2页开始
}
}
setInitialDataLoaded(true);
}
} catch (error) {
console.error('Failed to fetch initial page data:', error);
// 如果聚合接口失败,回退到原来的方式
setInitialDataLoaded(true);
}
};
fetchInitialData();
}, [studentInfo?.id]);
// 获取面试信息 - 用于分页加载更多
const fetchInterviewsData = async () => { const fetchInterviewsData = async () => {
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
if (!initialDataLoaded || (interviewsPage === 1 && interviews.length > 0)) {
return;
}
if (studentInfo?.id) { if (studentInfo?.id) {
const res = await getInterviewsList({ const res = await getInterviewsList({
page: interviewsPage, page: interviewsPage,
@@ -42,14 +89,21 @@ const CompanyJobsPage = () => {
return newList; return newList;
}); });
} else { } else {
setInterviews([]); if (interviewsPage === 1) {
setInterviews([]);
}
toast.error(res.message); toast.error(res.message);
} }
} }
}; };
// 获取企业内推岗位 // 获取企业内推岗位 - 用于分页加载更多
const fetchJobsList = async () => { const fetchJobsList = async () => {
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
if (!initialDataLoaded || (jobsListPage === 1 && jobs.length > 0)) {
return;
}
try { try {
const res = await getJobsList({ const res = await getJobsList({
page: jobsListPage, page: jobsListPage,
@@ -71,7 +125,9 @@ const CompanyJobsPage = () => {
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch data:", error); console.error("Failed to fetch data:", error);
setJobs([]); if (jobsListPage === 1) {
setJobs([]);
}
} }
}; };

View File

@@ -33,5 +33,48 @@
} }
.arco-calendar-cell { .arco-calendar-cell {
height: 40px !important; height: 40px !important;
position: relative;
cursor: pointer;
}
.calendar-date-cell {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
&.is-today {
.date-number {
background-color: rgb(0, 119, 255);
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
}
}
}
.date-number {
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.task-dot {
position: absolute;
bottom: 4px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: rgb(0, 119, 255);
} }
} }

View File

@@ -1,25 +1,68 @@
import "./index.css"; import "./index.css";
import { Calendar } from "@arco-design/web-react"; import { Calendar, Skeleton } from "@arco-design/web-react";
// 获取当前日期 const CalendarTaskModule = ({ tasks = [], selectedDate, onDateChange, loading }) => {
const today = new Date(); if (loading) {
// 提取年、月、日 return (
const year = today.getFullYear(); <div className="module-calendar-task-wrapper">
const month = String(today.getMonth() + 1).padStart(2, "0"); // 月份从0开始需+1并补0 <p className="module-calendar-task-title">日历</p>
const day = String(today.getDate()).padStart(2, "0"); // 日期补0 <Skeleton loading={true} />
// 格式化日期字符串 </div>
const formattedDate = `${year}-${month}-${day}`; );
}
// 格式化今天的日期
const today = new Date();
const formattedToday = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
// 获取有任务的日期集合
const datesWithTasks = new Set(tasks?.map(task => task.date) || []);
// 日历单元格渲染函数
const dateRender = (current) => {
const dateStr = current.format('YYYY-MM-DD');
const hasTasks = datesWithTasks.has(dateStr);
const isToday = dateStr === formattedToday;
return (
<div className={`calendar-date-cell ${hasTasks ? 'has-tasks' : ''} ${isToday ? 'is-today' : ''}`}>
<div className="date-number">{current.date()}</div>
{hasTasks && <div className="task-dot"></div>}
</div>
);
};
const handleDateChange = (date, dateString) => {
if (onDateChange) {
// Arco Calendar passes a dayjs object
if (date && date.format) {
// Convert dayjs to Date object
const dateStr = date.format('YYYY-MM-DD');
const dateObj = new Date(dateStr + 'T00:00:00');
if (!isNaN(dateObj.getTime())) {
onDateChange(dateObj);
}
} else if (dateString) {
// Fallback to dateString if available
const dateObj = new Date(dateString + 'T00:00:00');
if (!isNaN(dateObj.getTime())) {
onDateChange(dateObj);
}
}
}
};
const CalendarTaskModule = () => {
return ( return (
<div className="module-calendar-task-wrapper"> <div className="module-calendar-task-wrapper">
<p className="module-calendar-task-title">日历</p> <p className="module-calendar-task-title">日历</p>
<Calendar <Calendar
panelWidth="300" panelWidth="300"
panel panel
defaultValue={formattedDate} defaultValue={formattedToday}
value={selectedDate ? selectedDate.toISOString().split('T')[0] : formattedToday}
style={{ fontSize: "18px" }} style={{ fontSize: "18px" }}
onChange={(a) => console.log(a)} onChange={handleDateChange}
dateRender={dateRender}
/> />
</div> </div>
); );

View File

@@ -1,11 +1,35 @@
import { Skeleton } from "@arco-design/web-react";
import "./index.css"; import "./index.css";
const StartClass = () => { const StartClass = ({ courses, tasks, loading }) => {
// 获取当前时间用于筛选 if (loading) {
const now = new Date(); return (
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); <div className="start-class-wrapper">
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); <p className="start-class-title">开始上课</p>
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000); <Skeleton loading={true} />
</div>
);
}
// 获取待办任务
const pendingTasks = tasks?.allTasks?.filter(task =>
task.status === 'PENDING' || task.status === 'IN_PROGRESS'
).slice(0, 5) || [];
// 格式化日期显示
const formatCourseDate = (dateStr) => {
const date = new Date(dateStr);
const today = new Date();
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
if (date.toDateString() === today.toDateString()) {
return '今日已上';
} else if (date.toDateString() === yesterday.toDateString()) {
return '昨日已上';
} else {
return `${date.getMonth() + 1}/${date.getDate()}已上`;
}
};
return ( return (
<div className="start-class-wrapper"> <div className="start-class-wrapper">
@@ -14,32 +38,53 @@ const StartClass = () => {
<li className="start-class-item"> <li className="start-class-item">
<p className="start-class-item-title">最近课程</p> <p className="start-class-item-title">最近课程</p>
<ul className="start-class-item-list"> <ul className="start-class-item-list">
<li className="start-class-item-list-item"> {courses?.recentCourses?.length > 0 ? (
昨日已上xxxxx计算机课 courses.recentCourses.slice(0, 3).map((course, index) => (
</li> <li key={index} className="start-class-item-list-item">
<li className="start-class-item-list-item"> {formatCourseDate(course.date)}{course.courseName}
今日已上xxxxxx高等数学课 </li>
</li> ))
<li className="start-class-item-list-item"> ) : (
今日计划xxxxxx中国现代史 <li className="start-class-item-list-item">暂无最近课程记录</li>
</li> )}
{courses?.todaysCourses?.length > 0 && (
courses.todaysCourses.slice(0, 2).map((course, index) => (
<li key={`today-${index}`} className="start-class-item-list-item">
今日计划{course.courseName}
</li>
))
)}
</ul> </ul>
</li> </li>
<li className="start-class-item"> <li className="start-class-item">
<p className="start-class-item-title">下次上课</p> <p className="start-class-item-title">下次上课</p>
<ul className="start-class-item-list"> <ul className="start-class-item-list">
<li className="start-class-item-list-item">xxxxxx中国现代史</li> {courses?.nextCourse ? (
<li className="start-class-item-list-item">
{courses.nextCourse.courseName}
{courses.nextCourse.classroom && (
<span className="classroom"> - {courses.nextCourse.classroom}</span>
)}
</li>
) : (
<li className="start-class-item-list-item">暂无安排课程</li>
)}
</ul> </ul>
</li> </li>
<li className="start-class-item"> <li className="start-class-item">
<p className="start-class-item-title">待办事项2</p> <p className="start-class-item-title">
待办事项{pendingTasks.length}
</p>
<ul className="start-class-item-list"> <ul className="start-class-item-list">
<li className="start-class-item-list-item"> {pendingTasks.length > 0 ? (
计算机课 Excel 数据处理 pendingTasks.map((task) => (
</li> <li key={task.id} className="start-class-item-list-item">
<li className="start-class-item-list-item"> {task.courseName}{task.title}
高等数学课线性代数的矩阵运算题 </li>
</li> ))
) : (
<li className="start-class-item-list-item">暂无待办事项</li>
)}
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@@ -1,13 +1,25 @@
import EchartsProgress from "../EchartsProgress"; import EchartsProgress from "../EchartsProgress";
import { Skeleton } from "@arco-design/web-react";
import "./index.css"; import "./index.css";
const StudyStatus = () => { const StudyStatus = ({ progress = 0, loading }) => {
if (loading) {
return (
<div className="module-study-status-wrapper">
<p className="module-study-status-title">学习情况</p>
<Skeleton loading={true} />
</div>
);
}
return ( return (
<div className="module-study-status-wrapper"> <div className="module-study-status-wrapper">
<p className="module-study-status-title">学习情况</p> <p className="module-study-status-title">学习情况</p>
<div className="progress-container"> <div className="progress-container">
<EchartsProgress percent={75} strokeWidth={20} /> <EchartsProgress percent={progress || 0} strokeWidth={20} />
<span className="progress-text">整体课程完成进度</span> <span className="progress-text">
整体课程完成进度 ({progress || 0}%)
</span>
</div> </div>
</div> </div>
); );

View File

@@ -18,6 +18,55 @@
width: 100%; width: 100%;
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
display: flex;
align-items: center;
justify-content: space-between;
.task-count {
font-size: 14px;
color: #86909c;
font-weight: normal;
}
}
.no-tasks {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.task-type {
font-weight: bold;
color: #1d2129;
}
.task-status {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
&.status-pending {
background-color: #fff3cd;
color: #856404;
}
&.status-in_progress {
background-color: #d1ecf1;
color: #0c5460;
}
&.status-completed {
background-color: #d4edda;
color: #155724;
}
}
.classroom {
color: #86909c;
font-size: 12px;
line-height: 30px; line-height: 30px;
color: #262626; color: #262626;
} }

View File

@@ -1,45 +1,103 @@
import { mockData } from "@/data/mockData"; import { Avatar, Skeleton, Empty } from "@arco-design/web-react";
import { Avatar } from "@arco-design/web-react";
import "./index.css"; import "./index.css";
const TaskList = () => { const TaskList = ({ tasks = [], selectedDate, loading }) => {
const { tasks } = mockData; if (loading) {
return (
<div className="module-tasks-wrapper">
<p className="module-tasks-title">事项</p>
<Skeleton loading={true} />
</div>
);
}
const formatDate = (date) => {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const getTaskTypeText = (type) => {
const typeMap = {
'HOMEWORK': '作业',
'PROJECT': '项目',
'REPORT': '报告',
'INTERVIEW': '面试',
'OTHER': '其他'
};
return typeMap[type] || type;
};
const getPriorityClass = (priority) => {
const classMap = {
'URGENT': 'urgent',
'HIGH': 'high',
'MEDIUM': 'medium',
'LOW': 'low'
};
return classMap[priority] || 'medium';
};
return ( return (
<div className="module-tasks-wrapper"> <div className="module-tasks-wrapper">
<p className="module-tasks-title">事项</p> <p className="module-tasks-title">
<ul className="module-tasks-list"> 事项 - {formatDate(selectedDate)}
{tasks.map((item, index) => ( {tasks.length > 0 && <span className="task-count">({tasks.length})</span>}
<li key={index} className="module-tasks-item"> </p>
<div className="module-tasks-item-info">
<Avatar className="module-tasks-item-info-avatar"> {tasks.length === 0 ? (
<img alt="avatar" src={item?.avatar} /> <div className="no-tasks">
</Avatar> <Empty description="该日无事项" />
<span className="module-tasks-item-info-teacher-name"> </div>
{item?.teacher} ) : (
</span> <ul className="module-tasks-list">
<span className="module-tasks-item-info-time">{item?.time}</span> {tasks.map((item, index) => (
</div> <li key={item.id} className="module-tasks-item">
<div <div className="module-tasks-item-info">
className={`module-tasks-item-content ${ <Avatar className="module-tasks-item-info-avatar" size="small">
index === tasks.length - 1 {item?.teacherAvatar ? (
? "module-tasks-item-content-last" <img alt="avatar" src={item.teacherAvatar} />
: "" ) : (
}`} item?.teacherName?.charAt(0) || 'T'
> )}
<div className="module-tasks-item-content-info"> </Avatar>
<p>单元名称{item?.type}</p> <span className="module-tasks-item-info-teacher-name">
<div> {item?.teacherName || '未知教师'}
课程名称{item?.course} </span>
<span className="module-tasks-item-content-info-duration"> </div>
{item?.duration} <div
</span> className={`module-tasks-item-content ${
index === tasks.length - 1
? "module-tasks-item-content-last"
: ""
}`}
>
<div className="module-tasks-item-content-info">
<p>
<span className="task-type">
{getTaskTypeText(item.type)}
</span>
{item?.title}
</p>
<div>
课程名称{item?.courseName}
<span className="module-tasks-item-content-info-duration">
{item?.duration}
</span>
<span className={`task-status status-${item.status?.toLowerCase()}`}>
{item.status === 'PENDING' ? '待完成' :
item.status === 'IN_PROGRESS' ? '进行中' :
item.status === 'COMPLETED' ? '已完成' : '未知'}
</span>
</div>
</div> </div>
</div> </div>
</div> </li>
</li> ))}
))} </ul>
</ul> )}
</div> </div>
); );
}; };

View File

@@ -6,44 +6,46 @@ import StudyStatus from "./components/StudyStatus";
import Rank from "@/components/Rank"; import Rank from "@/components/Rank";
import StageProgress from "@/components/StageProgress"; import StageProgress from "@/components/StageProgress";
import TaskList from "./components/TaskList"; import TaskList from "./components/TaskList";
import { getClassRanking, getStudyRecordsProgress } from "@/services"; import { getDashboardStatistics } from "@/services";
import "./index.css"; import "./index.css";
const Dashboard = () => { const Dashboard = () => {
const [rankingData, setRankingData] = useState(null); const [dashboardData, setDashboardData] = useState(null);
const [rankingLoading, setRankingLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedDate, setSelectedDate] = useState(new Date());
useEffect(() => { useEffect(() => {
fetchRankingData(); fetchDashboardData();
fetchLearningProgressSummary();
}, []); }, []);
// 获取班级排名数据 // 获取仪表板完整数据
const fetchRankingData = async () => { const fetchDashboardData = async () => {
try { try {
setRankingLoading(true); setLoading(true);
const response = await getClassRanking(); const response = await getDashboardStatistics();
if (response && response.success) { if (response && response.success) {
setRankingData(response.data); setDashboardData(response.data);
} else if (response) { } else if (response) {
// 兼容直接返回数据的情况 // 兼容直接返回数据的情况
setRankingData(response); setDashboardData(response);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch ranking data:', error); console.error('Failed to fetch dashboard data:', error);
} finally { } finally {
setRankingLoading(false); setLoading(false);
} }
}; };
// 获取整体学习进度 // 根据选中日期筛选任务
const fetchLearningProgressSummary = async () => { const getTasksForDate = (date) => {
try { if (!dashboardData?.tasks?.allTasks) return [];
const res = await getStudyRecordsProgress(); // Check if date is valid before calling toISOString
console.log("learningProgressSummary", res); if (!date || isNaN(date.getTime())) {
} catch (error) { console.warn('Invalid date provided to getTasksForDate:', date);
console.error('Failed to fetch learning progress:', error); return [];
} }
const dateStr = date.toISOString().split('T')[0];
return dashboardData.tasks.allTasks.filter(task => task.date === dateStr);
}; };
return ( return (
@@ -51,12 +53,33 @@ const Dashboard = () => {
<StageProgress showBlockageAlert={true} /> <StageProgress showBlockageAlert={true} />
<div className="dashboard-grid"> <div className="dashboard-grid">
<StartClass /> <StartClass
courses={dashboardData?.courses}
tasks={dashboardData?.tasks}
loading={loading}
/>
<QuickAccess /> <QuickAccess />
<CalendarTaskModule /> <CalendarTaskModule
<StudyStatus /> tasks={dashboardData?.tasks?.allTasks}
<Rank data={rankingData} loading={rankingLoading} /> selectedDate={selectedDate}
<TaskList /> onDateChange={setSelectedDate}
loading={loading}
/>
<StudyStatus
progress={dashboardData?.overview?.overallProgress}
loading={loading}
/>
<Rank
data={dashboardData?.ranking ? {
rankings: dashboardData.ranking.topStudents
} : null}
loading={loading}
/>
<TaskList
tasks={getTasksForDate(selectedDate)}
selectedDate={selectedDate}
loading={loading}
/>
</div> </div>
</div> </div>
); );

View File

@@ -3,66 +3,107 @@ import { useSelector } from "react-redux";
import StudyProgress from "../StudyProgress"; import StudyProgress from "../StudyProgress";
import ScoreRingChart from "../ScoreRingChart"; import ScoreRingChart from "../ScoreRingChart";
import AttendanceRingChart from "../AttendanceRingChart"; import AttendanceRingChart from "../AttendanceRingChart";
import { getDashboardStatistics } from "@/services";
import "./index.css"; import "./index.css";
import { useEffect } from "react";
const ringData = [
{
value: 80,
name: "我的",
title: {
offsetCenter: ["-30%", "-90%"],
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#2C7AFF" },
{ offset: 1, color: "#00AEFF" },
]),
},
}, // 外环
{
value: 60,
name: "班级",
title: {
offsetCenter: ["-60%", "-90%"],
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#4564FF" },
{ offset: 1, color: "#AB2CFF" },
]),
},
}, // 中环
];
const attendanceData = [ const StudyStudes = ({ studyStatistics, learningProgress, loading = false }) => {
{
name: "出勤率",
value: 22,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#2C7AFF" },
{ offset: 1, color: "#00D5FF" },
]),
},
},
{ name: "缺勤率", value: 8, itemStyle: { color: "#FF9D2C" } },
// 可添加更多状态(如迟到、早退、缺勤等)
];
const StudyStudes = () => {
const studentInfo = useSelector((state) => state.student.studentInfo); const studentInfo = useSelector((state) => state.student.studentInfo);
console.log("StudyStudes props:", { studyStatistics, learningProgress, loading });
// 获取仪表盘数据 // 如果数据还在加载中,显示加载状态
const queryDashboardStatistics = async () => { if (loading) {
const res = await getDashboardStatistics(); return (
console.log(res); <div className="study-studes-card-wrapper">
}; <p className="study-studes-card-title">学习情况</p>
<div>加载中...</div>
</div>
);
}
useEffect(() => { // 使用传入的数据只在undefined时使用默认值0也是有效值
queryDashboardStatistics(); const personalStudyHours = studyStatistics?.studyTime?.personal ?? 0;
}, []); const classAverageStudyHours = studyStatistics?.studyTime?.classAverage ?? 0;
const overallProgress = learningProgress?.overallProgress ?? 0;
const personalCourseProgress = studyStatistics?.courseCompletion?.personalProgress ?? 0;
const classAverageCourseProgress = studyStatistics?.courseCompletion?.classAverageProgress ?? 0;
const personalHomeworkProgress = studyStatistics?.homeworkCompletion?.personalProgress ?? 0;
const classAverageHomeworkProgress = studyStatistics?.homeworkCompletion?.classAverageProgress ?? 0;
const attendanceRate = studyStatistics?.attendance?.attendanceRate ?? 0;
const absenceRate = studyStatistics?.attendance?.absenceRate ?? 0;
// 动态生成环形图数据
const courseRingData = [
{
value: personalCourseProgress,
name: "我的",
title: {
offsetCenter: ["-30%", "-90%"],
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#2C7AFF" },
{ offset: 1, color: "#00AEFF" },
]),
},
},
{
value: classAverageCourseProgress,
name: "班级",
title: {
offsetCenter: ["-60%", "-90%"],
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#4564FF" },
{ offset: 1, color: "#AB2CFF" },
]),
},
},
];
const homeworkRingData = [
{
value: personalHomeworkProgress,
name: "我的",
title: {
offsetCenter: ["-30%", "-90%"],
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#2C7AFF" },
{ offset: 1, color: "#00AEFF" },
]),
},
},
{
value: classAverageHomeworkProgress,
name: "班级",
title: {
offsetCenter: ["-60%", "-90%"],
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#4564FF" },
{ offset: 1, color: "#AB2CFF" },
]),
},
},
];
const attendanceData = [
{
name: "出勤率",
value: attendanceRate,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#2C7AFF" },
{ offset: 1, color: "#00D5FF" },
]),
},
},
{ name: "缺勤率", value: absenceRate, itemStyle: { color: "#FF9D2C" } },
];
return ( return (
<div className="study-studes-card-wrapper"> <div className="study-studes-card-wrapper">
@@ -76,8 +117,8 @@ const StudyStudes = () => {
个人学习时长h 个人学习时长h
</p> </p>
<p className="study-studes-card-study-time-line"> <p className="study-studes-card-study-time-line">
<i style={{ width: "70%" }}> <i style={{ width: `${Math.min(personalStudyHours / classAverageStudyHours * 70, 100)}%` }}>
<span>145</span> <span>{personalStudyHours}</span>
</i> </i>
</p> </p>
</div> </div>
@@ -87,7 +128,7 @@ const StudyStudes = () => {
</p> </p>
<p className="study-studes-card-study-time-line study-studes-card-study-time-line-class"> <p className="study-studes-card-study-time-line study-studes-card-study-time-line-class">
<i style={{ width: "80%" }}> <i style={{ width: "80%" }}>
<span>145</span> <span>{classAverageStudyHours}</span>
</i> </i>
</p> </p>
</div> </div>
@@ -96,7 +137,7 @@ const StudyStudes = () => {
{/* 进度 */} {/* 进度 */}
<li className="study-studes-card-study-progress"> <li className="study-studes-card-study-progress">
<StudyProgress <StudyProgress
value={75} value={overallProgress}
className="study-studes-card-study-progress-chart" className="study-studes-card-study-progress-chart"
/> />
<p className="study-studes-card-study-progress-title"> <p className="study-studes-card-study-progress-title">
@@ -107,32 +148,32 @@ const StudyStudes = () => {
<li className="study-studes-card-curriculum"> <li className="study-studes-card-curriculum">
<ScoreRingChart <ScoreRingChart
title={`整体课程\n完成情况`} title={`整体课程\n完成情况`}
ringData={ringData} ringData={courseRingData}
className="study-studes-card-curriculum-chart" className="study-studes-card-curriculum-chart"
/> />
<div className="study-studes-card-curriculum-info"> <div className="study-studes-card-curriculum-info">
<p>班级平均进度</p> <p>班级平均进度</p>
<span className="study-studes-card-curriculum-info-span1">60%</span> <span className="study-studes-card-curriculum-info-span1">{classAverageCourseProgress}%</span>
</div> </div>
<div className="study-studes-card-curriculum-info"> <div className="study-studes-card-curriculum-info">
<p>我的进度</p> <p>我的进度</p>
<span className="study-studes-card-curriculum-info-span2">60%</span> <span className="study-studes-card-curriculum-info-span2">{personalCourseProgress}%</span>
</div> </div>
</li> </li>
{/* 课后作业完成情况 */} {/* 课后作业完成情况 */}
<li className="study-studes-card-curriculum-homework study-studes-card-curriculum"> <li className="study-studes-card-curriculum-homework study-studes-card-curriculum">
<ScoreRingChart <ScoreRingChart
title={`课后作业\n完成情况`} title={`课后作业\n完成情况`}
ringData={ringData} ringData={homeworkRingData}
className="study-studes-card-curriculum-chart" className="study-studes-card-curriculum-chart"
/> />
<div className="study-studes-card-curriculum-info"> <div className="study-studes-card-curriculum-info">
<p>班级平均进度</p> <p>班级平均进度</p>
<span className="study-studes-card-curriculum-info-span1">60%</span> <span className="study-studes-card-curriculum-info-span1">{classAverageHomeworkProgress}%</span>
</div> </div>
<div className="study-studes-card-curriculum-info"> <div className="study-studes-card-curriculum-info">
<p>我的进度</p> <p>我的进度</p>
<span className="study-studes-card-curriculum-info-span2">60%</span> <span className="study-studes-card-curriculum-info-span2">{personalHomeworkProgress}%</span>
</div> </div>
</li> </li>
{/* 考勤情况 */} {/* 考勤情况 */}
@@ -148,11 +189,11 @@ const StudyStudes = () => {
<div className="study-studes-card-curriculum-info"> <div className="study-studes-card-curriculum-info">
<p>出勤率</p> <p>出勤率</p>
<span className="study-studes-card-curriculum-info-span1">60%</span> <span className="study-studes-card-curriculum-info-span1">{attendanceRate}%</span>
</div> </div>
<div className="study-studes-card-curriculum-info"> <div className="study-studes-card-curriculum-info">
<p>缺勤率</p> <p>缺勤率</p>
<span className="study-studes-card-curriculum-info-span3">60%</span> <span className="study-studes-card-curriculum-info-span3">{absenceRate}%</span>
</div> </div>
</li> </li>
</ul> </ul>

View File

@@ -5,34 +5,45 @@ import Rank from "@/components/Rank";
import StageProgress from "@/components/StageProgress"; import StageProgress from "@/components/StageProgress";
import StudyStudes from "./components/StudyStudes"; import StudyStudes from "./components/StudyStudes";
import { updateStudentInfo } from "@/store/slices/studentSlice"; import { updateStudentInfo } from "@/store/slices/studentSlice";
import { getClassRanking, getStudyRecordsProgress } from "@/services"; import { getProfileOverview } from "@/services";
import "./index.css"; import "./index.css";
const PersonalProfile = () => { const PersonalProfile = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [rankData, setRankData] = useState([]); // 班级排名数据 const [profileData, setProfileData] = useState(null); // 个人档案完整数据
const [loading, setLoading] = useState(true);
const queryLearningProgressSummary = async () => { // 获取个人档案完整数据
const res = await getStudyRecordsProgress(); const queryProfileOverview = async () => {
console.log("learningProgressSummary", res); try {
}; setLoading(true);
console.log("Fetching profile overview...");
// 获取班级排名 const res = await getProfileOverview();
const queryRankData = async () => { console.log("Profile overview response:", res);
const res = await getClassRanking();
if (res.success) { if (res.success) {
const studentInfo = { const data = res.data;
myRank: res.data.myRank,
classInfo: res.data.classInfo, // 更新Redux中的学生信息
}; const studentInfo = {
setRankData(res.data); ...data.studentInfo,
dispatch(updateStudentInfo(studentInfo)); myRank: data.ranking.myRank,
classInfo: data.ranking.classInfo,
};
setProfileData(data);
dispatch(updateStudentInfo(studentInfo));
console.log("Profile data set:", data);
}
} catch (error) {
console.error("Failed to fetch profile overview:", error);
} finally {
setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
queryRankData(); queryProfileOverview();
queryLearningProgressSummary();
}, []); }, []);
return ( return (
@@ -43,10 +54,22 @@ const PersonalProfile = () => {
<div className="unified-profile-layout"> <div className="unified-profile-layout">
<div className="unified-profile-left"> <div className="unified-profile-left">
<ProfileCard /> <ProfileCard />
<Rank className="unified-profile-rank" data={rankData} /> <Rank
className="unified-profile-rank"
data={profileData?.ranking ? {
rankings: profileData.ranking.rankings,
myRank: profileData.ranking.myRank,
classInfo: profileData.ranking.classInfo,
} : null}
loading={loading}
/>
</div> </div>
<div className="unified-profile-right"> <div className="unified-profile-right">
<StudyStudes /> <StudyStudes
studyStatistics={profileData?.studyStatistics}
learningProgress={profileData?.learningProgress}
loading={loading}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,15 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import InterviewQuestionsModal from "./components/InterviewQuestionsModal"; import InterviewQuestionsModal from "./components/InterviewQuestionsModal";
import { mockData } from "@/data/mockData"; import { getPageData } from "@/services/resumeInterview";
import "./index.css"; import "./index.css";
const { resumeInterview } = mockData;
const ResumeInterviewPage = () => { const ResumeInterviewPage = () => {
const [activeIndustry, setActiveIndustry] = useState("frontend"); const [activeIndustry, setActiveIndustry] = useState("frontend");
const [resumeModalVisible, setResumeModalVisible] = useState(false); const [resumeModalVisible, setResumeModalVisible] = useState(false);
const [modalData, setModalData] = useState(undefined); const [modalData, setModalData] = useState(undefined);
const [pageData, setPageData] = useState(null);
const [loading, setLoading] = useState(true);
const sectionsRef = useRef({}); const sectionsRef = useRef({});
// 导航到指定行业段落 // 导航到指定行业段落
@@ -32,12 +32,37 @@ const ResumeInterviewPage = () => {
setModalData(undefined); setModalData(undefined);
}; };
// 获取页面数据
useEffect(() => {
const fetchPageData = async () => {
try {
setLoading(true);
const response = await getPageData();
if (response.success) {
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(() => { useEffect(() => {
if (!pageData?.industries) return;
const handleScroll = () => { const handleScroll = () => {
const scrollPosition = window.scrollY + 200; const scrollPosition = window.scrollY + 200;
resumeInterview.industries.forEach((industry) => { pageData.industries.forEach((industry) => {
const section = sectionsRef.current[industry.id]; const section = sectionsRef.current[industry.id];
if (section) { if (section) {
const sectionTop = section.offsetTop; const sectionTop = section.offsetTop;
@@ -52,12 +77,28 @@ const ResumeInterviewPage = () => {
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, [resumeInterview.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"> <ul className="resume-interview-navigation">
{resumeInterview.industries.map((industry) => ( {pageData.industries.map((industry) => (
<li <li
key={industry.id} key={industry.id}
className={`resume-interview-navigation-item ${ className={`resume-interview-navigation-item ${
@@ -70,7 +111,7 @@ const ResumeInterviewPage = () => {
))} ))}
</ul> </ul>
<ul className="resume-interview-content-wrapper"> <ul className="resume-interview-content-wrapper">
{resumeInterview?.industries?.map((item) => ( {pageData.industries.map((item) => (
<li <li
className="resume-interview-content-item-wrapper" className="resume-interview-content-item-wrapper"
key={item.id} key={item.id}
@@ -80,23 +121,26 @@ const ResumeInterviewPage = () => {
<p className="item-subtitle">简历与面试题</p> <p className="item-subtitle">简历与面试题</p>
<div className="item-content-wrapper"> <div className="item-content-wrapper">
<ul className="jobs-list"> <ul className="jobs-list">
{item.positions.map((i) => ( {item.positions.map((position) => (
<li className="job-item job-item-change" key={i.id}> <li className="job-item job-item-change" key={position.id}>
<span>{i.level}</span> <span>{position.level}</span>
<div className="job-name"> <div className="job-name">
<p>{i.name}</p> <p>{position.name}</p>
<span>详情 &gt;</span> <span>详情 &gt;</span>
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
<ul className="resumes-list"> <ul className="resumes-list">
<li {item.questions.map((question) => (
className="resume-item" <li
onClick={() => handleHookQuestionClick(item)} key={question.id}
> className="resume-item"
<p>面试题1122345</p> onClick={() => handleHookQuestionClick({ ...item, question })}
</li> >
<p>{question.question}</p>
</li>
))}
</ul> </ul>
</div> </div>
</li> </li>
@@ -105,6 +149,7 @@ const ResumeInterviewPage = () => {
<InterviewQuestionsModal <InterviewQuestionsModal
visible={resumeModalVisible} visible={resumeModalVisible}
onClose={handleCloseModal} onClose={handleCloseModal}
data={modalData}
/> />
</div> </div>
); );

View File

@@ -1,5 +1,14 @@
import request from "@/utils/request"; import request from "@/utils/request";
// 获取企业内推岗位页面聚合数据
export async function getCompanyJobsPageData(params) {
return request({
url: `/api/company-jobs/page-data`,
method: "GET",
params,
});
}
// 获取企业内推岗位 // 获取企业内推岗位
export async function getJobsList(params) { export async function getJobsList(params) {
return request({ return request({

View File

@@ -6,6 +6,7 @@ import {
} from "./dashboard"; } from "./dashboard";
import { getProjectsList } from "./projectLibrary"; import { getProjectsList } from "./projectLibrary";
import { import {
getCompanyJobsPageData,
getJobsList, getJobsList,
getJobsDetail, getJobsDetail,
getInterviewsList, getInterviewsList,
@@ -18,6 +19,7 @@ import {
getLoginStudentProgress, getLoginStudentProgress,
getClassRank, getClassRank,
getMyRanking, getMyRanking,
getProfileOverview,
} from "./personalProfile"; } from "./personalProfile";
import {} from "./resumeInterview"; import {} from "./resumeInterview";
@@ -35,11 +37,13 @@ export {
getLoginStudentInfo, // 获取当前登录学生基本信息 getLoginStudentInfo, // 获取当前登录学生基本信息
getLoginStudentProgress, // 获取当前学生学习进度 getLoginStudentProgress, // 获取当前学生学习进度
getClassRank, // 获取班级排名(别名) getClassRank, // 获取班级排名(别名)
getProfileOverview, // 获取个人档案完整数据(新接口)
// 项目和作品相关 // 项目和作品相关
getProjectsList, // 获取项目列表 getProjectsList, // 获取项目列表
// 求职相关 // 求职相关
getCompanyJobsPageData, // 获取企业内推岗位页面聚合数据
getJobsList, // 获取岗位列表 getJobsList, // 获取岗位列表
getJobsDetail, // 岗位详情 getJobsDetail, // 岗位详情
getInterviewsList, // 获取面试列表 getInterviewsList, // 获取面试列表

View File

@@ -36,3 +36,12 @@ export async function getMyRanking() {
namespace: "profileLoading", namespace: "profileLoading",
}); });
} }
// 获取个人档案完整数据 (新接口)
export async function getProfileOverview() {
return request({
url: `/api/profile/overview`,
method: "GET",
namespace: "profileLoading",
});
}

View File

@@ -1 +1,33 @@
import request from "@/utils/request"; import request from "@/utils/request";
// Get all page data for resume-interview page
export const getPageData = () => {
return request({
url: "/api/resume-interview",
method: "GET"
});
};
// Export individual functions for future use if needed
export const getResumes = (params) => {
return request({
url: "/api/resumes",
method: "GET",
params
});
};
export const getInterviews = (params) => {
return request({
url: "/api/interviews",
method: "GET",
params
});
};
export const getMyInterviews = () => {
return request({
url: "/api/interviews/my-interviews",
method: "GET"
});
};