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, LessonPlanDocument,
LessonPlanDocumentV1, LessonPlanDocumentV1,
LessonPlanDocumentV2, LessonPlanDocumentV2,
LessonPlanDocumentV3,
LessonPlanDocumentV4,
LessonPlanEdge, LessonPlanEdge,
LessonPlanNode, LessonPlanNode,
NodeAnchor, NodeAnchor,
@@ -80,7 +82,7 @@ export function migrateV2ToV3(
doc: LessonPlanDocumentV2, doc: LessonPlanDocumentV2,
chapterId?: string | null, chapterId?: string | null,
chapterContent?: string | null, chapterContent?: string | null,
): LessonPlanDocument { ): LessonPlanDocumentV3 {
const textbookContentNodeId = createId(); const textbookContentNodeId = createId();
const textbookNode: TextbookContentNode = { const textbookNode: TextbookContentNode = {
id: textbookContentNodeId, 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 文档 ---- // ---- 类型守卫:判断是否为 v3 文档 ----
function isV3Document(content: unknown): content is LessonPlanDocument { function isV3Document(content: unknown): content is LessonPlanDocumentV3 {
if (!content || typeof content !== "object") return false; if (!content || typeof content !== "object") return false;
const c = content as { const c = content as {
version?: unknown; version?: unknown;
@@ -148,20 +205,23 @@ function isV1Document(content: unknown): content is LessonPlanDocumentV1 {
return c.version === 1 && Array.isArray(c.blocks); return c.version === 1 && Array.isArray(c.blocks);
} }
// ---- 规范化:确保 content 是 v3 格式(兼容 v1/v2 旧数据)---- // ---- 规范化:确保 content 是 v4 格式(兼容 v1/v2/v3 旧数据)----
export function normalizeDocument( export function normalizeDocument(
content: unknown, content: unknown,
chapterId?: string | null, chapterId?: string | null,
chapterContent?: string | null, chapterContent?: string | null,
): LessonPlanDocument { ): LessonPlanDocument {
if (isV3Document(content)) return content; if (isV4Document(content)) return content;
if (isV3Document(content)) return migrateV3ToV4(content).doc;
if (isV2Document(content)) { if (isV2Document(content)) {
return migrateV2ToV3(content, chapterId, chapterContent); return migrateV3ToV4(migrateV2ToV3(content, chapterId, chapterContent)).doc;
} }
if (isV1Document(content)) { if (isV1Document(content)) {
return migrateV2ToV3(migrateV1ToV2(content), chapterId, chapterContent); return migrateV3ToV4(
migrateV2ToV3(migrateV1ToV2(content), chapterId, chapterContent),
).doc;
} }
// 空文档:创建一个无正文的 v3 // 空文档:创建一个无正文的 v4
const textbookContentNodeId = createId(); const textbookContentNodeId = createId();
const textbookNode: TextbookContentNode = { const textbookNode: TextbookContentNode = {
id: textbookContentNodeId, id: textbookContentNodeId,
@@ -173,16 +233,17 @@ export function normalizeDocument(
draggable: false, draggable: false,
}; };
return { return {
version: 3, version: 4,
textbookContentNodeId, textbookContentNodeId,
nodes: [textbookNode], nodes: [textbookNode],
edges: [], edges: [],
anchors: [], anchors: [],
expandedNodeIds: [],
}; };
} }
// ---- 模板初始化:根据骨架生成初始 contentv3,无正文节点)---- // ---- 模板初始化:根据骨架生成初始 contentv4,无正文节点)----
// 注意:此函数不创建正文节点,调用方应使用 buildDefaultSkeleton 创建完整 v3 文档 // 注意:此函数不创建正文节点,调用方应使用 buildDefaultSkeleton 创建完整 v4 文档
export function buildInitialContent( export function buildInitialContent(
blocks: TemplateBlockSkeleton[], blocks: TemplateBlockSkeleton[],
): LessonPlanDocument { ): LessonPlanDocument {
@@ -217,11 +278,12 @@ export function buildInitialContent(
} }
return { return {
version: 3, version: 4,
textbookContentNodeId, textbookContentNodeId,
nodes: [textbookNode, ...nodes], nodes: [textbookNode, ...nodes],
edges, edges,
anchors: [], anchors: [],
expandedNodeIds: [],
}; };
} }
@@ -283,11 +345,12 @@ export function buildDefaultSkeleton(
})); }));
return { return {
version: 3, version: 4,
textbookContentNodeId, textbookContentNodeId,
nodes: [textbookNode, ...nodes], nodes: [textbookNode, ...nodes],
edges, edges,
anchors: [], anchors: [],
expandedNodeIds: [],
}; };
} }