feat: 完成 P1 全部功能 + 修复 proxy 导出 + 切换 MySQL 端口至 14013

## 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
  完整记录所有新增表、模块、路由、权限、依赖关系
This commit is contained in:
SpecialX
2026-06-17 13:44:37 +08:00
parent 125f7ec54c
commit 3b6272c99d
195 changed files with 27274 additions and 416 deletions

View File

@@ -0,0 +1,138 @@
"use client"
import { PieChart as PieChartIcon } from "lucide-react"
import { Bar, BarChart, CartesianGrid, Cell, 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 type { GradeDistributionResult } from "@/modules/grades/types"
const BUCKET_COLORS: Record<string, string> = {
"90-100": "hsl(142, 71%, 45%)",
"80-89": "hsl(217, 91%, 60%)",
"70-79": "hsl(43, 96%, 56%)",
"60-69": "hsl(25, 95%, 53%)",
"<60": "hsl(0, 84%, 60%)",
}
const chartConfig = {
count: { label: "Students", color: "hsl(var(--primary))" },
}
interface GradeDistributionChartProps {
data: GradeDistributionResult | null
}
export function GradeDistributionChart({ data }: GradeDistributionChartProps) {
if (!data || data.totalCount === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChartIcon className="h-4 w-4" />
Score Distribution
</CardTitle>
<CardDescription>
Number of students in each score range (normalized to 0-100).
</CardDescription>
</CardHeader>
<CardContent>
<EmptyState
icon={PieChartIcon}
title="No distribution data"
description="Select a class and subject to view score distribution."
className="border-none h-60"
/>
</CardContent>
</Card>
)
}
const chartData = data.buckets.map((b) => ({
label: b.label,
count: b.count,
percentage:
data.totalCount > 0
? Math.round((b.count / data.totalCount) * 1000) / 10
: 0,
}))
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChartIcon className="h-4 w-4" />
Score Distribution
</CardTitle>
<CardDescription>
{data.totalCount} grade record{data.totalCount === 1 ? "" : "s"} across
score ranges.
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-[280px] w-full">
<BarChart
data={chartData}
margin={{ left: 8, right: 8, top: 8, bottom: 8 }}
>
<CartesianGrid
vertical={false}
strokeDasharray="4 4"
strokeOpacity={0.4}
/>
<XAxis
dataKey="label"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis
allowDecimals={false}
tickLine={false}
axisLine={false}
width={32}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[200px]"
formatter={(payload: unknown) => {
const item = (payload as { payload?: (typeof chartData)[number] })?.payload
if (!item) return null
return (
<div className="flex w-full flex-col gap-0.5">
<span className="text-sm font-medium">
{item.label}: {item.count} student
{item.count === 1 ? "" : "s"}
</span>
<span className="text-xs text-muted-foreground">
{item.percentage}% of total
</span>
</div>
)
}}
/>
}
/>
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
{chartData.map((entry) => (
<Cell key={entry.label} fill={BUCKET_COLORS[entry.label]} />
))}
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
)
}