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:
115
src/shared/config/exam-homework-role-config.test.ts
Normal file
115
src/shared/config/exam-homework-role-config.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
223
src/shared/config/exam-homework-role-config.ts
Normal file
223
src/shared/config/exam-homework-role-config.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user