feat(lesson-preparation): 备课模块审计重构 — 跨模块解耦 + i18n + 纯函数抽取 + 错误边界

P0-1 跨模块直查修复:publish-service 不再直查 examQuestions 表,新增 exams/data-access.addExamQuestions 接口,复用 classes/data-access.getStudentIdsByClassIds

P0-2 i18n 接入:新增 zh-CN/en 翻译文件,注册 lessonPreparation 命名空间,17 个组件改造为 useTranslations/getTranslations

P1 纯函数抽取:lib/document-migration.ts(类型守卫替代 as 断言)、lib/node-summary.ts(翻译函数注入)、lib/rf-mappers.ts

P1 错误边界+骨架屏:新增 LessonPlanErrorBoundary 和 4 个 Skeleton 组件

P1 Block 注册表:新增 config/block-registry.tsx(BlockRenderer 组件),node-edit-panel 重构为配置驱动渲染

P1 其他修复:exercise-block 改用 router.refresh(),node-editor/lesson-node 复用 lib/ 纯函数

架构图同步:更新 004 和 005 文档

Refs: docs/architecture/audit/lesson-preparation-audit-report.md
This commit is contained in:
SpecialX
2026-06-22 16:17:58 +08:00
parent 4833930834
commit 20691f53ce
32 changed files with 1456 additions and 360 deletions

View File

@@ -0,0 +1,59 @@
import type { LessonPlanNode } from "../types";
/**
* 节点摘要翻译函数接口。
* 调用方传入 next-intl 的 t 函数,避免纯函数直接耦合 i18n 实现。
* values 类型对齐 next-intl 的 TranslationValuesstring | number | Date
*/
export interface NodeSummaryT {
(key: "editor.questionCount" | "editor.charCount" | "editor.nodeSummaryEmpty", values?: Record<string, string | number | Date>): string;
}
/**
* 纯函数:获取节点摘要文本(用于节点卡片显示)。
* 从 lesson-node.tsx 抽取,便于单元测试。
* 翻译文本由调用方通过 t 函数注入,保证纯函数可测性。
*/
export function getNodeSummary(node: LessonPlanNode, t: NodeSummaryT): string {
const data = node.data as {
html?: string;
sourceText?: string;
items?: unknown[];
knowledgePointIds?: string[];
};
if (data.items !== undefined) {
return t("editor.questionCount", { count: data.items.length });
}
if (data.sourceText !== undefined && data.sourceText) {
return t("editor.charCount", { count: data.sourceText.length });
}
if (data.html) {
// 去标签后取前 40 字
const text = data.html.replace(/<[^>]+>/g, "").trim();
return text.slice(0, 40) || t("editor.nodeSummaryEmpty");
}
return t("editor.nodeSummaryEmpty");
}
/**
* 节点类型 → 图标颜色Material Design 色板)。
* 供 lesson-node 和 minimap 复用。
*/
export const NODE_COLORS: Record<string, string> = {
objective: "#4caf50",
key_point: "#f44336",
import: "#2196f3",
new_teaching: "#9c27b0",
consolidation: "#ff9800",
summary: "#607d8b",
homework: "#795548",
blackboard: "#009688",
text_study: "#3f51b5",
exercise: "#e91e63",
rich_text: "#9e9e9e",
reflection: "#cddc39",
};
export function getNodeColor(type: string): string {
return NODE_COLORS[type] ?? "#9e9e9e";
}