## P1 功能(20 项) - 站内消息系统、家长仪表盘、学生考勤管理 - Excel 导入导出、用户批量导入、成绩导出 - 排课规则+自动排课+课表调整 - 成绩趋势+对比分析、密码安全策略、速率限制 - 数据变更日志、文件预览+存储策略、全文检索 - 依赖审计集成 CI、数据库定时备份、E2E 测试完善 - 通知偏好管理 ## 基础设施修复 - src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求) - .env: MySQL 端口从 13002 切换至 14013 - scripts/create-db.ts: 新增数据库初始化脚本 ## 架构文档同步 - 004_architecture_impact_map.md 和 005_architecture_data.json 完整记录所有新增表、模块、路由、权限、依赖关系
138 lines
3.7 KiB
TypeScript
138 lines
3.7 KiB
TypeScript
"use client"
|
|
|
|
import { BarChart3 } from "lucide-react"
|
|
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
|
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/shared/components/ui/card"
|
|
import {
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
} from "@/shared/components/ui/chart"
|
|
import { EmptyState } from "@/shared/components/ui/empty-state"
|
|
import { formatDate } from "@/shared/lib/utils"
|
|
import type { GradeTrendResult } from "@/modules/grades/types"
|
|
|
|
const chartConfig = {
|
|
normalizedScore: {
|
|
label: "Score (%)",
|
|
color: "hsl(var(--primary))",
|
|
},
|
|
}
|
|
|
|
interface GradeTrendChartProps {
|
|
data: GradeTrendResult | null
|
|
}
|
|
|
|
export function GradeTrendChart({ data }: GradeTrendChartProps) {
|
|
if (!data || data.points.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<BarChart3 className="h-4 w-4" />
|
|
Grade Trend
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Score progression over time (normalized to 0-100).
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<EmptyState
|
|
icon={BarChart3}
|
|
title="No trend data"
|
|
description="Select a class and subject to view the grade trend."
|
|
className="border-none h-60"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const chartData = data.points.map((p) => ({
|
|
title: p.title,
|
|
normalizedScore: p.normalizedScore,
|
|
fullTitle: p.title,
|
|
date: formatDate(p.date),
|
|
rawScore: p.score,
|
|
fullScore: p.fullScore,
|
|
type: p.type,
|
|
}))
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<BarChart3 className="h-4 w-4" />
|
|
Grade Trend
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{data.label} · avg {data.averageScore.toFixed(1)}%
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
|
<LineChart
|
|
data={chartData}
|
|
margin={{ left: 8, right: 8, top: 8, bottom: 8 }}
|
|
>
|
|
<CartesianGrid
|
|
vertical={false}
|
|
strokeDasharray="4 4"
|
|
strokeOpacity={0.4}
|
|
/>
|
|
<XAxis
|
|
dataKey="title"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
tickFormatter={(value: string) =>
|
|
value.length > 10 ? `${value.slice(0, 10)}...` : value
|
|
}
|
|
/>
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value: number) => `${value}%`}
|
|
width={36}
|
|
/>
|
|
<ChartTooltip
|
|
cursor={{
|
|
stroke: "hsl(var(--muted-foreground))",
|
|
strokeWidth: 1,
|
|
strokeDasharray: "4 4",
|
|
}}
|
|
content={
|
|
<ChartTooltipContent
|
|
indicator="line"
|
|
labelKey="fullTitle"
|
|
className="w-[220px]"
|
|
/>
|
|
}
|
|
/>
|
|
<Line
|
|
dataKey="normalizedScore"
|
|
type="monotone"
|
|
stroke="var(--color-normalizedScore)"
|
|
strokeWidth={2}
|
|
dot={{
|
|
fill: "var(--color-normalizedScore)",
|
|
r: 3,
|
|
strokeWidth: 2,
|
|
}}
|
|
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
/>
|
|
</LineChart>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|