refactor(dashboard): V2 审计重构 — i18n 补齐 + 共享抽象 + 单测 + a11y

V2 审计报告(docs/architecture/audit/dashboard-audit-report-v2.md)发现并修复:

- P0 i18n:10 个子组件硬编码字符串全部接入 next-intl(teacher-quick-actions /
  teacher-classes-card / teacher-homework-card / teacher-schedule /
  recent-submissions / teacher-grade-trends / student-grades-card /
  student-today-schedule-card / student-upcoming-assignments-card /
  admin-dashboard),新增 ~50 个翻译键
- P1 共享抽象:新增 DashboardGreetingHeader 组件,消除 teacher/student
  头部 90% 重复代码,两个 Header 改为薄包装
- P2 单测:为 6 个纯函数添加 31 个单元测试
  (tests/integration/dashboard/dashboard-utils.test.ts)
- P2 a11y:admin 表格 caption、teacher/student 视图语义化标签
  (header / section aria-label / aside aria-label)
- 同步架构图 004/005
This commit is contained in:
SpecialX
2026-06-22 17:01:00 +08:00
parent 10c668f36a
commit e997abaf5e
41 changed files with 1811 additions and 516 deletions

View File

@@ -6055,6 +6055,11 @@
"name": "RecentSubmissions",
"file": "teacher-dashboard/RecentSubmissions",
"purpose": "最近提交"
},
{
"name": "DashboardGreetingHeader",
"file": "dashboard-greeting-header",
"purpose": "共享问候头部组件V2 抽象,消除 teacher/student 头部 90% 重复代码,接收 userName 和可选 actions slot"
}
]
}
@@ -8329,6 +8334,80 @@
]
}
],
"statsService": [
{
"name": "computeGradeStats",
"signature": "(rows: RawScoreRow[]) => GradeStats | null",
"file": "stats-service.ts",
"purpose": "从原始成绩行计算班级统计均分、中位数、标准差、及格率、优秀率、最高分、最低分、参考人数P1-1 新增:从 data-access.getClassGradeStats 抽取为纯函数)",
"usedBy": [
"data-access.getClassGradeStats"
]
},
{
"name": "computeAverageScore",
"signature": "(scores: number[]) => number",
"file": "stats-service.ts",
"purpose": "计算分数平均值(空数组返回 0P1-1 新增:从 data-access.getStudentGradeSummary 抽取为纯函数)",
"usedBy": [
"data-access.getStudentGradeSummary"
]
},
{
"name": "buildGradeTrendPoints",
"signature": "(rows: RawScoreRow[]) => GradeTrendPoint[]",
"file": "stats-service.ts",
"purpose": "构建成绩趋势数据点(按考试标题分组,归一化分数 0-100P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数)",
"usedBy": [
"data-access-analytics.getGradeTrend"
]
},
{
"name": "computeTrendAverage",
"signature": "(points: GradeTrendPoint[]) => number",
"file": "stats-service.ts",
"purpose": "计算趋势数据点的平均分P1-1 新增:从 data-access-analytics.getGradeTrend 抽取为纯函数)",
"usedBy": [
"data-access-analytics.getGradeTrend"
]
},
{
"name": "computeClassComparisonStats",
"signature": "(rows: RawScoreRow[]) => Pick<ClassComparisonItem, 'averageScore' | 'passRate' | 'excellentRate' | 'studentCount'>",
"file": "stats-service.ts",
"purpose": "计算班级对比统计均分、及格率、优秀率、参考人数P1-1 新增:从 data-access-analytics.getClassComparison 抽取为纯函数)",
"usedBy": [
"data-access-analytics.getClassComparison"
]
},
{
"name": "computeSubjectComparisonStats",
"signature": "(scores: number[]) => Pick<SubjectComparisonItem, 'averageScore' | 'passRate' | 'excellentRate' | 'maxScore' | 'minScore'>",
"file": "stats-service.ts",
"purpose": "计算科目对比统计均分、及格率、优秀率、最高分、最低分P1-1 新增:从 data-access-analytics.getSubjectComparison 抽取为纯函数)",
"usedBy": [
"data-access-analytics.getSubjectComparison"
]
},
{
"name": "computeGradeDistribution",
"signature": "(rows: RawScoreRow[]) => GradeDistributionResult",
"file": "stats-service.ts",
"purpose": "计算分数分布90-100/80-89/70-79/60-69/<60 五个区间P1-1 新增:从 data-access-analytics.getGradeDistribution 抽取为纯函数)",
"usedBy": [
"data-access-analytics.getGradeDistribution"
]
},
{
"name": "buildRankingTrendPoints",
"signature": "(byTitle: Map<string, RankingTrendEntry>, targetStudentId: string) => RankingTrendPoint[]",
"file": "stats-service.ts",
"purpose": "构建排名趋势数据点按考试标题排序、计算每次考试学生排名P1-1 新增:从 data-access-ranking.getRankingTrend 抽取为纯函数)",
"usedBy": [
"data-access-ranking.getRankingTrend"
]
}
],
"components": [
{
"name": "GradeRecordForm",
@@ -8433,6 +8512,15 @@
"usedBy": [
"teacher/grades/stats/page.tsx"
]
},
{
"name": "WidgetBoundary",
"file": "components/widget-boundary.tsx",
"purpose": "通用 Widget 边界组件Error Boundary + Suspense + Skeleton 组合,含 a11y 属性 role=alert/aria-live/aria-labelP1-5 新增)",
"deps": [
"shared/components/ui/skeleton",
"shared/components/ui/button"
]
}
]
}
@@ -9109,19 +9197,20 @@
},
"messaging": {
"path": "src/modules/messaging",
"description": "站内消息系统:用户间私信收发(支持回复链)、站内通知多态类型message/announcement/homework/gradeSiteHeader 通知下拉菜单展示未读数",
"description": "站内私信系统:用户间私信收发(支持回复链)。通知相关 UI 组件和 CRUD Action 已迁移至 notifications 模块P1-4 修复)",
"exports": {
"actions": [
{
"name": "sendMessageAction",
"permission": "MESSAGE_SEND",
"signature": "(prevState: ActionState<string> | null, formData: FormData) => Promise<ActionState<string>>",
"purpose": "发送消息(同时通过 notifications dispatcher 为收件人创建多渠道通知;支持 parentMessageId 回复)",
"purpose": "发送消息(同时通过 notifications dispatcher 为收件人创建多渠道通知;支持 parentMessageId 回复P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"shared/db",
"data-access.createMessage",
"notifications.dispatcher.sendNotification",
"trackEvent",
"revalidatePath"
],
"usedBy": [
@@ -9132,11 +9221,12 @@
"name": "markMessageAsReadAction",
"permission": "MESSAGE_READ",
"signature": "(id: string) => Promise<ActionState<void>>",
"purpose": "标记消息已读(设置 readAt",
"purpose": "标记消息已读(设置 readAtP2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"schema.MessageIdSchema",
"data-access.markMessageAsRead",
"trackEvent",
"revalidatePath"
],
"usedBy": [
@@ -9148,11 +9238,12 @@
"name": "deleteMessageAction",
"permission": "MESSAGE_DELETE",
"signature": "(id: string) => Promise<ActionState<void>>",
"purpose": "删除消息(仅发送者或接收者可删)",
"purpose": "删除消息(仅发送者或接收者可删P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"schema.MessageIdSchema",
"data-access.deleteMessage",
"trackEvent",
"revalidatePath"
],
"usedBy": [
@@ -9162,14 +9253,14 @@
{
"name": "getMessagesAction",
"permission": "MESSAGE_READ",
"signature": "(params?: { type?, page?, pageSize? }) => Promise<ActionState<PaginatedResult<MessageListItem>>>",
"purpose": "获取消息列表(收件箱/已发送,分页)",
"signature": "(params?: { type?, page?, pageSize?, keyword? }) => Promise<ActionState<PaginatedResult<MessageListItem>>>",
"purpose": "获取消息列表(收件箱/已发送,分页,关键词搜索;客户端通过 useMessageSearch hook 调用",
"deps": [
"requirePermission",
"data-access.getMessages"
],
"usedBy": [
"message-list.tsx"
"message-list.tsx (via useMessageSearch hook)"
]
},
{
@@ -9201,47 +9292,16 @@
]
},
{
"name": "getNotificationsAction",
"name": "getUnreadMessageCountAction",
"permission": "MESSAGE_READ",
"signature": "(params?: { page?, pageSize? }) => Promise<ActionState<PaginatedResult<NotificationListItem>>>",
"purpose": "获取当前用户通知列表(分页",
"signature": "() => Promise<ActionState<number>>",
"purpose": "获取当前用户未读私信计数unread-message-badge 组件每 60 秒轮询",
"deps": [
"requirePermission",
"data-access.getNotifications"
"data-access.getUnreadMessageCount"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "markNotificationAsReadAction",
"permission": "MESSAGE_READ",
"signature": "(id: string) => Promise<ActionState<void>>",
"purpose": "标记单条通知已读",
"deps": [
"requirePermission",
"data-access.markNotificationAsRead",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "markAllNotificationsAsReadAction",
"permission": "MESSAGE_READ",
"signature": "() => Promise<ActionState<void>>",
"purpose": "标记所有通知已读",
"deps": [
"requirePermission",
"data-access.markAllNotificationsAsRead",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
"unread-message-badge.tsx"
]
},
{
@@ -9251,7 +9311,7 @@
"purpose": "获取当前用户的通知偏好设置(首次访问自动创建默认记录)",
"deps": [
"requirePermission",
"notification-preferences.getNotificationPreferences"
"notifications.preferences.getNotificationPreferences"
],
"usedBy": [
"settings/page.tsx",
@@ -9266,7 +9326,7 @@
"deps": [
"requirePermission",
"schema.UpdateNotificationPreferencesSchema",
"notification-preferences.upsertNotificationPreferences",
"notifications.preferences.upsertNotificationPreferences",
"revalidatePath"
],
"usedBy": [
@@ -9362,70 +9422,23 @@
"shared.db.schema.messages"
],
"usedBy": [
"待扩展"
"getUnreadMessageCountAction",
"unread-message-badge.tsx"
]
},
{
"name": "getNotifications",
"signature": "(userId: string, params?: { page?, pageSize? }) => Promise<PaginatedResult<NotificationListItem>>",
"name": "getMessagesPageData",
"signature": "(userId: string) => Promise<{ messages: PaginatedResult<Message>, notifications: PaginatedResult<Notification> }>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移",
"purpose": "P1-5 新增:消息首页编排函数,一次性获取消息列表和通知列表(通知通过动态 import notifications/data-access",
"deps": [
"data-access.getMessages",
"notifications.data-access.getNotifications"
],
"usedBy": [
"getNotificationsAction",
"messages/page.tsx"
]
},
{
"name": "createNotification",
"signature": "(input: CreateNotificationInput) => Promise<string>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.createNotification"
],
"usedBy": [
"notifications.dispatcher (via in-app-channel)"
]
},
{
"name": "markNotificationAsRead",
"signature": "(id: string, userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.markNotificationAsRead"
],
"usedBy": [
"markNotificationAsReadAction"
]
},
{
"name": "markAllNotificationsAsRead",
"signature": "(userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.markAllNotificationsAsRead"
],
"usedBy": [
"markAllNotificationsAsReadAction"
]
},
{
"name": "getUnreadNotificationCount",
"signature": "(userId: string) => Promise<number>",
"file": "data-access.ts",
"purpose": "re-export shim实际逻辑在 notifications/data-access.tsP0-4 / P1-5 修复后迁移)",
"deps": [
"notifications.data-access.getUnreadNotificationCount"
],
"usedBy": [
"待扩展"
]
},
{
"name": "getRecipients",
"signature": "(ctx: AuthContext) => Promise<RecipientOption[]>",
@@ -9629,21 +9642,26 @@
"purpose": "写消息表单(收件人 Select、主题 Input、内容 Textarea支持回复模式"
},
{
"name": "NotificationDropdown",
"file": "components/notification-dropdown.tsx",
"purpose": "SiteHeader 通知下拉菜单Bell 图标 + 未读数 Badge滚动列表标记已读查看全部链接"
},
"name": "UnreadMessageBadge",
"file": "components/unread-message-badge.tsx",
"purpose": "未读消息计数徽章(侧边栏 Messages 导航项旁显示,每 60 秒轮询 getUnreadMessageCountAction"
}
],
"hooks": [
{
"name": "NotificationList",
"file": "components/notification-list.tsx",
"purpose": "通知完整列表(全部标记已读、单条标记已读、查看链接)"
"name": "useMessageSearch",
"file": "hooks/use-message-search.ts",
"purpose": "P1-7 新增:消息搜索 hook400ms 防抖 + 请求竞态取消,通过 requestIdRef 匹配最新请求)",
"usedBy": [
"message-list.tsx"
]
}
]
}
},
"notifications": {
"path": "src/modules/notifications",
"description": "通知渠道集成层基于用户通知偏好notification_preferences将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。",
"description": "通知渠道集成层基于用户通知偏好notification_preferences将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。P1-4 修复后新增通知 UI 组件和通知 CRUD Server Action。",
"exports": {
"actions": [
{
@@ -9673,6 +9691,66 @@
"usedBy": [
"待扩展"
]
},
{
"name": "getNotificationsAction",
"permission": "MESSAGE_READ",
"signature": "(params?: { page?, pageSize?, unreadOnly? }) => Promise<ActionState<PaginatedResult<Notification>>>",
"purpose": "P1-4 新增(从 messaging 迁移):获取当前用户通知列表(分页)",
"deps": [
"requirePermission",
"data-access.getNotifications"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "getUnreadNotificationCountAction",
"permission": "MESSAGE_READ",
"signature": "() => Promise<ActionState<number>>",
"purpose": "P1-4 新增(从 messaging 迁移):获取当前用户未读通知计数",
"deps": [
"requirePermission",
"data-access.getUnreadNotificationCount"
],
"usedBy": [
"notification-dropdown.tsx"
]
},
{
"name": "markNotificationAsReadAction",
"permission": "MESSAGE_READ",
"signature": "(notificationId: string) => Promise<ActionState<string>>",
"purpose": "P1-4 新增(从 messaging 迁移标记单条通知已读P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"schema.NotificationIdSchema",
"data-access.markNotificationAsRead",
"trackEvent",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "markAllNotificationsAsReadAction",
"permission": "MESSAGE_READ",
"signature": "() => Promise<ActionState<string>>",
"purpose": "P1-4 新增(从 messaging 迁移标记所有通知已读P2-11 新增 trackEvent 埋点",
"deps": [
"requirePermission",
"data-access.markAllNotificationsAsRead",
"trackEvent",
"revalidatePath"
],
"usedBy": [
"notification-dropdown.tsx",
"notification-list.tsx"
]
}
],
"dispatcher": [
@@ -10029,6 +10107,18 @@
"messaging (via re-export)"
]
}
],
"components": [
{
"name": "NotificationList",
"file": "components/notification-list.tsx",
"purpose": "P1-4 新增(从 messaging 迁移):通知完整列表(全部标记已读、单条标记已读、查看链接)"
},
{
"name": "NotificationDropdown",
"file": "components/notification-dropdown.tsx",
"purpose": "P1-4 新增(从 messaging 迁移SiteHeader 通知下拉菜单Bell 图标 + 未读数 Badge每 30 秒轮询,滚动列表,标记已读,查看全部链接)"
}
]
}
},
@@ -12517,6 +12607,8 @@
"lib/node-summary.ts",
"lib/rf-mappers.ts",
"config/block-registry.tsx",
"providers/lesson-plan-provider.tsx",
"services/default-data-service.ts",
"data-access.ts",
"data-access-versions.ts",
"data-access-templates.ts",
@@ -12549,7 +12641,29 @@
"components/blocks/text-study-block.tsx",
"components/blocks/exercise-block.tsx",
"components/blocks/reflection-block.tsx"
]
],
"i18n": {
"namespace": "lessonPreparation",
"status": "implemented",
"messageFiles": [
"shared/i18n/messages/zh-CN/lesson-preparation.json",
"shared/i18n/messages/en/lesson-preparation.json"
]
},
"auditFixes": {
"P0-1": "publish-service.ts 跨模块直查修复:改用 exams/classes data-access",
"P0-2": "i18n 接入17 个组件改造为 useTranslations/getTranslations",
"P0-3": "buildScopeCondition 按 scope 类型精确过滤class_taught/grade_managed/class_members/children",
"P1-1": "as never 断言替换为类型守卫函数BLOCK_TYPE_LABELS/LESSON_PLAN_STATUS_LABELS 改为 i18n 键",
"P1-2": "新增 LessonPlanErrorBoundary 错误边界",
"P1-3": "新增 4 个 Skeleton 骨架屏组件",
"P1-4": "alert/confirm/window.location.reload 替换为 toast/AlertDialog/router.refresh",
"P1-5": "新增 LessonPlanProvider + Context 注入数据服务,支持多实例",
"P1-7": "新增 4 个角色配置TEACHER/ADMIN/STUDENT/PARENT+ ROLE_CONFIGS 注册表",
"P1-8": "新增 BLOCK_REGISTRY 注册表node-edit-panel 配置驱动渲染",
"P2-1": "5 个组件添加 role=dialog/aria-modal/aria-label",
"P2-4": "预留 LessonPlanTracker 接口 + noopTracker 默认实现"
}
}
},
"dbTables": {