fix(dashboard): v3 审计修复 — 数据完整性、i18n、类型安全、死代码清理

P0 修复(严重):
- admin ContentRow 标签与值错配(stats.users→textbooks 等 6 处)
- admin/error.tsx 硬编码中文替换为 useTranslations
- UserGrowthChart 空数据时渲染 EmptyState(userGrowth/homeworkTrend 永远为空数组)

P1 修复(高):
- 新增 admin/dashboard 和 student/dashboard 的 loading.tsx + error.tsx
- 抽取 DashboardLoadingSkeleton 和 DashboardErrorFallback 共享组件,消除 5 套重复文件
- formatDate/formatLongDate 传入用户 locale(admin/teacher/student 共 6 个组件)
- 移除死代码:getCachedAdminDashboard、AvatarImage src={undefined}、TeacherStats isLoading prop
- filterTodaySchedule 改为泛型函数,消除 as 类型断言
- 辅助函数 getStatus/getDueUrgency 新增显式返回类型
- UserGrowthChart 新增 labelKey prop 区分用户增长/作业提交趋势标签

P2 修复(中):
- 4 个组件从客户端转为服务端组件(DashboardGreetingHeader、TeacherQuickActions、TeacherDashboardHeader、StudentDashboardHeader)
- Student dashboard 空状态新增 CTA(viewSchedule、viewAll)
- TeacherHomeworkCard 图标按钮新增 aria-label
- TeacherTodoCard 排序逻辑重写为可读的 if/return 模式

同步更新:
- docs/architecture/005_architecture_data.json 新增 DashboardLoadingSkeleton、DashboardErrorFallback 条目
- 新增 docs/architecture/audit/dashboard-audit-report-v3.md 审计报告
- dashboard.json 新增 6 个 i18n 键(textbooks/chapters/questions/exams/totalAssignments/totalSubmissions)
This commit is contained in:
SpecialX
2026-06-22 18:36:46 +08:00
parent f62b8c0f86
commit 682d385ee2
41 changed files with 4387 additions and 1979 deletions

View File

@@ -0,0 +1,115 @@
/**
* 6.5: 单测覆盖 ExamHomeworkRoleConfig 角色特性合并逻辑
*/
import { describe, it, expect } from "vitest"
import {
getExamHomeworkFeatures,
EXAM_HOMEWORK_ROLE_CONFIG,
DEFAULT_EXAM_HOMEWORK_FEATURES,
type ExamHomeworkRoleFeatures,
} from "./exam-homework-role-config"
describe("EXAM_HOMEWORK_ROLE_CONFIG", () => {
it("admin has all management features but not student take", () => {
const admin = EXAM_HOMEWORK_ROLE_CONFIG.admin
expect(admin.canCreate).toBe(true)
expect(admin.canManage).toBe(true)
expect(admin.canPublish).toBe(true)
expect(admin.canGrade).toBe(true)
expect(admin.canProctor).toBe(true)
expect(admin.canUseAi).toBe(true)
expect(admin.canViewStats).toBe(true)
expect(admin.canTake).toBe(false)
})
it("teacher has create/grade/build but not student take", () => {
const teacher = EXAM_HOMEWORK_ROLE_CONFIG.teacher
expect(teacher.canCreate).toBe(true)
expect(teacher.canBuild).toBe(true)
expect(teacher.canGrade).toBe(true)
expect(teacher.canUseAi).toBe(true)
expect(teacher.canTake).toBe(false)
})
it("student has take and viewResult but not manage/create", () => {
const student = EXAM_HOMEWORK_ROLE_CONFIG.student
expect(student.canTake).toBe(true)
expect(student.canViewResult).toBe(true)
expect(student.canCreate).toBe(false)
expect(student.canManage).toBe(false)
expect(student.canGrade).toBe(false)
expect(student.canPublish).toBe(false)
})
it("grade_head can grade/publish/proctor but not create/build", () => {
const gh = EXAM_HOMEWORK_ROLE_CONFIG.grade_head
expect(gh.canGrade).toBe(true)
expect(gh.canPublish).toBe(true)
expect(gh.canProctor).toBe(true)
expect(gh.canCreate).toBe(false)
expect(gh.canBuild).toBe(false)
expect(gh.canUseAi).toBe(false)
})
it("parent can only view results", () => {
const parent = EXAM_HOMEWORK_ROLE_CONFIG.parent
expect(parent.canViewResult).toBe(true)
expect(parent.canCreate).toBe(false)
expect(parent.canRead).toBe(false)
expect(parent.canTake).toBe(false)
expect(parent.canGrade).toBe(false)
})
})
describe("getExamHomeworkFeatures", () => {
it("returns default (all-false) for empty roles", () => {
const features = getExamHomeworkFeatures([])
expect(features).toEqual(DEFAULT_EXAM_HOMEWORK_FEATURES)
})
it("returns admin features for admin role", () => {
const features = getExamHomeworkFeatures(["admin"])
expect(features.canCreate).toBe(true)
expect(features.canManage).toBe(true)
expect(features.canTake).toBe(false)
})
it("returns student features for student role", () => {
const features = getExamHomeworkFeatures(["student"])
expect(features.canTake).toBe(true)
expect(features.canCreate).toBe(false)
})
it("merges features via union for multiple roles", () => {
// A user with both teacher and student roles should have both canCreate and canTake
const features = getExamHomeworkFeatures(["teacher", "student"])
expect(features.canCreate).toBe(true)
expect(features.canGrade).toBe(true)
expect(features.canTake).toBe(true)
expect(features.canViewResult).toBe(true)
})
it("ignores unknown roles gracefully", () => {
// Cast to bypass type-check for testing runtime safety
const features = getExamHomeworkFeatures(["unknown_role" as never])
expect(features).toEqual(DEFAULT_EXAM_HOMEWORK_FEATURES)
})
it("DEFAULT_EXAM_HOMEWORK_FEATURES has all features disabled", () => {
const allFalse: ExamHomeworkRoleFeatures = {
canCreate: false,
canRead: false,
canManage: false,
canPublish: false,
canBuild: false,
canGrade: false,
canProctor: false,
canTake: false,
canViewResult: false,
canUseAi: false,
canViewStats: false,
}
expect(DEFAULT_EXAM_HOMEWORK_FEATURES).toEqual(allFalse)
})
})

