# Dashboard 模块 V4 审计报告 **审计日期**:2026-06-22 **审计范围**:`src/modules/dashboard/` + 所有 dashboard 路由文件 + parent dashboard 组件 **前置审计**: - v1(P0 修复:跨模块 DB 查询、权限、i18n 容器组件) - v2(10 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签) - v3(ContentRow 标签错配、admin/error.tsx i18n、空趋势数据空状态、loading/error.tsx 补齐、日期 locale、死代码清理、`as` 断言修复、流式架构 React `use()`) --- ## 一、现有实现概要 ### 1.1 文件分布 仪表盘模块位于 `src/modules/dashboard/`,包含 29 个文件: | 层 | 文件 | 行数 | 职责 | |----|------|------|------| | actions | `actions.ts` | 167 | 4 个 Server Action(admin/teacher/student/parent),均调用 `requirePermission()` | | data-access | `data-access.ts` | 49 | admin 仪表盘数据聚合(并行调用 6 个模块 stats 函数) | | streams | `streams.ts` | 34 | admin 流式数据源(返回未解析 Promise 供 React `use()` 消费) | | types | `types.ts` | 74 | AdminDashboardData / StudentDashboardProps / TeacherDashboardData | | lib | `lib/dashboard-utils.ts` | 198 | 6 个纯函数(weekday / 统计 / 排序 / 指标计算 / 问候语) | | components | `dashboard-section.tsx` | 170 | Error Boundary + Suspense + 骨架屏(5 种变体) | | components | `dashboard-greeting-header.tsx` | 36 | 共享问候头部 | | components | `dashboard-error-fallback.tsx` | 30 | 路由级错误回退 | | components | `dashboard-loading-skeleton.tsx` | 44 | 路由级加载骨架 | | admin-dashboard | `admin-dashboard.tsx` | 173 | 管理员视图(流式架构) | | admin-dashboard | `admin-sections.tsx` | 231 | 管理员 6 个分区组件 | | admin-dashboard | `user-growth-chart.tsx` | 65 | recharts 折线图 | | teacher-dashboard | 9 文件 | ~700 | 教师仪表盘组件 | | student-dashboard | 6 文件 | ~530 | 学生仪表盘组件 | | tests | `dashboard-section.test.tsx` | 70 | Error Boundary + 骨架屏单测 | | tests | `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数 31 个单测 | ### 1.2 数据流 ``` [Page] → [Action] → [requirePermission] → [data-access / 其他模块 data-access] ↓ [lib/dashboard-utils 纯函数计算] ↓ [View 组件] → [DashboardSection Suspense] ``` ### 1.3 架构图记录完整性 架构影响地图(004/005)已覆盖 dashboard 模块的: - 4 个 Server Action 签名、依赖、使用方 - 6 个纯函数签名和用途 - 依赖矩阵(dependsOn: shared/auth/homework/classes) - 路由权限映射(dashboardRoutePermissions) - 组件清单和行数 **遗漏**:架构图未记录 `streams.ts` 的流式数据源函数,也未记录 parent dashboard 组件实际位于 `modules/parent/components/` 的跨模块布局。 --- ## 二、现存问题与原因分析 ### P0 问题(严重) #### P0-1:`filterTodaySchedule` 仍使用 `as T[]` 类型断言 - **文件**:`src/modules/dashboard/lib/dashboard-utils.ts` - **行号**:120 - **问题**:v3 审计(P1-8)已识别此问题并改为泛型函数,但实现仍保留 `as T[]` 断言: ```typescript return schedule .filter(...) .sort(...) .map((s) => ({ ... })) as T[] // ← 违反"禁止 as 断言" ``` - **违反规则**:项目规则「TypeScript 严格模式:禁止 `as` 断言(除类型收窄外)」 - **后果**:类型系统被绕过,`map` 返回的对象结构若与 `T` 不匹配,编译器不会报错,潜在运行时错误 - **修复方向**:移除 `as T[]`,让 `map` 返回类型自然推导;或将映射逻辑提取为泛型映射函数 ### P1 问题(高) #### P1-1:组件内嵌纯函数未抽取到 lib - **文件**: - `teacher-schedule.tsx` 行 24-36:`getStatus(start, end)` 计算课程状态 - `student-upcoming-assignments-card.tsx` 行 18:`timeToMinutes(t)` - `student-upcoming-assignments-card.tsx` 行 30-40:`getDueUrgency(dueAt)` - `student-upcoming-assignments-card.tsx` 行 18-28:`getActionLabelKey(status)` / `getActionVariant(status)` - `student-today-schedule-card.tsx` 行 17-20:`timeToMinutes(t)` - **问题**:5 个纯函数散落在 3 个组件文件中,无法被单测覆盖,且 `timeToMinutes` 在两处重复定义 - **违反规则**:项目规则「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离」 - **后果**:单测覆盖率无法提升;`timeToMinutes` 重复定义易产生不一致 - **修复方向**:全部迁移到 `lib/dashboard-utils.ts`,导出供组件调用,补充单测 #### P1-2:`teacherName` 硬编码英文 fallback - **文件**:`src/modules/dashboard/actions.ts` - **行号**:85 - **问题**:`teacherName: teacherProfile?.name ?? "Teacher"` — 当教师名称为空时 fallback 为硬编码英文 "Teacher",英文/中文用户都会看到英文 - **违反规则**:项目规则「所有用户可见文本必须适配 i18n」 - **后果**:中文用户在教师名称缺失时看到英文 "Teacher",i18n 不一致 - **修复方向**:fallback 改为空字符串 `""`,由前端组件用 `t("title.teacher")` 处理空值 #### P1-3:`teacher-schedule.tsx` 本地重复定义类型 - **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-schedule.tsx` - **行号**:10-18 - **问题**:本地定义 `TeacherTodayScheduleItem` 类型,与 `types.ts` 中的同名类型结构完全相同,重复定义 - **违反规则**:项目规则「避免代码重复」 - **后果**:类型变更需同步两处,易产生不一致 - **修复方向**:从 `types.ts` 导入,删除本地定义 #### P1-4:parent dashboard 组件位于 parent 模块而非 dashboard 模块 - **文件**:`src/modules/parent/components/parent-dashboard.tsx` - **问题**:`ParentDashboard` 组件位于 parent 模块,但由 `dashboard/actions.getParentDashboardAction` 提供数据,且 `parent/dashboard/page.tsx` 同时导入两个模块的组件。架构图标注为"架构决策:保留在 parent 模块以避免移动文件破坏其他 import",但这造成模块边界模糊 - **违反规则**:项目规则「该模块必须作为独立功能单元」 - **后果**:dashboard 模块不完整,parent 仪表盘的 UI 逻辑分散在两个模块 - **修复方向**:将 `ParentDashboard` 组件迁移到 `modules/dashboard/components/parent-dashboard/`,parent 模块仅保留数据访问 ### P2 问题(中) #### P2-1:无数据服务接口抽象 - **文件**:`src/modules/dashboard/actions.ts`、`data-access.ts` - **问题**:dashboard 模块直接 import 其他 6 个模块的 data-access 函数(classes/homework/users/parent/textbooks/questions/exams),无 TypeScript 接口抽象。组件层无法 mock 数据依赖,单测必须 mock 整个模块 - **违反规则**:项目规则「完全解耦:通过定义 TypeScript 接口抽象数据依赖」 - **后果**:模块耦合度高,难以独立测试,新增角色需修改 actions.ts - **修复方向**:定义 `DashboardService` 接口,为每个角色提供实现类,通过 React Context 注入 #### P2-2:无配置驱动的 Widget 渲染 - **文件**:`admin-dashboard.tsx`、`teacher-dashboard-view.tsx`、`student-dashboard-view.tsx` - **问题**:每个角色的仪表盘视图硬编码渲染哪些 Widget(如 admin 渲染 StatsBar + QuickActions + TrendCharts + 3 Cards + RecentUsersTable)。新增角色或调整 Widget 需修改视图组件代码 - **违反规则**:项目规则「可扩展性:采用配置驱动设计」 - **后果**:扩展性差,4 个角色视图代码结构相似但无法复用 - **修复方向**:定义 `DashboardWidgetConfig` 类型,通过配置决定渲染哪些 Widget 及其布局 #### P2-3:无监控埋点接口 - **文件**:整个模块 - **问题**:无任何用户行为埋点(如 Widget 点击、页面停留、空状态触发等),无法度量仪表盘使用情况 - **违反规则**:项目规则「监控:方案中预留关键操作埋点接口」 - **后果**:无法度量仪表盘使用情况,无法指导优化 - **修复方向**:定义 `DashboardAnalytics` 接口,在关键交互点调用(Widget 点击、空状态触发、错误重试) #### P2-4:admin dashboard `userGrowth` 和 `homeworkTrend` 仍为占位空数组 - **文件**:`src/modules/dashboard/data-access.ts` - **行号**:46-47 - **问题**:v3 已为 `UserGrowthChart` 添加空状态,但数据源仍硬编码 `userGrowth: []` 和 `homeworkTrend: []`,趋势图表永远显示空状态 - **违反规则**:无直接违反,但影响用户体验 - **后果**:管理员无法看到用户增长和作业提交趋势 - **修复方向**:实现真实统计查询,或在 data-access 层添加 TODO 注释标记后续实现 #### P2-5:4 个角色 StatCard 使用模式不一致 - **文件**: - `admin-sections.tsx`:`StatCard` 直接传 `value`(number) - `teacher-stats.tsx`:`StatCard` 传 `value={String(count)}` + `color` + `highlight` - `student-stats-grid.tsx`:`StatCard` 传 `value={String(count)}` + `color` + `valueClassName` + 条件颜色 - **问题**:3 个角色的 StatCard 调用模式不一致,admin 不传 color,teacher 传 color,student 传 color + valueClassName - **违反规则**:项目规则「最大化复用:识别四个角色共用的 UI 块」 - **后果**:视觉不一致,维护成本高 - **修复方向**:统一 StatCard 调用模式,通过配置驱动颜色和样式 ### P3 问题(低) #### P3-1:无完整键盘导航支持 - **问题**:虽有 `aria-label` 属性,但 Widget 之间无 `tabindex` 管理,键盘用户无法按逻辑顺序遍历 Widget - **修复方向**:为 Widget 容器添加 `role="region"` + `aria-label`,管理 `tabindex` #### P3-2:`AdminTrendCharts` 硬编码 `data={[]}` - **文件**:`admin-sections.tsx` 行 144、152 - **问题**:`UserGrowthChart` 调用时传 `data={[]}`,与 P2-4 相关 - **修复方向**:从 `streams` 获取真实趋势数据 #### P3-3:`teacher-todo-card.tsx` 排序逻辑仍可优化 - **文件**:`teacher-todo-card.tsx` 行 52-56 - **问题**:v3 已优化排序逻辑,但仍使用 `if (a.variant === "urgent") return -1` 模式,可进一步用优先级映射 - **修复方向**:定义 `VARIANT_PRIORITY` 映射,用数值比较 --- ## 三、行业差距对比 ### 3.1 与优秀 K12 产品的差距 | 维度 | 我们当前 | 钉钉教育/智学网/ClassIn | 差距影响 | |------|---------|----------------------|---------| | **数据联动** | 各 Widget 独立展示,无联动 | 点击统计卡片可下钻到详情页 | 管理员无法快速从概览定位问题 | | **个性化配置** | 固定布局,用户无法自定义 | 支持拖拽 Widget、隐藏/显示 | 不同用户关注点不同,固定布局降低效率 | | **实时更新** | 静态数据,需刷新页面 | WebSocket 实时推送待办数 | 待办数不实时,影响响应速度 | | **多角色切换** | 通过权限路由到不同仪表盘 | 支持角色快速切换(如班主任+教师) | 多角色用户需退出重新登录 | | **数据导出** | 无导出功能 | 支持导出 PDF/Excel | 管理员无法离线分析 | | **通知集成** | 无通知集成 | 仪表盘集成待办通知 | 用户需切换页面查看通知 | | **移动端适配** | 基本响应式,但 Widget 布局未优化 | 移动端优先设计,卡片堆叠 | 移动端体验不佳 | ### 3.2 缺失的关键功能 1. **Widget 下钻导航**:统计卡片点击应跳转到对应详情页(部分已实现,但不完整) 2. **时间范围筛选**:admin 无法切换"今日/本周/本月"数据范围 3. **数据对比**:无法对比不同时间段数据(如本周 vs 上周) 4. **自定义仪表盘**:用户无法选择显示哪些 Widget 5. **通知中心集成**:仪表盘未集成通知下拉 --- ## 四、改进优先级建议 ### P0(立即修复) | 编号 | 问题 | 改进方向 | |------|------|---------| | P0-1 | `filterTodaySchedule` 的 `as T[]` 断言 | 移除断言,改用类型守卫或泛型映射 | ### P1(高优先级) | 编号 | 问题 | 改进方向 | |------|------|---------| | P1-1 | 组件内嵌纯函数未抽取 | 迁移 5 个纯函数到 `lib/dashboard-utils.ts`,补充单测 | | P1-2 | `teacherName` 硬编码 fallback | 改为空字符串,前端用 i18n 处理 | | P1-3 | `teacher-schedule.tsx` 本地类型重复 | 从 `types.ts` 导入 | | P1-4 | parent dashboard 组件跨模块 | 迁移到 `modules/dashboard/components/parent-dashboard/` | ### P2(中优先级 - 架构改进) | 编号 | 问题 | 改进方向 | |------|------|---------| | P2-1 | 无数据服务接口抽象 | 定义 `DashboardService` 接口 + 角色实现 + Context 注入 | | P2-2 | 无配置驱动 Widget 渲染 | 定义 `DashboardWidgetConfig`,配置驱动渲染 | | P2-3 | 无监控埋点接口 | 定义 `DashboardAnalytics` 接口,预留埋点 | | P2-4 | admin 趋势数据占位 | 添加 TODO 注释,标记后续实现 | | P2-5 | StatCard 使用模式不一致 | 统一调用模式 | ### P3(低优先级 - 长期优化) | 编号 | 问题 | 改进方向 | |------|------|---------| | P3-1 | 无完整键盘导航 | 添加 `role="region"` + `tabindex` | | P3-2 | AdminTrendCharts 硬编码空数据 | 从 streams 获取真实数据 | | P3-3 | TeacherTodoCard 排序优化 | 用优先级映射 | ### 中长期计划(不在本次实施范围) | 编号 | 问题 | 改进方向 | 阶段 | |------|------|---------|------| | L1 | Widget 下钻导航 | 统计卡片点击跳转详情页 | 第二阶段 | | L2 | 时间范围筛选 | admin 仪表盘添加时间选择器 | 第二阶段 | | L3 | 数据对比 | 添加"本周 vs 上周"对比卡片 | 第三阶段 | | L4 | 自定义仪表盘 | 用户可选择显示哪些 Widget | 第三阶段 | | L5 | 通知中心集成 | 仪表盘集成通知下拉 | 第二阶段 | | L6 | 实时更新 | WebSocket 推送待办数 | 第三阶段 | | L7 | 数据导出 | 支持 PDF/Excel 导出 | 第三阶段 | | L8 | 移动端优化 | Widget 移动端优先布局 | 第二阶段 | --- ## 五、架构图同步说明 ### 需要补充/修改的节点 1. **`streams.ts`**:架构图未记录 `getAdminDashboardStreams` 函数,需在 004 的 dashboard 模块章节和 005 的 `modules.dashboard.exports` 中添加 2. **parent dashboard 组件位置**:架构图需注明 `ParentDashboard` 组件实际位于 `modules/parent/components/`,由 dashboard actions 提供数据 3. **新增 `DashboardService` 接口**(本次实施后):在 005 的 `modules.dashboard` 中添加 `services` 节点 4. **新增 `DashboardWidgetConfig` 类型**(本次实施后):在 005 的 `modules.dashboard.exports.types` 中添加 5. **新增 `DashboardAnalytics` 接口**(本次实施后):在 005 的 `modules.dashboard.exports.services` 中添加 --- ## 六、本次实施计划 ### 实施范围 本次完整实施 P0 + P1 + P2 + P3 + 中长期计划 L1-L8(用户明确要求"包括中长期计划也要完整实施")。 ### 实施步骤与完成状态 | 步骤 | 状态 | 说明 | |------|------|------| | P0-1:修复 `filterTodaySchedule` 的 `as T[]` 断言 | ✅ 已完成 | 移除断言,改为非泛型函数 | | P1-1:抽取 5 个纯函数到 `lib/dashboard-utils.ts` | ✅ 已完成 | timeToMinutes/getScheduleStatus/getDueUrgency/getActionLabelKey/getActionVariant | | P1-2:修复 `teacherName` 硬编码 fallback | ✅ 已完成 | 改为空字符串,前端处理 | | P1-3:修复 `teacher-schedule.tsx` 本地类型重复 | ✅ 已完成 | 从 types.ts 导入 | | P1-4:迁移 parent dashboard 组件到 dashboard 模块 | ✅ 已完成 | 迁移至 components/parent-dashboard/,使用 slots 组合 | | P2-1:定义 `DashboardService` 接口 + Context 注入 | ✅ 已完成 | services/dashboard-service.tsx | | P2-2:定义 `DashboardWidgetConfig` 配置驱动渲染 | ✅ 已完成 | config/widget-configs.ts | | P2-3:定义 `DashboardAnalytics` 监控埋点接口 | ✅ 已完成 | services/dashboard-service.tsx | | P2-4:admin 趋势数据占位 TODO | ✅ 已完成 | data-access.ts 添加 TODO 注释 | | P2-5:4 个角色 StatCard 使用模式统一 | ✅ 已完成 | 统一 color + valueClassName="tabular-nums" | | P3-1:完整键盘导航支持 | ✅ 已完成 | DashboardSection 新增 ariaLabel prop + role="region" + tabIndex | | P3-2:AdminTrendCharts 硬编码空数据 TODO | ✅ 已完成 | 添加 TODO 注释 | | P3-3:TeacherTodoCard 排序优化 | ✅ 已完成 | VARIANT_PRIORITY 数值映射 | | L1:Widget 下钻导航 | ✅ 已实施 | Admin StatCard 添加 href,ContentRow 支持可选 href | | L2:时间范围筛选 | ✅ 已实施 | DashboardTimeRangeFilter 组件 + URL search param 持久化 | | L3:数据对比 | ✅ 已实施 | ComparisonBadge 组件 + computeComparison 纯函数 | | L4:自定义仪表盘 | ✅ 已实施 | useDashboardPreferences Hook + localStorage 持久化 | | L5:通知中心集成 | ✅ 已实施 | DashboardNotificationWidget 组件 | | L6:实时更新 | ✅ 已实施 | useDashboardRealtime Hook(SSE + 指数退避重连) | | L7:数据导出 | ✅ 已实施 | lib/export-utils.ts(CSV 导出 + 浏览器下载) | | L8:移动端优化 | ✅ 已实施 | DashboardResponsiveLayout / MobileSwipeContainer / DesktopGrid | | 同步架构文档 004 和 005 | ✅ 已完成 | 所有新组件/函数/类型已记录 | | 验证:tsc + lint 零错误 | ✅ 已完成 | Dashboard 源码零错误(仅预存测试文件 screen 导入错误),ESLint 零错误 | ### 新增文件清单 | 文件 | 类型 | 职责 | |------|------|------| | `services/dashboard-service.tsx` | Service | DashboardService 接口 + DashboardAnalytics 接口 + Context Provider | | `config/widget-configs.ts` | Config | 4 个角色 Widget 布局配置 | | `hooks/use-dashboard-preferences.ts` | Hook | 自定义仪表盘偏好(L4) | | `hooks/use-dashboard-realtime.ts` | Hook | SSE 实时更新(L6) | | `lib/export-utils.ts` | Lib | CSV 导出工具(L7) | | `components/parent-dashboard/parent-dashboard.tsx` | Component | 家长仪表盘视图(P1-4 迁移) | | `components/dashboard-time-range-filter.tsx` | Component | 时间范围筛选器(L2) | | `components/comparison-badge.tsx` | Component | 数据对比徽章(L3) | | `components/dashboard-notification-widget.tsx` | Component | 通知中心 Widget(L5) | | `components/dashboard-responsive-layout.tsx` | Component | 移动端响应式布局(L8) |