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

@@ -60,23 +60,25 @@ export async function createLessonPlanVersion(input: {
isAuto: boolean;
label?: string;
}): Promise<{ versionNo: number }> {
// 取当前最大 versionNo
const maxRow = await db
.select({ maxNo: max(lessonPlanVersions.versionNo) })
.from(lessonPlanVersions)
.where(eq(lessonPlanVersions.planId, input.planId));
const nextNo = (maxRow[0]?.maxNo ?? 0) + 1;
// P0 修复max(versionNo)+1 必须在事务内完成,避免并发产生重复版本号
return await db.transaction(async (tx) => {
const maxRow = await tx
.select({ maxNo: max(lessonPlanVersions.versionNo) })
.from(lessonPlanVersions)
.where(eq(lessonPlanVersions.planId, input.planId));
const nextNo = (maxRow[0]?.maxNo ?? 0) + 1;
await db.insert(lessonPlanVersions).values({
id: createId(),
planId: input.planId,
versionNo: nextNo,
label: input.label ?? null,
content: input.content,
isAuto: input.isAuto,
creatorId: input.userId,
await tx.insert(lessonPlanVersions).values({
id: createId(),
planId: input.planId,
versionNo: nextNo,
label: input.label ?? null,
content: input.content,
isAuto: input.isAuto,
creatorId: input.userId,
});
return { versionNo: nextNo };
});
return { versionNo: nextNo };
}
export async function getVersionContent(
@@ -116,22 +118,33 @@ export async function revertToVersion(
const content = await getVersionContent(planId, versionNo, userId);
if (!content) return null;
// 用该版本 content 覆盖当前 + 生成新版本
await db
.update(lessonPlans)
.set({ content, lastSavedAt: new Date() })
.where(
and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId)),
);
// P0 修复update 与 createLessonPlanVersion 必须在同一事务内,
// 避免回退后 content 已覆盖但版本记录未生成的不一致状态
return await db.transaction(async (tx) => {
await tx
.update(lessonPlans)
.set({ content, lastSavedAt: new Date() })
.where(
and(eq(lessonPlans.id, planId), eq(lessonPlans.creatorId, userId)),
);
const { versionNo: newNo } = await createLessonPlanVersion({
planId,
content,
userId,
isAuto: false,
label: `回退到 v${versionNo}`,
const maxRow = await tx
.select({ maxNo: max(lessonPlanVersions.versionNo) })
.from(lessonPlanVersions)
.where(eq(lessonPlanVersions.planId, planId));
const newNo = (maxRow[0]?.maxNo ?? 0) + 1;
await tx.insert(lessonPlanVersions).values({
id: createId(),
planId,
versionNo: newNo,
label: `回退到 v${versionNo}`,
content,
isAuto: false,
creatorId: userId,
});
return { newVersionNo: newNo };
});
return { newVersionNo: newNo };
}
export async function pruneAutoVersions(