- Update architecture impact map, data, feature checklist, gap audit - Add audit reports for dashboard, exam-homework, grades-diagnostic, settings-profile, textbooks - Update bug reports (admin, teacher, lesson-preparation, others, shared) - Update coding standards, DR plan, design docs, and README
14 KiB
Dashboard 模块 V3 审计报告
审计日期:2026-06-22
审计范围:src/modules/dashboard/ + 所有 dashboard 路由文件
前置审计:v1(P0 修复:跨模块 DB 查询、权限、i18n 容器组件)、v2(10 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签)
概览
v1/v2 审计解决了表层问题。v3 审计发现了更深层次的问题,涉及数据完整性、i18n 完整性、死代码、类型安全、流式架构和测试缺口。最严重的是 admin dashboard 中 ContentRow 标签与值完全错配的 P0 数据展示 bug。
| 严重度 | 数量 |
|---|---|
| P0 | 3 |
| P1 | 10 |
| P2 | 9 |
P0 问题(严重)
P0-1:Admin Dashboard ContentRow 标签与值错配(数据完整性)
- 文件:
src/modules/dashboard/components/admin-dashboard/admin-dashboard.tsx - 行号:166-169(Content 区块)、180-181(Homework Activity 区块)
- 问题:"Content" 区块显示教材/章节/题目/考试数量,但使用了用户/班级/待批改/已发布作业的标签。图标正确(Library, BookOpen, FileText, ClipboardList),但标签错误:
- 行 166:
label={t("stats.users")}+value={data.textbookCount}→ 应为t("stats.textbooks") - 行 167:
label={t("stats.classes")}+value={data.chapterCount}→ 应为t("stats.chapters") - 行 168:
label={t("stats.toGrade")}+value={data.questionCount}→ 应为t("stats.questions") - 行 169:
label={t("stats.homeworkPublished")}+value={data.examCount}→ 应为t("stats.exams") - 行 180:
label={t("stats.activeAssignments")}+value={data.homeworkAssignmentCount}→ 标签说"active"但值是总数 - 行 181:
label={t("stats.submissionRate")}+value={data.homeworkSubmissionCount}→ 标签说"rate"(百分比)但值是原始计数
- 行 166:
- 修复:使用与值匹配的正确翻译键。新增缺失键(
stats.textbooks、stats.chapters、stats.questions、stats.exams、stats.totalAssignments、stats.totalSubmissions)到messages/{zh-CN,en}/dashboard.json。
P0-2:admin/error.tsx 硬编码中文,无 i18n
- 文件:
src/app/(dashboard)/admin/error.tsx - 行号:12-14
- 问题:此错误边界有硬编码中文字符串(
"页面加载失败"、"抱歉,页面加载时发生了意外错误。请稍后重试。"、"重试"),未导入或使用useTranslations。英文用户会看到中文文本。v2 审计遗漏了此文件,因为只关注了dashboard/模块而非admin/路由错误边界。其他 dashboard error.tsx(teacher、parent、root)都正确使用了useTranslations。 - 修复:导入
useTranslations,替换硬编码字符串为t("error.loadFailed")、t("error.loadFailedDesc")、t("error.retry")。
P0-3:userGrowth 和 homeworkTrend 永远返回空数组
- 文件:
src/modules/dashboard/data-access.ts - 行号:46-47
- 问题:
getAdminDashboardData硬编码userGrowth: []和homeworkTrend: []。UserGrowthChart组件(admin-dashboard.tsx 行 123、133)渲染这些空数组,产生永久空图表且无空状态。架构图(行 973)标注为"待后续接入真实统计",但至今未修复。用户看到两个空白图表区域,有标题但无数据也无说明。 - 修复:为
UserGrowthChart添加空状态(当data.length === 0时显示"暂无数据"),与其他图表组件的空状态保持一致。
P1 问题(高)
P1-1:admin/dashboard 路由缺失 loading.tsx
- 文件(缺失):
src/app/(dashboard)/admin/dashboard/loading.tsx - 问题:admin dashboard 路由无路由级
loading.tsx,回退到admin/loading.tsx(通用骨架屏,不匹配 admin dashboard 布局)。Teacher、student、parent 都有 dashboard 专属loading.tsx。 - 修复:创建
admin/dashboard/loading.tsx,骨架屏匹配AdminDashboardView布局。
P1-2:admin/dashboard 和 student/dashboard 路由缺失 error.tsx
- 文件(缺失):
src/app/(dashboard)/admin/dashboard/error.tsx、src/app/(dashboard)/student/dashboard/error.tsx - 问题:这些路由无路由级错误边界。Admin 回退到
admin/error.tsx(有硬编码中文 — 见 P0-2)。Student 回退到student/error.tsx。Teacher 和 parent 都有 dashboard 专属error.tsx(含 i18n + 重试按钮)。 - 修复:为两个路由创建 dashboard 专属
error.tsx,使用useTranslations和reset()。
P1-3:UserGrowthChart 硬编码标签用于两个图表
- 文件:
src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx - 行号:44
- 问题:
name属性硬编码为t("chart.newUsers")。此组件在admin-dashboard.tsx中被复用于用户增长(行 123)和作业提交趋势(行 133)。作业趋势图错误地显示"新用户"作为图例/提示标签。 - 修复:为
UserGrowthChart添加labelKey或nameprop,让调用方指定正确标签。
P1-4:formatDate / formatLongDate 总是使用 zh-CN locale
-
文件:
src/shared/lib/utils.ts(行 8、35),及所有不传 locale 的 dashboard 组件 -
问题:
formatDate和formatLongDate默认locale = "zh-CN"。所有 dashboard 组件调用时未传用户 locale:dashboard-greeting-header.tsx行 22admin-dashboard.tsx行 215teacher-homework-card.tsx行 69recent-submissions.tsx行 96student-grades-card.tsx行 23、105student-upcoming-assignments-card.tsx行 106
英文用户看到中文格式日期(如"2026年6月22日 周一"而非"Monday, June 22, 2026")。
-
修复:客户端组件用
useLocale()(next-intl),服务端组件用getLocale()(next-intl/server),传入formatDate/formatLongDate。
P1-5:死代码 — getCachedAdminDashboard 从未使用
- 文件:
src/modules/dashboard/actions.ts - 行号:146
- 问题:
export const getCachedAdminDashboard = cache(getAdminDashboardAction)定义但从未被导入或调用。data-access.ts中的getAdminDashboardData已用cache()包裹。此外,用 Reactcache()包裹调用requirePermission()的 Server Action 语义上不正确。 - 修复:删除行 146 及未使用的
cache导入。
P1-6:死代码 — AvatarImage src={undefined}
- 文件:
src/modules/dashboard/components/teacher-dashboard/recent-submissions.tsx - 行号:76
- 问题:
<AvatarImage src={undefined} alt={item.studentName} />总是传undefined作为src,AvatarImage永远不会渲染实际图片,总是回退到AvatarFallback。 - 修复:移除
AvatarImage行,仅保留AvatarFallback。
P1-7:死 prop — TeacherStats isLoading 从未传入
- 文件:
src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx - 行号:10、18、32、41、50、59
- 问题:
TeacherStats接受isLoadingprop(默认false)并传给所有 4 个StatCard。但TeacherStats仅在DashboardSection中渲染(teacher-dashboard-view.tsx行 53),未传isLoading。prop 永远为false。StudentStatsGrid无此 prop,造成不一致。 - 修复:移除
TeacherStats的isLoadingprop 及StatCard调用。
P1-8:dashboard-utils.ts 中的 as 类型断言违反项目规则
-
文件:
src/modules/dashboard/lib/dashboard-utils.ts -
行号:114、145
-
问题:项目规则明确"禁止
as断言"(除unknown转换或测试外)。两处违规:- 行 114:
})) as StudentTodayScheduleItem[] | TeacherTodayScheduleItem[] - 行 145:
) as TeacherTodayScheduleItem[]
根因是
filterTodaySchedule重载服务于学生和教师课表,但返回类型是联合类型。 - 行 114:
-
修复:将
filterTodaySchedule改为泛型函数,或拆分为两个函数。
P1-9:辅助函数缺失显式返回类型
- 文件:
teacher-schedule.tsx行 24:const getStatus = (start: string, end: string) => {student-upcoming-assignments-card.tsx行 30:const getDueUrgency = (dueAt: string | null) => {
- 问题:项目规则要求"函数返回值必须显式标注"。
- 修复:添加显式返回类型。
P1-10:重复的 loading.tsx 和 error.tsx 文件
- 文件:
src/app/(dashboard)/dashboard/loading.tsx和src/app/(dashboard)/teacher/dashboard/loading.tsx— 字节级完全相同src/app/(dashboard)/dashboard/error.tsx、teacher/dashboard/error.tsx、parent/dashboard/error.tsx— 全部相同
- 问题:这些文件是精确副本。任何修复必须应用到所有副本,容易产生漂移。
- 修复:抽取共享
DashboardLoadingSkeleton和DashboardErrorFallback组件到src/modules/dashboard/components/,每个路由的loading.tsx/error.tsx渲染共享组件。
P2 问题(中)
P2-1:流式/Suspense 未生效 — 数据在页面级获取
- 文件:所有
page.tsx(admin/teacher/student/parent dashboard) - 问题:所有页面用
export const dynamic = "force-dynamic"和await getDashboardAction()在渲染任何子组件前获取所有数据。DashboardSection包裹子组件于<Suspense>,但数据已在页面级解析并作为 props 传入,Suspense 永远不会在初始渲染时触发。 - 修复:将数据获取移入各卡片组件(使其成为异步服务端组件自行获取数据),或传入未解析的 promise 并用 React
use()hook。这是较大的架构变更。
P2-2:4 个组件不必要标记为 "use client"
- 文件:
dashboard-greeting-header.tsx— 仅用useTranslations、formatLongDate、getGreetingKeyteacher-quick-actions.tsx— 仅用useTranslations、Link、Buttonteacher-dashboard-header.tsx— 包裹上述两个student-dashboard-header.tsx— 包裹DashboardGreetingHeader
- 问题:这些组件标记为
"use client"但不含客户端 only hook(useState、useEffect、事件处理器等)。useTranslations在服务端组件中可用。转为服务端组件(用getTranslations替代useTranslations)可减少客户端包大小。 - 修复:移除
"use client",改useTranslations为getTranslations(async),组件改为async function。
P2-3:UserGrowthChart 无空状态
- 文件:
src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx - 问题:当
data为空(当前永远如此 — 见 P0-3),recharts 渲染空图表有坐标轴但无线条无说明。其他图表组件(TeacherGradeTrends、StudentGradesCard)使用ChartCardShell有正确空状态。 - 修复:添加空状态检查:
data.length === 0时渲染EmptyState。
P2-4:Student dashboard 空状态缺少 CTA(与 teacher 不一致)
- 文件:
student-today-schedule-card.tsx行 58-63:EmptyState无actionstudent-upcoming-assignments-card.tsx行 59-64:EmptyState无action
- 问题:Teacher dashboard 空状态都含 CTA。Student dashboard 空状态无 CTA,用户无明确下一步。
- 修复:为 student 空状态添加
actionprop。
P2-5:StudentTodayScheduleCard 过时数据 — useMemo 不随时间更新
- 文件:
src/modules/dashboard/components/student-dashboard/student-today-schedule-card.tsx - 行号:25-43
- 问题:
useMemo(() => { ... }, [items])基于new Date()计算currentId和nextId。依赖数组是[items],仅在items变化时重新计算。用户保持页面打开时,"进行中"和"下一个"徽章会过时。 - 修复:添加基于时间的重渲染机制(如
useEffect+setInterval每分钟更新nowstate)。
P2-6:仅图标按钮缺少 aria-label
- 文件:
src/modules/dashboard/components/teacher-dashboard/teacher-homework-card.tsx - 行号:22
- 问题:
<Button asChild size="icon" variant="ghost" className="h-8 w-8" title={...}>用title作 tooltip 但无aria-label。屏幕阅读器可能不播报按钮用途。 - 修复:添加
aria-label={t("quickActions.createNewAssignment")}。
P2-7:无组件测试 — 仅有纯函数测试
- 文件:
tests/integration/dashboard/dashboard-utils.test.ts(408 行,31 个测试覆盖 6 个纯函数)、tests/integration/dashboard/dashboard-routing.test.ts(6 个测试覆盖重定向逻辑) - 问题:v2 添加了纯函数单测,但零组件测试、零 Server Action 测试、零 data-access 测试、零错误边界测试。
dashboard-routing.test.ts在用户对象上 mockpermissions(行 41),但实际代码用resolvePermissions(roles)— mock 的permissions字段被忽略,测试设置有误导性。 - 修复:添加组件测试(RTL)、Action 测试(mock data-access,验证权限调用)、修复路由测试。
P2-8:TeacherTodoCard 排序逻辑晦涩
- 文件:
src/modules/dashboard/components/teacher-dashboard/teacher-todo-card.tsx - 行号:52
- 问题:
.sort((a, b) => (a.variant === "urgent" ? -1 : 1) - (b.variant === "urgent" ? -1 : 1))难以阅读。布尔转数字的算术不透明。 - 修复:重写为更可读的比较函数。
P2-9:TeacherSchedule 渲染两次(移动端 + 桌面端)— 重复服务端渲染
- 文件:
src/modules/dashboard/components/teacher-dashboard/teacher-dashboard-view.tsx - 行号:63-67(移动端)、85-89(桌面端)
- 问题:
TeacherSchedule(异步服务端组件调用getTranslations)在 React 树中渲染两次 — 一次在lg:hiddendiv,一次在hidden lg:blockdiv。两个实例都在服务端渲染并发送到客户端,使此区块 HTML 负载翻倍。 - 修复:渲染一次并用 CSS grid/flexbox 重排序实现响应式布局,或接受此重复为较小代价。
修复顺序
- P0-1(ContentRow 标签)— 直接面向用户的数据 bug
- P0-2(admin/error.tsx i18n)— 直接 i18n 回归
- P0-3 + P1-3 + P2-3(空趋势数据 + 图表标签 + 空状态)— 一起修复
- P1-1、P1-2(缺失 loading.tsx/error.tsx)— 一致性
- P1-4(日期 locale)— 系统性 i18n 修复
- P1-5、P1-6、P1-7(死代码)— 快速清理
- P1-8、P1-9(类型安全)— 重构
filterTodaySchedule - P1-10(重复文件)— 抽取共享组件
- P2-2、P2-4、P2-6、P2-8(增量改进)