refactor(lesson-preparation): editor-slice 移除画布依赖(V4)

This commit is contained in:
SpecialX
2026-07-04 11:16:05 +08:00
parent 0f4e58d7e1
commit ef1be6d04f

View File

@@ -1,12 +1,9 @@
import type { StateCreator } from "zustand";
import { createId } from "@paralleldrive/cuid2";
import type {
AnchorEdge,
AnchorType,
AnyLessonPlanEdge,
Block,
BlockType,
FlowEdge,
LessonPlanDocument,
LessonPlanNode,
NodeAnchor,
@@ -14,7 +11,6 @@ import type {
TextbookContentNodeData,
} from "../types";
import { defaultDataForType } from "../lib/document-migration";
import { computeAutoLayout } from "../lib/auto-layout";
import type { EditorState } from "./use-lesson-plan-editor";
export interface EditorSlice {
@@ -25,24 +21,27 @@ export interface EditorSlice {
setPlanId: (planId: string) => void;
addNode: (type: BlockType, position?: { x: number; y: number }, title?: string) => string;
updateNode: (id: string, patch: Omit<Partial<Block>, "type">) => void;
updateNodePosition: (id: string, position: { x: number; y: number }) => void;
removeNode: (id: string) => void;
updateTextbookContent: (data: Partial<TextbookContentNodeData>) => void;
getTextbookContentNode: () => TextbookContentNode | undefined;
/**
* V4添加锚点不再传 start/end锚点位置由 Tiptap Mark 内嵌)。
*
* 兼容说明:旧调用方可能传入 { nodeId, type, start, end, textPreview }
* 这些 v3 字段会被忽略,等阶段 H 改造调用方时再清理。
*/
addAnchor: (params: {
nodeId: string;
type: AnchorType;
start: number;
/** @deprecated v3 字段v4 已由 Tiptap Mark 承载,忽略 */
start?: number;
/** @deprecated v3 字段v4 忽略 */
end?: number;
/** @deprecated v3 字段v4 忽略 */
textPreview?: string;
}) => string;
removeAnchor: (anchorId: string) => void;
updateAnchor: (anchorId: string, patch: Partial<NodeAnchor>) => void;
connect: (source: string, target: string) => void;
disconnect: (edgeId: string) => void;
setEdges: (edges: AnyLessonPlanEdge[]) => void;
/** V5-8自动布局使用 dagre 计算并应用新位置 */
autoLayout: (direction?: "TB" | "LR") => void;
}
function reindex(nodes: LessonPlanNode[]): LessonPlanNode[] {
@@ -58,11 +57,12 @@ export const createEditorSlice: StateCreator<
planId: "",
title: "",
doc: {
version: 3,
version: 4,
textbookContentNodeId: "",
nodes: [],
edges: [],
anchors: [],
expandedNodeIds: [],
},
setTitle: (title) => {
@@ -71,7 +71,7 @@ export const createEditorSlice: StateCreator<
},
setPlanId: (planId) => set({ planId }),
addNode: (type, position, title) => {
addNode: (type, _position, title) => {
const id = createId();
const state = get();
state.pushHistory(); // V5-2撤销/重做
@@ -85,10 +85,7 @@ export const createEditorSlice: StateCreator<
title: title ?? type,
data: defaultDataForType(type),
order: nodeCount,
position: position ?? {
x: 80 + (nodeCount % 4) * 280,
y: 80 + Math.floor(nodeCount / 4) * 200,
},
position: { x: 0, y: 0 }, // V4不再使用保留字段兼容
};
set((s) => ({
doc: { ...s.doc, nodes: [...s.doc.nodes, node] },
@@ -115,24 +112,6 @@ export const createEditorSlice: StateCreator<
}));
},
updateNodePosition: (id, position) => {
// V5-2 注:拖拽过程中会产生大量 position 更新,仅在拖拽开始时推一次 history。
// 调用方应在 onDragStart 时调用 pushHistory此处不重复推入。
set((s) => ({
doc: {
...s.doc,
nodes: s.doc.nodes.map((n) =>
n.id === id
? n.type === "textbook_content"
? ({ ...n, position } as TextbookContentNode)
: ({ ...n, position } as LessonPlanNode)
: n,
),
},
isDirty: true,
}));
},
removeNode: (id) => {
get().pushHistory(); // V5-2撤销/重做
set((s) => {
@@ -184,38 +163,19 @@ export const createEditorSlice: StateCreator<
);
},
addAnchor: ({ nodeId, type, start, end, textPreview }) => {
addAnchor: ({ nodeId, type }) => {
const anchorId = createId();
const state = get();
state.pushHistory(); // V5-2撤销/重做
const textbookNodeId = state.doc.textbookContentNodeId;
const anchor: NodeAnchor = {
id: anchorId,
nodeId,
type,
start,
...(end !== undefined ? { end } : {}),
...(textPreview ? { textPreview } : {}),
};
const edge: AnchorEdge = {
id: `ae_${nodeId}_${textbookNodeId}_${anchorId.slice(0, 6)}`,
source: nodeId,
target: textbookNodeId,
type: "anchor",
anchorId,
};
get().pushHistory(); // V5-2撤销/重做
set((s) => ({
doc: {
...s.doc,
anchors: [...s.doc.anchors, anchor],
edges: [...s.doc.edges, edge],
anchors: [
...s.doc.anchors,
{ id: anchorId, nodeId, type },
],
},
isDirty: true,
}));
return anchorId;
},
@@ -245,61 +205,4 @@ export const createEditorSlice: StateCreator<
isDirty: true,
}));
},
connect: (source, target) => {
get().pushHistory(); // V5-2撤销/重做
set((s) => {
if (
s.doc.edges.some((e) => e.source === source && e.target === target)
)
return s;
const edge: FlowEdge = {
id: `e_${source}_${target}_${createId().slice(0, 6)}`,
source,
target,
type: "flow",
};
return {
doc: { ...s.doc, edges: [...s.doc.edges, edge] },
isDirty: true,
};
});
},
disconnect: (edgeId) => {
get().pushHistory(); // V5-2撤销/重做
set((s) => ({
doc: {
...s.doc,
edges: s.doc.edges.filter((e) => e.id !== edgeId),
},
isDirty: true,
}));
},
setEdges: (edges) => {
get().pushHistory(); // V5-2撤销/重做
set((s) => ({ doc: { ...s.doc, edges }, isDirty: true }));
},
autoLayout: (direction = "TB") => {
const state = get();
const positions = computeAutoLayout(state.doc.nodes, state.doc.edges, { direction });
if (positions.size === 0) return;
state.pushHistory(); // V5-2撤销/重做
set((s) => ({
doc: {
...s.doc,
nodes: s.doc.nodes.map((n) => {
const p = positions.get(n.id);
return p
? n.type === "textbook_content"
? ({ ...n, position: p } as TextbookContentNode)
: ({ ...n, position: p } as LessonPlanNode)
: n;
}),
},
isDirty: true,
}));
},
});