# 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 - **问题**:`