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,110 @@
import type { ReactElement } from "react";
import type {
BlockType,
ExerciseBlockData,
RichTextBlockData,
TextStudyBlockData,
} from "../types";
import { RichTextBlock } from "../components/blocks/rich-text-block";
import { ExerciseBlock } from "../components/blocks/exercise-block";
import { TextStudyBlock } from "../components/blocks/text-study-block";
import { ReflectionBlock } from "../components/blocks/reflection-block";
/**
* Block 注册表:配置驱动渲染。
* 新增 Block 类型只需在此注册,无需修改 NodeEditPanel。
*/
export interface BlockRenderProps {
blockId: string;
data: RichTextBlockData | ExerciseBlockData | TextStudyBlockData;
textbookId?: string;
chapterId?: string;
classes?: { id: string; name: string }[];
onUpdate: (data: RichTextBlockData | ExerciseBlockData | TextStudyBlockData) => void;
}
export interface BlockRegistryEntry {
/** 是否为富文本类(共享 RichTextBlock 编辑器) */
isRichText?: boolean;
}
const RICH_TEXT_TYPES: BlockType[] = [
"objective",
"key_point",
"import",
"new_teaching",
"consolidation",
"summary",
"homework",
"blackboard",
"rich_text",
];
/**
* Block 注册表元数据(用于查询 isRichText 等属性)。
* 组件渲染由 BlockRenderer 统一处理,避免在 render 中动态获取组件引用。
*/
export const BLOCK_REGISTRY: Record<BlockType, BlockRegistryEntry> = {
objective: { isRichText: true },
key_point: { isRichText: true },
import: { isRichText: true },
new_teaching: { isRichText: true },
consolidation: { isRichText: true },
summary: { isRichText: true },
homework: { isRichText: true },
blackboard: { isRichText: true },
rich_text: { isRichText: true },
exercise: {},
text_study: {},
reflection: {},
};
export function isRichTextBlock(type: BlockType): boolean {
return RICH_TEXT_TYPES.includes(type);
}
/**
* 静态 Block 渲染组件。
* 根据 type 从注册表查找并渲染对应 Block所有组件引用均为模块顶层静态声明
* 满足 react-hooks/static-components 规则。
* 新增 Block 类型时,在此 switch 中添加对应 case 即可。
*/
export function BlockRenderer(props: BlockRenderProps & { type: BlockType }): ReactElement | null {
const { type, ...rest } = props;
switch (type) {
case "exercise":
return (
<ExerciseBlock
blockId={rest.blockId}
data={rest.data as ExerciseBlockData}
classes={rest.classes ?? []}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
/>
);
case "text_study":
return <TextStudyBlock blockId={rest.blockId} data={rest.data as TextStudyBlockData} />;
case "reflection":
return <ReflectionBlock data={rest.data as RichTextBlockData} onUpdate={rest.onUpdate} />;
case "objective":
case "key_point":
case "import":
case "new_teaching":
case "consolidation":
case "summary":
case "homework":
case "blackboard":
case "rich_text":
return (
<RichTextBlock
data={rest.data as RichTextBlockData}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={rest.onUpdate}
/>
);
default:
return null;
}
}