feat(lesson-preparation): add anchor canvas design, new blocks, and textbook content node

- Add anchor injector for canvas-based anchor positioning

- Add new block components: blackboard, homework, import, key-point, new-teaching, objective, summary

- Add textbook content node for React Flow canvas

- Update actions (kp, publish, main), data-access (templates, versions, main)

- Update editor, node-editor, block-renderer, and picker components

- Update schema, types, hooks, and lib utilities (document-migration, node-summary, rf-mappers)
This commit is contained in:
SpecialX
2026-06-23 17:37:19 +08:00
parent 1fcef5c3aa
commit 2197e68069
34 changed files with 3190 additions and 402 deletions

View File

@@ -1,13 +1,29 @@
import type { ReactElement } from "react";
import type {
BlackboardBlockData,
BlockData,
BlockType,
ExerciseBlockData,
HomeworkBlockData,
ImportBlockData,
KeyPointBlockData,
NewTeachingBlockData,
ObjectiveBlockData,
ReflectionBlockData,
RichTextBlockData,
SummaryBlockData,
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 { ObjectiveBlock } from "../components/blocks/objective-block";
import { KeyPointBlock } from "../components/blocks/key-point-block";
import { ImportBlock } from "../components/blocks/import-block";
import { NewTeachingBlock } from "../components/blocks/new-teaching-block";
import { SummaryBlock } from "../components/blocks/summary-block";
import { HomeworkBlock } from "../components/blocks/homework-block";
import { BlackboardBlock } from "../components/blocks/blackboard-block";
import { ReflectionBlock } from "../components/blocks/reflection-block";
/**
@@ -17,11 +33,11 @@ import { ReflectionBlock } from "../components/blocks/reflection-block";
export interface BlockRenderProps {
blockId: string;
data: RichTextBlockData | ExerciseBlockData | TextStudyBlockData;
data: BlockData;
textbookId?: string;
chapterId?: string;
classes?: { id: string; name: string }[];
onUpdate: (data: RichTextBlockData | ExerciseBlockData | TextStudyBlockData) => void;
onUpdate: (data: BlockData) => void;
}
export interface BlockRegistryEntry {
@@ -29,34 +45,24 @@ export interface BlockRegistryEntry {
isRichText?: boolean;
}
const RICH_TEXT_TYPES: BlockType[] = [
"objective",
"key_point",
"import",
"new_teaching",
"consolidation",
"summary",
"homework",
"blackboard",
"rich_text",
];
const RICH_TEXT_TYPES: BlockType[] = ["rich_text", "consolidation"];
/**
* 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 },
objective: {},
key_point: {},
import: {},
new_teaching: {},
consolidation: { isRichText: true },
summary: { isRichText: true },
homework: { isRichText: true },
blackboard: { isRichText: true },
rich_text: { isRichText: true },
exercise: {},
summary: {},
homework: {},
blackboard: {},
text_study: {},
exercise: {},
rich_text: { isRichText: true },
reflection: {},
};
@@ -73,6 +79,66 @@ export function isRichTextBlock(type: BlockType): boolean {
export function BlockRenderer(props: BlockRenderProps & { type: BlockType }): ReactElement | null {
const { type, ...rest } = props;
switch (type) {
case "objective":
return (
<ObjectiveBlock
data={rest.data as ObjectiveBlockData}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "key_point":
return (
<KeyPointBlock
data={rest.data as KeyPointBlockData}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "import":
return (
<ImportBlock
data={rest.data as ImportBlockData}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "new_teaching":
return (
<NewTeachingBlock
data={rest.data as NewTeachingBlockData}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "summary":
return (
<SummaryBlock
data={rest.data as SummaryBlockData}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "homework":
return (
<HomeworkBlock
data={rest.data as HomeworkBlockData}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "blackboard":
return (
<BlackboardBlock
data={rest.data as BlackboardBlockData}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "reflection":
return (
<ReflectionBlock
data={rest.data as ReflectionBlockData}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
case "exercise":
return (
<ExerciseBlock
@@ -85,23 +151,14 @@ export function BlockRenderer(props: BlockRenderProps & { type: BlockType }): Re
);
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":
case "consolidation":
return (
<RichTextBlock
data={rest.data as RichTextBlockData}
textbookId={rest.textbookId}
chapterId={rest.chapterId}
onUpdate={rest.onUpdate}
onUpdate={(d) => rest.onUpdate(d)}
/>
);
default: