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,4 +1,4 @@
import type { LessonPlanNode } from "../types";
import type { LessonPlanNode, TextbookContentNode } from "../types";
/**
* 节点摘要翻译函数接口。
@@ -6,7 +6,17 @@ import type { LessonPlanNode } from "../types";
* values 类型对齐 next-intl 的 TranslationValuesstring | number | Date
*/
export interface NodeSummaryT {
(key: "editor.questionCount" | "editor.charCount" | "editor.nodeSummaryEmpty", values?: Record<string, string | number | Date>): string;
(
key:
| "editor.questionCount"
| "editor.charCount"
| "editor.nodeSummaryEmpty"
| "editor.itemCount"
| "editor.pointCount"
| "editor.assignmentCount"
| "editor.durationMin",
values?: Record<string, string | number | Date>,
): string;
}
/**
@@ -16,17 +26,69 @@ export interface NodeSummaryT {
*/
export function getNodeSummary(node: LessonPlanNode, t: NodeSummaryT): string {
const data = node.data as {
// 富文本类
html?: string;
// 文本研习
sourceText?: string;
annotations?: unknown[];
// 练习
items?: unknown[];
// 教学目标
objectives?: unknown[];
// 重难点
keyPoints?: unknown[];
// 导入
durationMin?: number;
prompt?: string;
// 新授
teachingPoints?: unknown[];
// 小结
summaryPoints?: unknown[];
// 作业
assignments?: unknown[];
// 板书
content?: string;
// 反思
reflection?: unknown[];
// 知识点
knowledgePointIds?: string[];
};
// 按类型优先级提取摘要
if (data.items !== undefined) {
return t("editor.questionCount", { count: data.items.length });
}
if (data.objectives !== undefined) {
return t("editor.itemCount", { count: data.objectives.length });
}
if (data.keyPoints !== undefined) {
return t("editor.itemCount", { count: data.keyPoints.length });
}
if (data.teachingPoints !== undefined) {
return t("editor.pointCount", { count: data.teachingPoints.length });
}
if (data.summaryPoints !== undefined) {
return t("editor.itemCount", { count: data.summaryPoints.length });
}
if (data.assignments !== undefined) {
return t("editor.assignmentCount", { count: data.assignments.length });
}
if (data.reflection !== undefined) {
return t("editor.itemCount", { count: data.reflection.length });
}
if (data.durationMin !== undefined) {
return t("editor.durationMin", { count: data.durationMin });
}
if (data.annotations !== undefined && data.sourceText !== undefined) {
return t("editor.charCount", { count: data.sourceText.length });
}
if (data.sourceText !== undefined && data.sourceText) {
return t("editor.charCount", { count: data.sourceText.length });
}
if (data.content !== undefined && data.content) {
const text = data.content.replace(/<[^>]+>/g, "").trim();
return text.slice(0, 40) || t("editor.nodeSummaryEmpty");
}
if (data.html) {
// 去标签后取前 40 字
const text = data.html.replace(/<[^>]+>/g, "").trim();
@@ -35,6 +97,17 @@ export function getNodeSummary(node: LessonPlanNode, t: NodeSummaryT): string {
return t("editor.nodeSummaryEmpty");
}
/**
* 纯函数:获取正文节点摘要。
*/
export function getTextbookContentSummary(
node: TextbookContentNode,
t: NodeSummaryT,
): string {
if (!node.data.content) return t("editor.nodeSummaryEmpty");
return t("editor.charCount", { count: node.data.content.length });
}
/**
* 节点类型 → 图标颜色Material Design 色板)。
* 供 lesson-node 和 minimap 复用。
@@ -52,6 +125,7 @@ export const NODE_COLORS: Record<string, string> = {
exercise: "#e91e63",
rich_text: "#9e9e9e",
reflection: "#cddc39",
textbook_content: "#455a64",
};
export function getNodeColor(type: string): string {