- 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
216 lines
14 KiB
Markdown
216 lines
14 KiB
Markdown
# 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
|
||
- **问题**:`<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` 接受 `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` 包裹子组件于 `<Suspense>`,但数据已在页面级解析并作为 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
|
||
- **问题**:`<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` 在用户对象上 mock `permissions`(行 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:hidden` div,一次在 `hidden lg:block` div。两个实例都在服务端渲染并发送到客户端,使此区块 HTML 负载翻倍。
|
||
- **修复**:渲染一次并用 CSS grid/flexbox 重排序实现响应式布局,或接受此重复为较小代价。
|
||
|
||
---
|
||
|
||
## 修复顺序
|
||
|
||
1. **P0-1**(ContentRow 标签)— 直接面向用户的数据 bug
|
||
2. **P0-2**(admin/error.tsx i18n)— 直接 i18n 回归
|
||
3. **P0-3 + P1-3 + P2-3**(空趋势数据 + 图表标签 + 空状态)— 一起修复
|
||
4. **P1-1、P1-2**(缺失 loading.tsx/error.tsx)— 一致性
|
||
5. **P1-4**(日期 locale)— 系统性 i18n 修复
|
||
6. **P1-5、P1-6、P1-7**(死代码)— 快速清理
|
||
7. **P1-8、P1-9**(类型安全)— 重构 `filterTodaySchedule`
|
||
8. **P1-10**(重复文件)— 抽取共享组件
|
||
9. **P2-2、P2-4、P2-6、P2-8**(增量改进)
|