View File

@@ -0,0 +1,223 @@
/**
* P2-13: 配置驱动的角色渲染 ExamHomeworkRoleConfig
*
* 单一数据源:声明每个角色在考试/作业模块中可见的功能区与可执行的操作。
* 配合 usePermission() 使用,作为 UI 渲染的防御性筛选层(权限系统仍为最终授权源)。
*
* 使用方式:
* const features = getRoleFeatures(roles)
* if (features.canCreateExam) { render <CreateExamButton /> }
*/
import type { Permission, Role } from "@/shared/types/permissions"
import { Permissions } from "@/shared/types/permissions"
/**
* 考试/作业模块中按角色控制的功能区。
* 字段语义:`can*` 表示是否可见/可执行某类操作。
*/
export interface ExamHomeworkRoleFeatures {
/** 创建考试/作业(教师、管理员) */
canCreate: boolean
/** 查看考试/作业列表(所有有读权限的角色) */
canRead: boolean
/** 编辑/删除考试/作业(教师、管理员) */
canManage: boolean
/** 发布考试(教师、年级组长、教务主任) */
canPublish: boolean
/** 组卷/编辑作业题目(教师、管理员) */
canBuild: boolean
/** 批改作业/考试(教师、年级组长、教务主任) */
canGrade: boolean
/** 监考(教师、年级组长、教务主任) */
canProctor: boolean
/** 学生作答(学生) */
canTake: boolean
/** 查看成绩/反馈(学生、家长) */
canViewResult: boolean
/** AI 生成考试(教师、管理员) */
canUseAi: boolean
/** 查看统计/分析(教师、年级组长、教务主任、管理员) */
canViewStats: boolean
}
/**
* 角色到权限点的映射,作为渲染配置的依据。
* 每个角色对应一组 Permission 常量,用于推导 features。
*/
export const EXAM_HOMEWORK_ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
admin: [
Permissions.EXAM_CREATE,
Permissions.EXAM_READ,
Permissions.EXAM_UPDATE,
Permissions.EXAM_DELETE,
Permissions.EXAM_PUBLISH,
Permissions.EXAM_AI_GENERATE,
Permissions.EXAM_PROCTOR,
Permissions.HOMEWORK_CREATE,
Permissions.HOMEWORK_GRADE,
Permissions.HOMEWORK_SUBMIT,
],
teacher: [
Permissions.EXAM_CREATE,
Permissions.EXAM_READ,
Permissions.EXAM_UPDATE,
Permissions.EXAM_PUBLISH,
Permissions.EXAM_AI_GENERATE,
Permissions.EXAM_PROCTOR,
Permissions.HOMEWORK_CREATE,
Permissions.HOMEWORK_GRADE,
],
grade_head: [
Permissions.EXAM_READ,
Permissions.EXAM_PUBLISH,
Permissions.EXAM_PROCTOR,
Permissions.HOMEWORK_GRADE,
],
teaching_head: [
Permissions.EXAM_READ,
Permissions.EXAM_PUBLISH,
Permissions.EXAM_PROCTOR,
Permissions.HOMEWORK_GRADE,
],
student: [
Permissions.EXAM_READ,
Permissions.EXAM_SUBMIT,
Permissions.HOMEWORK_SUBMIT,
],
parent: [],
} as const
/**
* 角色到功能区配置的映射。
* 这是渲染层的单一数据源UI 根据 features 决定显示哪些入口/按钮。
*/
export const EXAM_HOMEWORK_ROLE_CONFIG: Record<Role, ExamHomeworkRoleFeatures> = {
admin: {
canCreate: true,
canRead: true,
canManage: true,
canPublish: true,
canBuild: true,
canGrade: true,
canProctor: true,
canTake: false,
canViewResult: true,
canUseAi: true,
canViewStats: true,
},
teacher: {
canCreate: true,
canRead: true,
canManage: true,
canPublish: true,
canBuild: true,
canGrade: true,
canProctor: true,
canTake: false,
canViewResult: true,
canUseAi: true,
canViewStats: true,
},
grade_head: {
canCreate: false,
canRead: true,
canManage: false,
canPublish: true,
canBuild: false,
canGrade: true,
canProctor: true,
canTake: false,
canViewResult: true,
canUseAi: false,
canViewStats: true,
},
teaching_head: {
canCreate: false,
canRead: true,
canManage: false,
canPublish: true,
canBuild: false,
canGrade: true,
canProctor: true,
canTake: false,
canViewResult: true,
canUseAi: false,
canViewStats: true,
},
student: {
canCreate: false,
canRead: true,
canManage: false,
canPublish: false,
canBuild: false,
canGrade: false,
canProctor: false,
canTake: true,
canViewResult: true,
canUseAi: false,
canViewStats: false,
},
parent: {
canCreate: false,
canRead: false,
canManage: false,
canPublish: false,
canBuild: false,
canGrade: false,
canProctor: false,
canTake: false,
canViewResult: true,
canUseAi: false,
canViewStats: false,
},
}
/**
* 默认(无角色)特性集:所有功能关闭。
* 用于未登录或角色未识别的兜底场景。
*/
export const DEFAULT_EXAM_HOMEWORK_FEATURES: ExamHomeworkRoleFeatures = {
canCreate: false,
canRead: false,
canManage: false,
canPublish: false,
canBuild: false,
canGrade: false,
canProctor: false,
canTake: false,
canViewResult: false,
canUseAi: false,
canViewStats: false,
}
/**
* 根据用户角色列表合并出最终的功能特性。
*
* 合并策略:任一角色拥有某功能即视为启用(并集)。
* 例如:同时具有 teacher + grade_head 的用户,将获得两个角色的所有能力。
*
* @param roles 当前用户的角色列表
* @returns 合并后的功能特性
*/
export function getExamHomeworkFeatures(roles: readonly Role[]): ExamHomeworkRoleFeatures {
if (roles.length === 0) return DEFAULT_EXAM_HOMEWORK_FEATURES
const merged: ExamHomeworkRoleFeatures = { ...DEFAULT_EXAM_HOMEWORK_FEATURES }
for (const role of roles) {
const cfg = EXAM_HOMEWORK_ROLE_CONFIG[role]
if (!cfg) continue
merged.canCreate = merged.canCreate || cfg.canCreate
merged.canRead = merged.canRead || cfg.canRead
merged.canManage = merged.canManage || cfg.canManage
merged.canPublish = merged.canPublish || cfg.canPublish
merged.canBuild = merged.canBuild || cfg.canBuild
merged.canGrade = merged.canGrade || cfg.canGrade
merged.canProctor = merged.canProctor || cfg.canProctor
merged.canTake = merged.canTake || cfg.canTake
merged.canViewResult = merged.canViewResult || cfg.canViewResult
merged.canUseAi = merged.canUseAi || cfg.canUseAi
merged.canViewStats = merged.canViewStats || cfg.canViewStats
}
return merged
}