Files
NextEdu/docs/architecture/audit/archive/dashboard-audit-report-v4.md

321 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dashboard 模块 V4 审计报告
**审计日期**2026-06-22
**审计范围**`src/modules/dashboard/` + 所有 dashboard 路由文件 + parent dashboard 组件
**前置审计**
- v1P0 修复:跨模块 DB 查询、权限、i18n 容器组件)
- v210 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签)
- v3ContentRow 标签错配、admin/error.tsx i18n、空趋势数据空状态、loading/error.tsx 补齐、日期 locale、死代码清理、`as` 断言修复、流式架构 React `use()`
---
## 一、现有实现概要
### 1.1 文件分布
仪表盘模块位于 `src/modules/dashboard/`,包含 29 个文件:
| 层 | 文件 | 行数 | 职责 |
|----|------|------|------|
| actions | `actions.ts` | 167 | 4 个 Server Actionadmin/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-4parent 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-4admin dashboard `userGrowth` 和 `homeworkTrend` 仍为占位空数组
- **文件**`src/modules/dashboard/data-access.ts`
- **行号**46-47
- **问题**v3 已为 `UserGrowthChart` 添加空状态,但数据源仍硬编码 `userGrowth: []` 和 `homeworkTrend: []`,趋势图表永远显示空状态
- **违反规则**:无直接违反,但影响用户体验
- **后果**:管理员无法看到用户增长和作业提交趋势
- **修复方向**:实现真实统计查询,或在 data-access 层添加 TODO 注释标记后续实现
#### P2-54 个角色 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 不传 colorteacher 传 colorstudent 传 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-4admin 趋势数据占位 TODO | ✅ 已完成 | data-access.ts 添加 TODO 注释 |
| P2-54 个角色 StatCard 使用模式统一 | ✅ 已完成 | 统一 color + valueClassName="tabular-nums" |
| P3-1完整键盘导航支持 | ✅ 已完成 | DashboardSection 新增 ariaLabel prop + role="region" + tabIndex |
| P3-2AdminTrendCharts 硬编码空数据 TODO | ✅ 已完成 | 添加 TODO 注释 |
| P3-3TeacherTodoCard 排序优化 | ✅ 已完成 | VARIANT_PRIORITY 数值映射 |
| L1Widget 下钻导航 | ✅ 已实施 | Admin StatCard 添加 hrefContentRow 支持可选 href |
| L2时间范围筛选 | ✅ 已实施 | DashboardTimeRangeFilter 组件 + URL search param 持久化 |
| L3数据对比 | ✅ 已实施 | ComparisonBadge 组件 + computeComparison 纯函数 |
| L4自定义仪表盘 | ✅ 已实施 | useDashboardPreferences Hook + localStorage 持久化 |
| L5通知中心集成 | ✅ 已实施 | DashboardNotificationWidget 组件 |
| L6实时更新 | ✅ 已实施 | useDashboardRealtime HookSSE + 指数退避重连) |
| L7数据导出 | ✅ 已实施 | lib/export-utils.tsCSV 导出 + 浏览器下载) |
| 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 | 通知中心 WidgetL5 |
| `components/dashboard-responsive-layout.tsx` | Component | 移动端响应式布局L8 |