Some checks failed
Security / deep-security-scan (push) Failing after 20m5s
DR Drill / dr-drill (push) Failing after 1m31s
CI / scheduled-backup (push) Failing after 1m31s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
主要变更: - 新增 lesson-preparation 模块: 备课编辑器、节点编辑、AI 建议、知识点选择、版本历史、作业发布 - 新增 shared 通用组件: charts/question-bank-filters/schedule-list/ui (chip-nav/filter-bar/page-header/stat-card/stat-item) - 新增 student/admin 端 loading.tsx 与 error.tsx, 优化加载与错误态体验 - 新增 teacher/lesson-plans 页面 (列表/新建/编辑) - 新增 drizzle 迁移 0002_tiny_lionheart 及 snapshot - 新增 textbooks/schema.ts 与 exams/utils/normalize-structure.ts - 修复 Tiptap v3 SSR hydration 崩溃 (rich-text-block immediatelyRender: false) - 重构多模块 data-access/actions/组件, 修复权限校验与类型规范 - 同步架构文档 004/005 反映新增模块、导出、依赖关系 - 归档 bugs/* 测试报告与 e2e 测试脚本 (admin/parent/student/teacher web_test)
55 lines
1.8 KiB
TypeScript
55 lines
1.8 KiB
TypeScript
import { createId } from "@paralleldrive/cuid2"
|
|
import type { ExamNode } from "../components/assembly/selected-question-list"
|
|
|
|
/**
|
|
* Normalize raw exam structure data into typed `ExamNode[]`.
|
|
*
|
|
* - Validates each node's shape at runtime (type guard pattern, no `as`).
|
|
* - Ensures every node has a unique id (generates one if missing or duplicate).
|
|
* - Recursively normalizes group children.
|
|
* - Returns `[]` for non-array input.
|
|
*
|
|
* Used by the exam build page to convert persisted `exam.structure` (unknown
|
|
* JSON from DB) into a typed tree before passing to `<ExamAssembly />`.
|
|
*/
|
|
export function normalizeStructure(nodes: unknown): ExamNode[] {
|
|
const seen = new Set<string>()
|
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
typeof v === "object" && v !== null
|
|
|
|
const normalize = (raw: unknown[]): ExamNode[] => {
|
|
return raw
|
|
.map((n): ExamNode | null => {
|
|
if (!isRecord(n)) return null
|
|
const type = n.type
|
|
if (type !== "group" && type !== "question") return null
|
|
|
|
let id = typeof n.id === "string" && n.id.length > 0 ? n.id : createId()
|
|
while (seen.has(id)) id = createId()
|
|
seen.add(id)
|
|
|
|
if (type === "group") {
|
|
return {
|
|
id,
|
|
type: "group",
|
|
title: typeof n.title === "string" ? n.title : undefined,
|
|
children: normalize(Array.isArray(n.children) ? n.children : []),
|
|
} satisfies ExamNode
|
|
}
|
|
|
|
if (typeof n.questionId !== "string" || n.questionId.length === 0) return null
|
|
|
|
return {
|
|
id,
|
|
type: "question",
|
|
questionId: n.questionId,
|
|
score: typeof n.score === "number" ? n.score : undefined,
|
|
} satisfies ExamNode
|
|
})
|
|
.filter((n): n is ExamNode => n !== null)
|
|
}
|
|
|
|
if (!Array.isArray(nodes)) return []
|
|
return normalize(nodes)
|
|
}
|