完整的教务系统前端项目 - 包含所有修复和9月份数据

This commit is contained in:
KQL
2025-09-03 13:26:13 +08:00
commit 87b06d3176
270 changed files with 116169 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
.module-calendar-task-wrapper {
width: 100%;
height: 393px;
border-radius: 16px;
border-bottom: 1px solid #fff;
flex-shrink: 0;
box-sizing: border-box;
padding: 20px;
position: relative;
z-index: 1;
&::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 160px;
height: 160px;
background-image: url("@/assets/images/Dashboard/CalendarTaskModule/calendar_task_bg.png");
background-size: 100% 100%;
z-index: -1;
}
.arco-calendar {
background-color: transparent;
}
.arco-calendar-header {
height: 50px;
display: flex;
align-items: center;
.arco-calendar-header-icon {
font-size: 18px;
}
.arco-calendar-header-value {
font-size: 18px;
}
}
.arco-calendar-week-list {
height: 50px;
}
.arco-calendar-cell {
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) !important;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
}
}
&.is-current-day {
.date-number {
background-color: rgba(10, 216, 239, 0.696);
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

@@ -0,0 +1,73 @@
import dayjs from "dayjs";
import { Calendar } from "@arco-design/web-react";
import "./index.css";
const CalendarTaskModule = ({ tasks = [], selectedDate, onDateChange }) => {
// 格式化今天的日期
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 isCurrentDay = dateStr === dayjs(selectedDate).format("YYYY-MM-DD");
const isToday = dateStr === formattedToday;
return (
<div
className={`calendar-date-cell ${hasTasks ? "has-tasks" : ""} ${
isCurrentDay ? "is-current-day" : ""
} ${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);
}
}
}
};
return (
<div className="module-calendar-task-wrapper">
<Calendar
panelWidth="300"
panel
defaultValue={formattedToday}
value={
selectedDate
? selectedDate.toISOString().split("T")[0]
: formattedToday
}
style={{ fontSize: "18px" }}
onChange={handleDateChange}
dateRender={dateRender}
/>
</div>
);
};
export default CalendarTaskModule;

View File

@@ -0,0 +1,4 @@
.progress-chart {
width: 214px;
height: 214px;
}

View File

@@ -0,0 +1,100 @@
import { useEffect, useRef } from "react";
import * as echarts from "echarts";
import "./index.css";
const EchartsProgress = ({
percent = 60,
strokeWidth = 20,
progressColor = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "#0275F2" },
{ offset: 1, color: "#389CFA" },
]), // 进度条颜色,默认为渐变色
backgroundColor = "#0275F21A", // 背景颜色
}) => {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
// 初始化图表
if (chartRef.current && !chartInstance.current) {
chartInstance.current = echarts.init(chartRef.current);
}
// 配置图表选项 - 得分环
const option = {
series: [
{
type: "gauge",
startAngle: -90,
endAngle: 270,
pointer: {
show: false,
},
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false,
itemStyle: {
borderWidth: 0,
color: progressColor, // 设置进度条颜色
},
},
axisLine: {
lineStyle: {
width: strokeWidth,
color: [
[1, backgroundColor], // 背景颜色
],
},
},
splitLine: {
show: false,
distance: 0,
length: 10,
},
axisTick: {
show: false,
},
axisLabel: {
show: false,
distance: 50,
},
data: [
{
value: percent,
detail: {
valueAnimation: true,
offsetCenter: ["0%", "0%"],
},
},
],
detail: {
width: 50,
height: 14,
fontSize: 34,
color: "#262626",
formatter: "{value}%",
},
},
],
};
// 设置图表选项
chartInstance.current.setOption(option);
// 响应窗口大小变化
const resizeHandler = () => {
chartInstance.current.resize();
};
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("resize", resizeHandler);
};
}, [backgroundColor, percent, progressColor, strokeWidth]);
return <div ref={chartRef} className="progress-chart" />;
};
export default EchartsProgress;

View File

@@ -0,0 +1,330 @@
/* 个人数据展示区样式 */
.personal-data-display {
background: var(--card-bg);
border-radius: 8px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
padding: 20px;
grid-column: span 3; /* 占满整行 */
margin-bottom: 16px;
}
.personal-data-display .module-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 12px;
}
.semester-indicator {
font-size: 12px;
color: var(--text-muted);
background: #f3f4f6;
padding: 4px 8px;
border-radius: 4px;
}
/* 左右分栏布局 */
.data-display-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* 通用面板样式 */
.left-panel, .right-panel {
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.left-panel:hover, .right-panel:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
transform: translateY(-2px);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.panel-header h4 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.click-hint {
font-size: 11px;
color: var(--text-muted);
opacity: 0;
transition: opacity 0.3s ease;
}
.left-panel:hover .click-hint,
.right-panel:hover .click-hint {
opacity: 1;
}
/* 左侧面板:可视化容器 */
.visualization-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.progress-section {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.progress-circle {
width: 80px;
height: 80px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.progress-text {
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
}
.progress-label {
font-size: 12px;
color: var(--text-secondary);
margin: 0;
}
/* 成绩趋势图 */
.trend-section h5 {
font-size: 12px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 12px 0;
}
.mini-chart {
display: flex;
justify-content: space-between;
align-items: end;
height: 60px;
gap: 8px;
}
.trend-point {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.trend-bar {
width: 12px;
background: linear-gradient(to top, var(--primary-color), #60a5fa);
border-radius: 2px 2px 0 0;
margin-bottom: 4px;
transition: all 0.3s ease;
}
.trend-label {
font-size: 10px;
color: var(--text-muted);
}
/* 知识点掌握度 */
.knowledge-section h5 {
font-size: 12px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 12px 0;
}
.knowledge-bars {
display: flex;
flex-direction: column;
gap: 8px;
}
.knowledge-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.knowledge-name {
color: var(--text-secondary);
width: 60px;
flex-shrink: 0;
}
.knowledge-bar {
flex: 1;
height: 6px;
background: #f3f4f6;
border-radius: 3px;
overflow: hidden;
}
.knowledge-fill {
height: 100%;
background: linear-gradient(90deg, #10b981, #34d399);
border-radius: 3px;
transition: width 0.5s ease;
}
.knowledge-value {
color: var(--text-primary);
font-weight: 500;
width: 30px;
text-align: right;
}
/* 右侧面板:排名表格 */
.ranking-table-container {
position: relative;
}
.ranking-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.ranking-table th {
background: #f8fafc;
color: var(--text-secondary);
font-weight: 500;
padding: 8px 4px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.ranking-table td {
padding: 8px 4px;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.ranking-table tbody tr:hover {
background: #f8fafc;
}
.rank-number {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
background: #f3f4f6;
color: var(--text-secondary);
}
.rank-number.gold {
background: #fbbf24;
color: white;
}
.rank-number.silver {
background: #d1d5db;
color: white;
}
.rank-number.bronze {
background: #cd7c2f;
color: white;
}
.student-name {
color: var(--text-primary);
font-weight: 500;
}
.student-score {
color: var(--text-primary);
font-weight: 600;
}
.subject-score {
color: var(--text-secondary);
}
.trend-cell {
text-align: center;
}
.trend-indicator {
font-size: 10px;
font-weight: 500;
padding: 2px 4px;
border-radius: 2px;
}
.trend-indicator.up {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
.trend-indicator.down {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.more-indicator {
text-align: center;
margin-top: 12px;
font-size: 11px;
color: var(--primary-color);
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.data-display-layout {
grid-template-columns: 1fr;
gap: 16px;
}
.personal-data-display {
grid-column: span 2;
}
}
@media (max-width: 768px) {
.personal-data-display {
grid-column: span 1;
}
.visualization-container {
gap: 16px;
}
.mini-chart {
height: 50px;
}
.ranking-table th,
.ranking-table td {
padding: 6px 2px;
}
}

View File

@@ -0,0 +1,222 @@
import { useNavigate } from "react-router-dom";
import { mockData } from "@/data/mockData";
import "./index.css";
const PersonalDataDisplay = () => {
const navigate = useNavigate();
const { studyProgress, classRanking } = mockData;
// 点击跳转到个人档案,并传递上下文状态
const handleNavigateToProfile = (sourceType) => {
const contextState = {
semester: "2024春季学期",
subject: "all",
sourceModule: sourceType, // 'learning' 或 'ranking'
};
navigate("/profile", {
state: contextState,
});
};
// 计算环形进度条的参数
const radius = 35;
const circumference = 2 * Math.PI * radius;
const strokeDasharray = circumference;
const strokeDashoffset =
circumference - (studyProgress.percentage / 100) * circumference;
// 模拟多维度学习数据
const learningMetrics = {
gradesTrend: [
{ month: "9月", score: 78 },
{ month: "10月", score: 82 },
{ month: "11月", score: 85 },
{ month: "12月", score: 88 },
{ month: "1月", score: 92 },
],
knowledgePoints: [
{ subject: "数据结构", mastery: 85 },
{ subject: "算法设计", mastery: 78 },
{ subject: "数据库", mastery: 92 },
{ subject: "网络编程", mastery: 76 },
],
taskCompletion: 85.6,
};
// 扩展班级排名数据
const extendedRankingData = classRanking.map((student) => ({
...student,
subjects: {
math: Math.floor(Math.random() * 20) + 80,
programming: Math.floor(Math.random() * 20) + 80,
database: Math.floor(Math.random() * 20) + 80,
},
trend: Math.random() > 0.5 ? "up" : "down",
trendValue: Math.floor(Math.random() * 5) + 1,
}));
return (
<div className="personal-data-display">
<div className="module-header">
<h3 className="module-title">个人数据展示区</h3>
<div className="semester-indicator">2024春季学期</div>
</div>
<div className="data-display-layout">
{/* 左侧面板:学习数据可视化 */}
<div
className="left-panel"
onClick={() => handleNavigateToProfile("learning")}
>
<div className="panel-header">
<h4>学习数据分析</h4>
<span className="click-hint">点击查看详情</span>
</div>
<div className="visualization-container">
{/* 圆环进度图 */}
<div className="progress-section">
<div className="progress-circle">
<svg width="80" height="80">
<circle
cx="40"
cy="40"
r={radius}
fill="none"
stroke="#f3f4f6"
strokeWidth="6"
/>
<circle
cx="40"
cy="40"
r={radius}
fill="none"
stroke="var(--primary-color)"
strokeWidth="6"
strokeLinecap="round"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
style={{
transform: "rotate(-90deg)",
transformOrigin: "40px 40px",
transition: "stroke-dashoffset 0.5s ease",
}}
/>
</svg>
<div className="progress-text">{studyProgress.percentage}%</div>
</div>
<p className="progress-label">总体完成率</p>
</div>
{/* 成绩趋势折线图(简化版) */}
<div className="trend-section">
<h5>成绩趋势</h5>
<div className="mini-chart">
{learningMetrics.gradesTrend.map((item, index) => (
<div key={index} className="trend-point">
<div
className="trend-bar"
style={{ height: `${(item.score - 70) * 2}px` }}
/>
<span className="trend-label">{item.month}</span>
</div>
))}
</div>
</div>
{/* 知识点掌握度 */}
<div className="knowledge-section">
<h5>知识点掌握度</h5>
<div className="knowledge-bars">
{learningMetrics.knowledgePoints
.slice(0, 3)
.map((point, index) => (
<div key={index} className="knowledge-item">
<span className="knowledge-name">{point.subject}</span>
<div className="knowledge-bar">
<div
className="knowledge-fill"
style={{ width: `${point.mastery}%` }}
/>
</div>
<span className="knowledge-value">{point.mastery}%</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* 右侧面板:班级排名数据表格 */}
<div
className="right-panel"
onClick={() => handleNavigateToProfile("ranking")}
>
<div className="panel-header">
<h4>班级排名</h4>
<span className="click-hint">点击查看详情</span>
</div>
<div className="ranking-table-container">
<table className="ranking-table">
<thead>
<tr>
<th>排名</th>
<th>姓名</th>
<th>综合分</th>
<th>数学</th>
<th>编程</th>
<th>变化</th>
</tr>
</thead>
<tbody>
{extendedRankingData.slice(0, 5).map((student) => (
<tr key={student.id}>
<td>
<div
className={`rank-number ${
student.rank === 1
? "gold"
: student.rank === 2
? "silver"
: student.rank === 3
? "bronze"
: ""
}`}
>
{student.rank <= 3
? student.rank === 1
? "🥇"
: student.rank === 2
? "🥈"
: "🥉"
: student.rank}
</div>
</td>
<td className="student-name">{student.name}</td>
<td className="student-score">{student.score}</td>
<td className="subject-score">{student.subjects.math}</td>
<td className="subject-score">
{student.subjects.programming}
</td>
<td className="trend-cell">
<span className={`trend-indicator ${student.trend}`}>
{student.trend === "up" ? "↗" : "↘"}{" "}
{student.trendValue}
</span>
</td>
</tr>
))}
</tbody>
</table>
<div className="more-indicator">查看完整排名 </div>
</div>
</div>
</div>
</div>
);
};
export default PersonalDataDisplay;

View File

@@ -0,0 +1,76 @@
.module-quick-access-wrapper {
width: 100%;
height: 181px;
background-color: #fff;
border-radius: 16px;
border: 1px solid #fff;
flex-shrink: 0;
box-sizing: border-box;
padding: 20px;
margin-top: 20px;
background-image: linear-gradient(0deg, #e5f1ff, #f1f9ff);
position: relative;
&::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-image: url("@/assets/images/Dashboard/QuickAccess/bg.png");
background-size: 100% 100%;
}
.module-quick-access-title {
height: 27px;
width: 100%;
font-size: 20px;
font-weight: 700;
line-height: 27px;
color: #1d2129;
margin-bottom: 20px;
display: flex;
align-items: center;
.title-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
}
.module-quick-access-list {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.module-quick-access-item {
width: 172px;
height: 102px;
background-color: #fafafa;
border-radius: 16px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
background-image: url("@/assets/images/Dashboard/QuickAccess/item_bg.png");
background-size: 100% 100%;
z-index: 1;
.module-quick-access-item-icon {
width: 48px;
height: 48px;
}
.module-quick-access-item-text {
font-size: 16px;
font-weight: 400;
color: #1d2129;
margin-top: 8px;
}
}
}
}

View File

@@ -0,0 +1,55 @@
import { useNavigate } from "react-router-dom";
import IconFont from "@/components/IconFont";
import "./index.css";
const QuickAccessArray = [
{
name: "我的简历与面试题",
src: "recuV81M2OeGcv",
route: "/resume-interview",
},
{ name: "日历", src: "recuV83P0hfWEO", route: "/calendar" },
{
name: "个人档案",
src: "recuV81JPXTspn",
route: "/profile",
},
{
name: "课后作业",
src: "recuV81LmPLGOM",
route: "/homework",
},
];
const QuickAccess = () => {
const navigate = useNavigate();
const handleClick = (path) => {
navigate(path);
};
return (
<div className="module-quick-access-wrapper">
<p className="module-quick-access-title">
<IconFont className="title-icon" src="recuUY5mia5pYp" />
<span>快捷入口</span>
</p>
<ul className="module-quick-access-list">
{QuickAccessArray.map((item, index) => (
<li
key={index}
className="module-quick-access-item"
onClick={() => handleClick(item.route)}
>
<IconFont
className="module-quick-access-item-icon"
src={item.src}
/>
<p className="module-quick-access-item-text">{item.name}</p>
</li>
))}
</ul>
</div>
);
};
export default QuickAccess;

View File

@@ -0,0 +1,149 @@
.start-class-wrapper {
width: 100%;
height: 325px;
border-radius: 16px;
border: 1px solid #fff;
flex-shrink: 0;
background-position: 50% 30%;
box-sizing: border-box;
padding: 10px 20px;
background-color: #e2edff;
position: relative;
overflow: hidden;
&::after {
content: "";
width: 242px;
height: 100px;
position: absolute;
right: 0;
top: 0;
background-image: url("@/assets/images/Dashboard/StartClass/start_class_bg.png");
background-size: 100% 100%;
}
.start-class-title {
width: 100%;
height: 27px;
font-size: 20px;
font-weight: 700;
line-height: 27px;
color: #1d2129;
position: relative;
box-sizing: border-box;
margin-bottom: 10px;
display: flex;
align-items: center;
.title-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
}
.start-class-content {
width: 100%;
height: 246px;
overflow-y: hidden;
display: flex;
justify-content: space-between;
align-items: center;
.start-class-item-left {
width: 357px;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
> li {
width: 100%;
height: 117px;
border-radius: 12px;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
background-color: #fff;
box-sizing: border-box;
padding: 15px;
z-index: 1;
}
}
.start-class-item-right {
width: 357px;
height: 100%;
> li {
width: 100%;
height: 100%;
border-radius: 12px;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
background-color: #fff;
box-sizing: border-box;
padding: 15px;
z-index: 1;
}
}
.start-class-item {
.start-class-item-title {
width: 100%;
height: 25px;
line-height: 25px;
font-size: 18px;
font-weight: 700;
color: #1d2129;
text-align: left;
box-sizing: border-box;
position: relative;
margin-bottom: 10px;
display: flex;
align-items: center;
.title-icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
> .num {
margin-left: 10px;
width: 18px;
height: 18px;
text-align: center;
line-height: 15px;
box-sizing: border-box;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #0077ff;
background-color: rgba(0, 119, 255, 0.1);
border: 1px solid rgba(0, 119, 255, 0.2);
}
}
.start-class-item-list {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
overflow-y: auto;
.start-class-item-list-item {
width: 100%;
color: #86909c;
font-size: 14px;
text-align: left;
margin-top: 5px;
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
import { Skeleton } from "@arco-design/web-react";
import IconFont from "@/components/IconFont";
import "./index.css";
const StartClass = ({ courses, tasks, loading }) => {
if (loading) {
return (
<div className="start-class-wrapper">
<p className="start-class-title">开始上课</p>
<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 (
<div className="start-class-wrapper">
<p className="start-class-title">
<IconFont className="title-icon" src="recuUY5ua0CLHi" />
<span>开始上课</span>
</p>
<ul className="start-class-content">
<div className="start-class-item-left">
<li className="start-class-item">
<p className="start-class-item-title">
<IconFont className="title-icon" src="recuUY5km8qdgo" />
<span>下次上课</span>
<span className="num">{courses?.nextCourse?.length || 0}</span>
</p>
<ul className="start-class-item-list">
{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>
</li>
<li className="start-class-item">
<p className="start-class-item-title">
<IconFont className="title-icon" src="recuUY5jNLbEPE" />
<span>待办事项</span>
<span className="num">{pendingTasks.length || 0}</span>
</p>
<ul className="start-class-item-list">
{pendingTasks.length > 0 ? (
pendingTasks.map((task) => (
<li key={task.id} className="start-class-item-list-item">
{task.courseName}{task.title}
</li>
))
) : (
<li className="start-class-item-list-item">暂无待办事项</li>
)}
</ul>
</li>
</div>
<div className="start-class-item-right">
<li className="start-class-item">
<p className="start-class-item-title">
<IconFont className="title-icon" src="recuUY5rdMHBHn" />
<span>最近课程</span>
<span className="num">{courses?.recentCourses?.length || 0}</span>
</p>
<ul className="start-class-item-list">
{courses?.recentCourses?.length > 0 ? (
courses?.recentCourses?.map((course, index) => (
<li key={index} className="start-class-item-list-item">
{formatCourseDate(course.date)}{course.courseName}
</li>
))
) : (
<li className="start-class-item-list-item">暂无最近课程记录</li>
)}
</ul>
</li>
</div>
</ul>
</div>
);
};
export default StartClass;

View File

@@ -0,0 +1,88 @@
.module-study-status-wrapper {
width: 373px;
height: 420px;
border-radius: 16px;
border: 1px solid #fff;
overflow: hidden;
flex-shrink: 0;
background-size: 150% 120%;
background-position: 50% 30%;
box-sizing: border-box;
padding: 20px;
background-color: #e8f6ff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
&::after {
content: "";
width: 180px;
height: 110px;
position: absolute;
right: 0;
top: 0;
background-image: url("@/assets/images/Dashboard/StudyStatus/study-status_bg.png");
background-size: 100% 100%;
}
.module-study-status-title {
height: 30px;
width: 100%;
font-size: 20px;
font-weight: 500;
line-height: 30px;
color: #262626;
margin-bottom: 20px;
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
.title-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
}
.module-study-status-progress {
width: 214px;
height: 214px;
margin: 30px;
background-color: antiquewhite;
}
.progress-chart {
width: 214px;
height: 214px;
}
.progress-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
.progress-text {
text-align: center;
font-size: 14px;
font-weight: 400;
line-height: 20px;
margin-top: 20px;
color: #616065;
position: relative;
&::after {
content: "";
position: absolute;
top: 50%;
left: -20px;
transform: translateY(-50%);
width: 10px;
height: 10px;
border-radius: 50%;
border: 3px solid #0275f2;
}
}
}
}

View File

@@ -0,0 +1,19 @@
import EchartsProgress from "@/components/EchartsProgress";
import IconFont from "@/components/IconFont";
import "./index.css";
const StudyStatus = ({ progress = 0 }) => {
return (
<div className="module-study-status-wrapper">
<p className="module-study-status-title">
<IconFont className="title-icon" src="recuUY5mDawm4e" />
<span>学习情况</span>
</p>
<div className="progress-container">
<EchartsProgress percent={progress || 0} strokeWidth={20} />
</div>
</div>
);
};
export default StudyStatus;

View File

@@ -0,0 +1,218 @@
.module-tasks-wrapper {
width: 100%;
background-color: #fff;
border-radius: 16px;
border: 1px solid #fff;
flex-shrink: 0;
box-sizing: border-box;
padding: 20px;
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
&::after {
content: "";
position: absolute;
right: 0;
top: 0;
width: 127px;
height: 95px;
background-image: url("@/assets/images/Dashboard/TaskList/task_list_bg.png");
background-size: 100% 100%;
}
.module-tasks-title {
height: 30px;
width: 100%;
font-size: 20px;
font-weight: 500;
display: flex;
align-items: center;
.title-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
}
.no-tasks {
margin-top: 130px;
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;
}
}
.module-tasks-list {
margin-top: 20px;
width: 100%;
height: 452px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
overflow-y: auto;
.module-tasks-item {
width: 100%;
height: 100px;
flex-shrink: 0;
margin-bottom: 10px;
.module-tasks-item-info {
width: 100%;
height: 32px;
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
.module-tasks-item-info-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
}
.module-tasks-item-info-teacher-name {
position: absolute;
left: 40px;
font-size: 14px;
font-weight: 400;
color: #616065;
}
.module-tasks-item-info-time {
position: absolute;
right: 0px;
font-size: 12px;
font-weight: 400;
color: #bfbfbf;
}
}
.module-tasks-item-content {
width: 100%;
height: 64px;
margin-top: 5px;
margin-left: 16px;
box-sizing: border-box;
padding-left: 16px;
position: relative;
border-left: 1px dashed #bfbfbf;
display: flex;
justify-content: flex-end;
align-items: center;
&::before {
content: "";
position: absolute;
top: 0;
left: -2px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #bfbfbf;
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: -2px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #bfbfbf;
}
.module-tasks-item-content-info {
width: 100%;
height: 58px;
border-radius: 8px;
background-color: #fafafa;
padding: 10px;
box-sizing: border-box;
position: relative;
> p {
font-size: 10px;
color: #616065;
font-weight: 400;
text-align: left;
margin-bottom: 5px;
}
> div {
position: relative;
font-size: 12px;
color: #262626;
font-weight: 400;
text-align: left;
.module-tasks-item-content-info-duration {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
font-weight: 400;
color: #bfbfbf;
&::after {
content: "";
position: absolute;
top: 50%;
left: -15px;
transform: translateY(-50%);
width: 12px;
height: 12px;
background-image: url("@/assets/images/TaskList/frame.png");
background-size: 100% 100%;
}
}
}
}
}
.module-tasks-item-content-last {
border: none;
&::after,
&::before {
display: none;
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
import { Avatar, Skeleton, Empty } from "@arco-design/web-react";
import IconFont from "@/components/IconFont";
import "./index.css";
const TaskList = ({ tasks = [], loading }) => {
if (loading) {
return (
<div className="module-tasks-wrapper">
<p className="module-tasks-title">事项</p>
<Skeleton loading={true} />
</div>
);
}
const getTaskTypeText = (type) => {
const typeMap = {
HOMEWORK: "作业",
PROJECT: "项目",
REPORT: "报告",
INTERVIEW: "面试",
OTHER: "其他",
course: "课程",
exam: "考试"
};
return typeMap[type] || type;
};
return (
<div className="module-tasks-wrapper">
<p className="module-tasks-title">
<IconFont className="title-icon" src="recuUY5obMePNQ" />
<span>当日事项</span>
</p>
{tasks.length === 0 ? (
<div className="no-tasks">
<Empty description="该日无事项" />
</div>
) : (
<ul className="module-tasks-list">
{tasks.map((item, index) => (
<li key={item.id} className="module-tasks-item">
<div className="module-tasks-item-info">
<Avatar className="module-tasks-item-info-avatar" size="small">
{item?.teacherAvatar ? (
<img alt="avatar" src={item.teacherAvatar} />
) : (
item?.teacherName?.charAt(0) || "T"
)}
</Avatar>
<span className="module-tasks-item-info-teacher-name">
{item?.teacherName || "未知教师"}
</span>
</div>
<div
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>
<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>
</li>
))}
</ul>
)}
</div>
);
};
export default TaskList;

View File

@@ -0,0 +1,64 @@
/* Dashboard页面专用变量定义 - 不与其他文件共享 */
.dashboard,
.dashboard * {
--primary-color: #3b82f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--card-bg: #ffffff;
--border-color: #e5e7eb;
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
/* Dashboard页面样式 */
.dashboard {
width: 100%;
animation: fadeIn 0.3s ease-in-out;
.dashboard-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
box-sizing: border-box;
padding: 20px;
.dashboard-left-content {
width: 766px;
height: 966px;
flex-shrink: 0;
.status-rank-wrapper {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
.class-rank-wrapper {
height: 420px;
}
}
}
.dashboard-right-content {
flex: 1;
height: 966px;
margin-left: 20px;
background-color: #ffffff;
border-radius: 16px;
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100px;
background-image: url("@/assets/images/Dashboard/right_content_bg.png");
background-size: 100% 100%;
}
}
}
}

View File

@@ -0,0 +1,102 @@
import { useState, useEffect } from "react";
import StartClass from "./components/StartClass";
import QuickAccess from "./components/QuickAccess";
import CalendarTaskModule from "./components/CalendarTaskModule";
import StudyStatus from "./components/StudyStatus";
import ClassRank from "@/components/ClassRank";
import StageProgress from "@/components/StageProgress";
import TaskList from "./components/TaskList";
import { getDashboardStatistics } from "@/services";
import "./index.css";
const Dashboard = () => {
const [dashboardData, setDashboardData] = useState(null);
const [loading, setLoading] = useState(true);
const [selectedDate, setSelectedDate] = useState(new Date());
useEffect(() => {
fetchDashboardData();
}, []);
// 获取仪表板完整数据
const fetchDashboardData = async () => {
try {
setLoading(true);
const response = await getDashboardStatistics();
if (response && response.success) {
setDashboardData(response.data);
} else if (response) {
// 兼容直接返回数据的情况
setDashboardData(response);
}
} catch (error) {
console.error("Failed to fetch dashboard data:", error);
} finally {
setLoading(false);
}
};
// 根据选中日期筛选任务
const getTasksForDate = (date) => {
if (!dashboardData?.tasks?.allTasks) return [];
// Check if date is valid before calling getTime
if (!date || isNaN(date.getTime())) {
console.warn("Invalid date provided to getTasksForDate:", date);
return [];
}
// 使用本地日期格式,避免时区问题
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
console.log("Looking for tasks on date:", dateStr);
const tasks = dashboardData.tasks.allTasks.filter((task) => task.date === dateStr);
console.log("Found tasks:", tasks);
return tasks;
};
return (
<div className="dashboard">
<StageProgress showBlockageAlert={true} />
<div className="dashboard-wrapper">
<div className="dashboard-left-content">
<StartClass
courses={dashboardData?.courses}
tasks={dashboardData?.tasks}
loading={loading}
/>
<QuickAccess />
<div className="status-rank-wrapper">
<StudyStatus
progress={dashboardData?.overview?.overallProgress}
loading={loading}
/>
<ClassRank
className="class-rank-wrapper"
data={
dashboardData?.ranking
? {
rankings: dashboardData.ranking.topStudents,
}
: null
}
/>
</div>
</div>
<div className="dashboard-right-content">
<CalendarTaskModule
tasks={dashboardData?.tasks?.allTasks}
selectedDate={selectedDate}
onDateChange={setSelectedDate}
/>
<TaskList tasks={getTasksForDate(selectedDate)} loading={loading} />
</div>
</div>
</div>
);
};
export default Dashboard;