Files
Academic-Affairs-System/web-react/src/pages/DashboardPage.tsx
T

134 lines
5.3 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'
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)
}
export default function DashboardPage() {
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>
)
}