Files
Academic-Affairs-System/web-react/src/pages/DashboardPage.tsx
T
biss 44f09cec1d feat(web): add role-aware React dashboards
Add student and teacher dashboard variants backed by existing APIs, retain the administrative overview for management roles, and provide responsive navigation back to legacy routes that have not migrated yet.
2026-10-01 13:34:23 +08:00

150 lines
6.0 KiB
TypeScript

import { Alert, Button, Progress, Skeleton } from 'antd'
import { ArrowRightOutlined, ReloadOutlined } from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'
import http from '../api/http'
import { useAuthStore } from '../stores/auth'
import StudentDashboardPage from './dashboard/StudentDashboardPage'
import TeacherDashboardPage from './dashboard/TeacherDashboardPage'
import './dashboard/role-dashboard.css'
interface DashboardData {
audience: {
level: string
title: string
scopeName: string
description: string
}
currentTerm?: {
id: string
name: string
startDate: string
endDate: string
}
counts: {
students: number
teachers: number
courses: number
teachingTasks: number
publishedTeachingTasks: number
scheduledTeachingTasks: number
courseEnrollments: number
gradeSheets: number
publishedGradeSheets: number
submittedGradeSheets: number
openCourseSelectionRounds: number
}
pending: Record<string, number>
greeting: {
title: string
subtitle: string
narrative: string
}
}
const number = new Intl.NumberFormat('zh-CN')
function percent(value: number, total: number) {
return total > 0 ? Math.min(100, Math.round(value / total * 100)) : 0
}
function formatDate(value?: string) {
if (!value) return '待设置'
const date = new Date(`${value.slice(0, 10)}T00:00:00`)
return new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric' }).format(date)
}
function AdminDashboardPage() {
const query = useQuery({
queryKey: ['dashboard'],
queryFn: async () => (await http.get<DashboardData>('/dashboard')).data,
})
if (query.isLoading) return <div className="page-container"><Skeleton active paragraph={{ rows: 10 }} /></div>
if (query.isError || !query.data) {
return (
<div className="page-container">
<Alert
type="error"
showIcon
message="教务总览暂时无法加载"
description="请确认后端服务已经启动,然后重新加载。"
action={<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>重新加载</Button>}
/>
</div>
)
}
const data = query.data
const metrics = [
{ label: '在籍学生', value: data.counts.students, hint: `${data.audience.scopeName}当前有效学籍` },
{ label: '教师档案', value: data.counts.teachers, hint: '当前数据范围内的教师' },
{ label: '本学期教学班', value: data.counts.teachingTasks, hint: `${data.counts.publishedTeachingTasks} 个已发布` },
{ label: '有效选课', value: data.counts.courseEnrollments, hint: `${data.counts.openCourseSelectionRounds} 个批次开放中` },
]
const readiness = [
{ label: '任务发布', value: data.counts.publishedTeachingTasks, total: data.counts.teachingTasks },
{ label: '课表覆盖', value: data.counts.scheduledTeachingTasks, total: data.counts.publishedTeachingTasks },
{ label: '成绩发布', value: data.counts.publishedGradeSheets, total: data.counts.gradeSheets },
]
const pending = Object.entries(data.pending).filter(([, value]) => value > 0)
return (
<main className="page-container">
<div className="migration-banner"><b>React 迁移预览</b><span>当前页面直接读取现有 `/api/dashboard`,业务数据未使用模拟值。</span></div>
<header className="page-heading">
<div>
<span className="page-heading__kicker">{data.audience.title}</span>
<h1>{data.greeting.title}</h1>
<p>{data.greeting.narrative || data.audience.description}</p>
</div>
<div className="term-status">
<span>ACADEMIC TERM</span>
<strong>{data.currentTerm?.name ?? '尚未设置当前学期'} · {formatDate(data.currentTerm?.startDate)}—{formatDate(data.currentTerm?.endDate)}</strong>
</div>
</header>
<section className="metric-grid" aria-label="教务指标">
{metrics.map((metric) => (
<article className="metric-card" key={metric.label}>
<span>{metric.label}</span><strong>{number.format(metric.value)}</strong><small>{metric.hint}</small>
</article>
))}
</section>
<section className="dashboard-grid">
<article className="content-panel">
<header className="content-panel__header"><h2>教学运行准备度</h2><Button type="link" icon={<ArrowRightOutlined />}>查看明细</Button></header>
<div className="content-panel__body readiness-list">
{readiness.map((item) => {
const value = percent(item.value, item.total)
return (
<div className="readiness-row" key={item.label}>
<span>{item.label}</span><Progress percent={value} strokeColor="#087f73" trailColor="#e9edf2" showInfo={false} /><strong>{value}%</strong>
</div>
)
})}
</div>
</article>
<article className="content-panel">
<header className="content-panel__header"><h2>等待处理</h2><span>{pending.reduce((sum, [, value]) => sum + value, 0)} 项</span></header>
<div className="content-panel__body pending-list">
{pending.length === 0 ? <Alert type="success" showIcon message="当前没有待处理事项" /> : pending.slice(0, 6).map(([key, value]) => (
<div className="pending-item" key={key}><span>{key}</span><strong>{value}</strong></div>
))}
</div>
</article>
</section>
</main>
)
}
export default function DashboardPage() {
const roles = useAuthStore((state) => state.user?.roles ?? [])
const isStudent = roles.includes('Student') &&
!roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(role))
const isTeacher = roles.includes('Teacher') &&
!roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'].includes(role))
if (isStudent) return <StudentDashboardPage />
if (isTeacher) return <TeacherDashboardPage />
return <AdminDashboardPage />
}