refactor(grades,diagnostic): 完成成绩和学情诊断模块审计 P1+P2 改进项

P1-1: 抽取 stats-service.ts,将 8 个统计计算纯函数从 data-access 层分离
P1-5: 创建 WidgetBoundary 组件 + 补齐 teacher 路由 loading.tsx/error.tsx (14 文件)
P1-6: 同步架构图文档 004/005,新增 stats-service 与 widget-boundary 节点
P2-1: 补充 a11y ARIA 属性(5 图表 role=img + aria-label,2 表格 caption,3 列表 role=list,3 按钮 aria-label)
P2-3: 修复班级报告 studentId 字段语义错误(schema 改为可空 + 迁移 + 代码适配)
P2-4: 修复 grade_managed scope 返回空数据(改为子查询 classes 表按 gradeId 过滤)
P2-5: 新增 /parent/diagnostic/ 页面(多子女学情诊断聚合 + loading + error)
P2-6: 统一 SearchParams 工具(student/grades 和 management/grade/insights 改用 @/shared/lib/search-params)
This commit is contained in:
SpecialX
2026-06-22 17:07:32 +08:00
parent e997abaf5e
commit 5f3a1a4662
41 changed files with 9043 additions and 381 deletions

View File

@@ -9,8 +9,8 @@ import { getStudentActiveClassId } from "@/modules/classes/data-access"
import { getUserNamesByIds } from "@/modules/users/data-access"
import { normalize, toNumber } from "./lib/grade-utils"
import { buildRankingTrendPoints, type RankingTrendEntry } from "./stats-service"
import type {
RankingTrendPoint,
RankingTrendResult,
} from "./types"
@@ -56,13 +56,7 @@ export const getRankingTrend = cache(
.where(and(...conditions))
.orderBy(asc(gradeRecords.createdAt))
const byTitle = new Map<
string,
{
date: Date
entries: Array<{ studentId: string; normalized: number }>
}
>()
const byTitle = new Map<string, RankingTrendEntry>()
for (const r of rows) {
const entry = byTitle.get(r.title) ?? { date: r.createdAt, entries: [] }
@@ -73,33 +67,7 @@ export const getRankingTrend = cache(
byTitle.set(r.title, entry)
}
const points: RankingTrendPoint[] = []
for (const [title, entry] of byTitle.entries()) {
if (entry.entries.length === 0) continue
const sorted = [...entry.entries].sort((a, b) => b.normalized - a.normalized)
// Single traversal: find rank and student entry together
let rank = 0
let studentEntry: { studentId: string; normalized: number } | null = null
for (let i = 0; i < sorted.length; i += 1) {
const e = sorted[i]
if (e.studentId === studentId) {
rank = i + 1
studentEntry = e
break
}
}
if (rank <= 0 || !studentEntry) continue
points.push({
title,
date: entry.date.toISOString(),
score: studentEntry.normalized,
rank,
totalStudents: sorted.length,
})
}
points.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
const points = buildRankingTrendPoints(byTitle, studentId)
return {
studentId,