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

@@ -18,6 +18,7 @@ import {
persistExamDraft,
resolveSubjectGradeNames,
updateExamWithQuestions,
type ExamModeConfig,
} from "./data-access"
import {
AiGeneratedStructureSchema,
@@ -58,6 +59,30 @@ const getStringValue = (formData: FormData, key: string) => {
return typeof value === "string" ? value : undefined
}
const getBoolValue = (formData: FormData, key: string, fallback = false): boolean => {
const value = formData.get(key)
if (typeof value !== "string") return fallback
return value === "true"
}
const parseExamModeConfig = (formData: FormData): ExamModeConfig => {
const rawMode = getStringValue(formData, "examMode")
const examMode: ExamModeConfig["examMode"] =
rawMode === "timed" || rawMode === "proctored" ? rawMode : "homework"
const rawDuration = getStringValue(formData, "durationMinutes")
const durationMinutes = rawDuration && Number.isFinite(Number(rawDuration))
? Number(rawDuration)
: null
return {
examMode,
durationMinutes,
shuffleQuestions: getBoolValue(formData, "shuffleQuestions", false),
allowLateStart: getBoolValue(formData, "allowLateStart", false),
lateStartGraceMinutes: Number(getStringValue(formData, "lateStartGraceMinutes") ?? "0") || 0,
antiCheatEnabled: getBoolValue(formData, "antiCheatEnabled", false),
}
}
const failState = <T>(message: string, errors?: Record<string, string[]>): ActionState<T> => ({
success: false,
message,
@@ -317,6 +342,7 @@ export async function createExamAction(
gradeId: input.grade,
scheduledAt: context.scheduled,
description,
examModeConfig: parseExamModeConfig(formData),
})
} catch (error) {
console.error("Failed to create exam:", error)
@@ -436,6 +462,7 @@ export async function createAiExamAction(
description,
structure,
generated,
examModeConfig: parseExamModeConfig(formData),
})
} catch (error) {
console.error("Failed to create exam:", error)