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:
27
src/app/(dashboard)/parent/diagnostic/error.tsx
Normal file
27
src/app/(dashboard)/parent/diagnostic/error.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { AlertCircle } from "lucide-react"
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function ParentDiagnosticError({
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
|
||||
<EmptyState
|
||||
icon={AlertCircle}
|
||||
title="子女学情诊断加载失败"
|
||||
description="抱歉,加载子女诊断数据时发生了意外错误。请稍后重试。"
|
||||
action={{
|
||||
label: "重试",
|
||||
onClick: () => reset(),
|
||||
}}
|
||||
className="border-none shadow-none h-auto"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
32
src/app/(dashboard)/parent/diagnostic/loading.tsx
Normal file
32
src/app/(dashboard)/parent/diagnostic/loading.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-8 p-6 md:p-8">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Skeleton className="h-72 w-full" />
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{Array.from({ length: 2 }).map((_, j) => (
|
||||
<Skeleton key={j} className="h-40 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
70
src/app/(dashboard)/parent/diagnostic/page.tsx
Normal file
70
src/app/(dashboard)/parent/diagnostic/page.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Stethoscope } from "lucide-react"
|
||||
|
||||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getStudentMasterySummary } from "@/modules/diagnostic/data-access"
|
||||
import { getDiagnosticReports } from "@/modules/diagnostic/data-access-reports"
|
||||
import { StudentDiagnosticView } from "@/modules/diagnostic/components/student-diagnostic-view"
|
||||
import {
|
||||
ParentChildrenDataPage,
|
||||
ParentNoChildrenPage,
|
||||
} from "@/modules/parent/components/parent-children-data-page"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ParentDiagnosticPage() {
|
||||
const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ)
|
||||
|
||||
if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) {
|
||||
return (
|
||||
<ParentNoChildrenPage
|
||||
title="Children Diagnostic"
|
||||
description="View your children's knowledge point mastery and diagnostic reports."
|
||||
icon={Stethoscope}
|
||||
emptyTitle="No children linked"
|
||||
emptyDescription="Your account is not linked to any student accounts yet. Please contact the school administrator."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 使用 allSettled 容错:单个子女查询失败不影响其他子女展示
|
||||
const results = await Promise.allSettled(
|
||||
ctx.dataScope.childrenIds.map(async (id) => {
|
||||
const [summary, reports] = await Promise.all([
|
||||
getStudentMasterySummary(id),
|
||||
getDiagnosticReports({ studentId: id }),
|
||||
])
|
||||
return { summary, reports, studentId: id }
|
||||
}),
|
||||
)
|
||||
const validItems = results
|
||||
.filter(
|
||||
(r): r is PromiseFulfilledResult<{
|
||||
summary: Awaited<ReturnType<typeof getStudentMasterySummary>>
|
||||
reports: Awaited<ReturnType<typeof getDiagnosticReports>>
|
||||
studentId: string
|
||||
}> => r.status === "fulfilled",
|
||||
)
|
||||
.map((r) => r.value)
|
||||
|
||||
return (
|
||||
<ParentChildrenDataPage
|
||||
title="Children Diagnostic"
|
||||
description="View knowledge point mastery and diagnostic reports for all your children."
|
||||
icon={Stethoscope}
|
||||
noRecordsTitle="No diagnostic data"
|
||||
noRecordsDescription="Your children don't have any diagnostic data yet."
|
||||
items={validItems}
|
||||
renderItem={({ summary, reports }) => (
|
||||
<>
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{summary?.studentName ?? "Unknown student"}
|
||||
</h3>
|
||||
</div>
|
||||
<StudentDiagnosticView summary={summary} reports={reports} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user