feat(lesson-preparation): add readonly view, anchor node selector, and type guards

- Add lesson-plan-readonly-view for viewing published plans

- Add anchor-node-selector and textbook-segments for canvas anchor positioning

- Add i18n-errors and type-guards lib utilities

- Add lesson-plan-provider-setup for provider initialization

- Update actions, data-access (knowledge, versions, main), publish-service

- Update blocks (blackboard, exercise, homework, import, key-point, objective, reflection)

- Update editor, node-editor, node-edit-panel, pickers, and providers
This commit is contained in:
SpecialX
2026-06-24 12:02:42 +08:00
parent a48e7d0e27
commit 6bc113eaff
39 changed files with 2129 additions and 571 deletions

View File

@@ -1,19 +1,21 @@
import type { ReactElement } from "react";
import type {
BlackboardBlockData,
BlockData,
BlockType,
ExerciseBlockData,
HomeworkBlockData,
ImportBlockData,
KeyPointBlockData,
NewTeachingBlockData,
ObjectiveBlockData,
ReflectionBlockData,
RichTextBlockData,
SummaryBlockData,
TextStudyBlockData,
} from "../types";
import {
isBlackboardBlockData,
isExerciseBlockData,
isHomeworkBlockData,
isImportBlockData,
isKeyPointBlockData,
isNewTeachingBlockData,
isObjectiveBlockData,
isReflectionBlockData,
isRichTextBlockData,
isSummaryBlockData,
isTextStudyBlockData,
} from "../lib/type-guards";
import { RichTextBlock } from "../components/blocks/rich-text-block";
import { ExerciseBlock } from "../components/blocks/exercise-block";
import { TextStudyBlock } from "../components/blocks/text-study-block";
@@ -75,92 +77,117 @@ export function isRichTextBlock(type: BlockType): boolean {
* 根据 type 从注册表查找并渲染对应 Block所有组件引用均为模块顶层静态声明
* 满足 react-hooks/static-components 规则。
* 新增 Block 类型时,在此 switch 中添加对应 case 即可。
*
* V3 修复:使用类型守卫替代 `as` 断言,安全收窄 BlockData 联合类型。
* 类型守卫失败时返回 null理论上不会发生因为 type 与 data 由调用方保证一致)。
*/
export function BlockRenderer(props: BlockRenderProps & { type: BlockType }): ReactElement | null {
const { type, ...rest } = props;
switch (type) {
case "objective":
case "objective": {
if (!isObjectiveBlockData(rest.data)) return null;
return (
<ObjectiveBlock
data={rest.data as ObjectiveBlockData}
data={rest.data}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "key_point":
}
case "key_point": {
if (!isKeyPointBlockData(rest.data)) return null;
return (
<KeyPointBlock
data={rest.data as KeyPointBlockData}
data={rest.data}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "import":
}
case "import": {
if (!isImportBlockData(rest.data)) return null;
return (
<ImportBlock
data={rest.data as ImportBlockData}
data={rest.data}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "new_teaching":
}
case "new_teaching": {
if (!isNewTeachingBlockData(rest.data)) return null;
return (
<NewTeachingBlock
data={rest.data as NewTeachingBlockData}
data={rest.data}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "summary":
}
case "summary": {
if (!isSummaryBlockData(rest.data)) return null;
return (
<SummaryBlock
data={rest.data as SummaryBlockData}
data={rest.data}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "homework":
}
case "homework": {
if (!isHomeworkBlockData(rest.data)) return null;
return (
<HomeworkBlock
data={rest.data as HomeworkBlockData}
data={rest.data}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "blackboard":
}
case "blackboard": {
if (!isBlackboardBlockData(rest.data)) return null;
return (
<BlackboardBlock
data={rest.data as BlackboardBlockData}
data={rest.data}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "reflection":
}
case "reflection": {
if (!isReflectionBlockData(rest.data)) return null;
return (
<ReflectionBlock
data={rest.data as ReflectionBlockData}
data={rest.data}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "exercise":
}
case "exercise": {
if (!isExerciseBlockData(rest.data)) return null;
return (
<ExerciseBlock
blockId={rest.blockId}
data={rest.data as ExerciseBlockData}
data={rest.data}
classes={rest.classes ?? []}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
/>
);
case "text_study":
return <TextStudyBlock blockId={rest.blockId} data={rest.data as TextStudyBlockData} />;
}
case "text_study": {
if (!isTextStudyBlockData(rest.data)) return null;
return <TextStudyBlock blockId={rest.blockId} data={rest.data} />;
}
case "rich_text":
case "consolidation":
case "consolidation": {
if (!isRichTextBlockData(rest.data)) return null;
return (
<RichTextBlock
data={rest.data as RichTextBlockData}
data={rest.data}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
}
default:
return null;
}