# 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"(百分比)但值是原始计数
- **修复**:使用与值匹配的正确翻译键。新增缺失键(`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` 或 `name` prop,让调用方指定正确标签。
### 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` 行 22
- `admin-dashboard.tsx` 行 215
- `teacher-homework-card.tsx` 行 69
- `recent-submissions.tsx` 行 96
- `student-grades-card.tsx` 行 23、105
- `student-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()` 包裹。此外,用 React `cache()` 包裹调用 `requirePermission()` 的 Server Action 语义上不正确。
- **修复**:删除行 146 及未使用的 `cache` 导入。
### P1-6:死代码 — AvatarImage src={undefined}
- **文件**:`src/modules/dashboard/components/teacher-dashboard/recent-submissions.tsx`
- **行号**:76
- **问题**:`` 总是传 `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` 接受 `isLoading` prop(默认 `false`)并传给所有 4 个 `StatCard`。但 `TeacherStats` 仅在 `DashboardSection` 中渲染(`teacher-dashboard-view.tsx` 行 53),未传 `isLoading`。prop 永远为 `false`。`StudentStatsGrid` 无此 prop,造成不一致。
- **修复**:移除 `TeacherStats` 的 `isLoading` prop 及 `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` 重载服务于学生和教师课表,但返回类型是联合类型。
- **修复**:将 `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` 包裹子组件于 ``,但数据已在页面级解析并作为 props 传入,Suspense 永远不会在初始渲染时触发。
- **修复**:将数据获取移入各卡片组件(使其成为异步服务端组件自行获取数据),或传入未解析的 promise 并用 React `use()` hook。这是较大的架构变更。
### P2-2:4 个组件不必要标记为 "use client"
- **文件**:
- `dashboard-greeting-header.tsx` — 仅用 `useTranslations`、`formatLongDate`、`getGreetingKey`
- `teacher-quick-actions.tsx` — 仅用 `useTranslations`、`Link`、`Button`
- `teacher-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` 无 `action`
- `student-upcoming-assignments-card.tsx` 行 59-64:`EmptyState` 无 `action`
- **问题**:Teacher dashboard 空状态都含 CTA。Student dashboard 空状态无 CTA,用户无明确下一步。
- **修复**:为 student 空状态添加 `action` prop。
### 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` 每分钟更新 `now` state)。
### P2-6:仅图标按钮缺少 aria-label
- **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-homework-card.tsx`
- **行号**:22
- **问题**:`