## 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 完整记录所有新增表、模块、路由、权限、依赖关系
133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
"use client"
|
|
|
|
import { BarChart3 } from "lucide-react"
|
|
import {
|
|
Bar,
|
|
BarChart,
|
|
CartesianGrid,
|
|
Legend,
|
|
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 { ClassComparisonItem } from "@/modules/grades/types"
|
|
|
|
const chartConfig = {
|
|
averageScore: { label: "Average (%)", color: "hsl(var(--primary))" },
|
|
passRate: { label: "Pass Rate (%)", color: "hsl(var(--chart-2))" },
|
|
excellentRate: { label: "Excellent (%)", color: "hsl(var(--chart-3))" },
|
|
}
|
|
|
|
interface ClassComparisonChartProps {
|
|
data: ClassComparisonItem[]
|
|
}
|
|
|
|
export function ClassComparisonChart({ data }: ClassComparisonChartProps) {
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<BarChart3 className="h-4 w-4" />
|
|
Class Comparison
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Compare average, pass rate, and excellent rate across classes.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<EmptyState
|
|
icon={BarChart3}
|
|
title="No comparison data"
|
|
description="Select a grade and subject to compare classes."
|
|
className="border-none h-60"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const chartData = data.map((d) => ({
|
|
name: d.className,
|
|
averageScore: d.averageScore,
|
|
passRate: d.passRate,
|
|
excellentRate: d.excellentRate,
|
|
count: d.count,
|
|
studentCount: d.studentCount,
|
|
}))
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<BarChart3 className="h-4 w-4" />
|
|
Class Comparison
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Average score, pass rate (≥60%), and excellent rate (≥85%) per class.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ChartContainer config={chartConfig} className="h-[300px] 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="name"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
tickFormatter={(value: string) =>
|
|
value.length > 8 ? `${value.slice(0, 8)}...` : value
|
|
}
|
|
/>
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value: number) => `${value}%`}
|
|
width={36}
|
|
/>
|
|
<ChartTooltip content={<ChartTooltipContent className="w-[240px]" />} />
|
|
<Legend />
|
|
<Bar
|
|
dataKey="averageScore"
|
|
fill="var(--color-averageScore)"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
<Bar
|
|
dataKey="passRate"
|
|
fill="var(--color-passRate)"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
<Bar
|
|
dataKey="excellentRate"
|
|
fill="var(--color-excellentRate)"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
</BarChart>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|