feat(lesson-preparation): V3→V4 迁移函数(锚点失效标记 + expandedNodeIds)
This commit is contained in:
@@ -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: [],
|
||||
};
|
||||
}
|
||||
|
||||
// ---- 模板初始化:根据骨架生成初始 content(v3,无正文节点)----
|
||||
// 注意:此函数不创建正文节点,调用方应使用 buildDefaultSkeleton 创建完整 v3 文档
|
||||
// ---- 模板初始化:根据骨架生成初始 content(v4,无正文节点)----
|
||||
// 注意:此函数不创建正文节点,调用方应使用 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: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user