feat(lesson-preparation): V3→V4 迁移函数(锚点失效标记 + expandedNodeIds)

This commit is contained in:
SpecialX
2026-07-04 11:14:27 +08:00
parent 8a7d0f69ca
commit f7ea76cd60

View File

@@ -9,6 +9,8 @@ import type {
LessonPlanDocument,
LessonPlanDocumentV1,
LessonPlanDocumentV2,
LessonPlanDocumentV3,
LessonPlanDocumentV4,
LessonPlanEdge,
LessonPlanNode,
NodeAnchor,
@@ -80,7 +82,7 @@ export function migrateV2ToV3(
doc: LessonPlanDocumentV2,
chapterId?: string | null,
chapterContent?: string | null,
): LessonPlanDocument {
): LessonPlanDocumentV3 {
const textbookContentNodeId = createId();
const textbookNode: TextbookContentNode = {
id: textbookContentNodeId,
@@ -111,8 +113,63 @@ export function migrateV2ToV3(
};
}
/**
* V3 → V4 迁移:
* - 锚点的 start/end/textPreview/invalid 标记为弃用(保留字段用于识别旧锚点)
* - 新增 expandedNodeIds默认空数组
* - 旧 v3 锚点在 v4 中视为失效,编辑器需显示 banner 提示重新锚定
*
* @returns V4 文档 + 是否含旧锚点(用于触发 banner
*/
export function migrateV3ToV4(
doc: LessonPlanDocumentV3,
): { doc: LessonPlanDocumentV4; hasLegacyAnchors: boolean } {
const hasLegacyAnchors = doc.anchors.length > 0;
return {
doc: {
version: 4,
textbookContentNodeId: doc.textbookContentNodeId,
nodes: doc.nodes,
edges: doc.edges,
anchors: doc.anchors.map((a) => ({
id: a.id,
nodeId: a.nodeId,
type: a.type,
// 保留旧字段用于识别(标记失效)
start: a.start,
end: a.end,
textPreview: a.textPreview,
invalid: true,
})),
expandedNodeIds: [],
},
hasLegacyAnchors,
};
}
// ---- 类型守卫:判断是否为 v4 文档 ----
function isV4Document(content: unknown): content is LessonPlanDocumentV4 {
if (!content || typeof content !== "object") return false;
const c = content as {
version?: unknown;
textbookContentNodeId?: unknown;
nodes?: unknown;
edges?: unknown;
anchors?: unknown;
expandedNodeIds?: unknown;
};
return (
c.version === 4 &&
typeof c.textbookContentNodeId === "string" &&
Array.isArray(c.nodes) &&
Array.isArray(c.edges) &&
Array.isArray(c.anchors) &&
Array.isArray(c.expandedNodeIds)
);
}
// ---- 类型守卫:判断是否为 v3 文档 ----
function isV3Document(content: unknown): content is LessonPlanDocument {
function isV3Document(content: unknown): content is LessonPlanDocumentV3 {
if (!content || typeof content !== "object") return false;
const c = content as {
version?: unknown;
@@ -148,20 +205,23 @@ function isV1Document(content: unknown): content is LessonPlanDocumentV1 {
return c.version === 1 && Array.isArray(c.blocks);
}
// ---- 规范化:确保 content 是 v3 格式(兼容 v1/v2 旧数据)----
// ---- 规范化:确保 content 是 v4 格式(兼容 v1/v2/v3 旧数据)----
export function normalizeDocument(
content: unknown,
chapterId?: string | null,
chapterContent?: string | null,
): LessonPlanDocument {
if (isV3Document(content)) return content;
if (isV4Document(content)) return content;
if (isV3Document(content)) return migrateV3ToV4(content).doc;
if (isV2Document(content)) {
return migrateV2ToV3(content, chapterId, chapterContent);
return migrateV3ToV4(migrateV2ToV3(content, chapterId, chapterContent)).doc;
}
if (isV1Document(content)) {
return migrateV2ToV3(migrateV1ToV2(content), chapterId, chapterContent);
return migrateV3ToV4(
migrateV2ToV3(migrateV1ToV2(content), chapterId, chapterContent),
).doc;
}
// 空文档:创建一个无正文的 v3
// 空文档:创建一个无正文的 v4
const textbookContentNodeId = createId();
const textbookNode: TextbookContentNode = {
id: textbookContentNodeId,
@@ -173,16 +233,17 @@ export function normalizeDocument(
draggable: false,
};
return {
version: 3,
version: 4,
textbookContentNodeId,
nodes: [textbookNode],
edges: [],
anchors: [],
expandedNodeIds: [],
};
}
// ---- 模板初始化:根据骨架生成初始 contentv3,无正文节点)----
// 注意:此函数不创建正文节点,调用方应使用 buildDefaultSkeleton 创建完整 v3 文档
// ---- 模板初始化:根据骨架生成初始 contentv4,无正文节点)----
// 注意:此函数不创建正文节点,调用方应使用 buildDefaultSkeleton 创建完整 v4 文档
export function buildInitialContent(
blocks: TemplateBlockSkeleton[],
): LessonPlanDocument {
@@ -217,11 +278,12 @@ export function buildInitialContent(
}
return {
version: 3,
version: 4,
textbookContentNodeId,
nodes: [textbookNode, ...nodes],
edges,
anchors: [],
expandedNodeIds: [],
};
}
@@ -283,11 +345,12 @@ export function buildDefaultSkeleton(
}));
return {
version: 3,
version: 4,
textbookContentNodeId,
nodes: [textbookNode, ...nodes],
edges,
anchors: [],
expandedNodeIds: [],
};
}