refactor(lesson-preparation): editor-slice 移除画布依赖(V4)
This commit is contained in:
@@ -1,12 +1,9 @@
|
|||||||
import type { StateCreator } from "zustand";
|
import type { StateCreator } from "zustand";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import type {
|
import type {
|
||||||
AnchorEdge,
|
|
||||||
AnchorType,
|
AnchorType,
|
||||||
AnyLessonPlanEdge,
|
|
||||||
Block,
|
Block,
|
||||||
BlockType,
|
BlockType,
|
||||||
FlowEdge,
|
|
||||||
LessonPlanDocument,
|
LessonPlanDocument,
|
||||||
LessonPlanNode,
|
LessonPlanNode,
|
||||||
NodeAnchor,
|
NodeAnchor,
|
||||||
@@ -14,7 +11,6 @@ import type {
|
|||||||
TextbookContentNodeData,
|
TextbookContentNodeData,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { defaultDataForType } from "../lib/document-migration";
|
import { defaultDataForType } from "../lib/document-migration";
|
||||||
import { computeAutoLayout } from "../lib/auto-layout";
|
|
||||||
import type { EditorState } from "./use-lesson-plan-editor";
|
import type { EditorState } from "./use-lesson-plan-editor";
|
||||||
|
|
||||||
export interface EditorSlice {
|
export interface EditorSlice {
|
||||||
@@ -25,24 +21,27 @@ export interface EditorSlice {
|
|||||||
setPlanId: (planId: string) => void;
|
setPlanId: (planId: string) => void;
|
||||||
addNode: (type: BlockType, position?: { x: number; y: number }, title?: string) => string;
|
addNode: (type: BlockType, position?: { x: number; y: number }, title?: string) => string;
|
||||||
updateNode: (id: string, patch: Omit<Partial<Block>, "type">) => void;
|
updateNode: (id: string, patch: Omit<Partial<Block>, "type">) => void;
|
||||||
updateNodePosition: (id: string, position: { x: number; y: number }) => void;
|
|
||||||
removeNode: (id: string) => void;
|
removeNode: (id: string) => void;
|
||||||
updateTextbookContent: (data: Partial<TextbookContentNodeData>) => void;
|
updateTextbookContent: (data: Partial<TextbookContentNodeData>) => void;
|
||||||
getTextbookContentNode: () => TextbookContentNode | undefined;
|
getTextbookContentNode: () => TextbookContentNode | undefined;
|
||||||
|
/**
|
||||||
|
* V4:添加锚点(不再传 start/end,锚点位置由 Tiptap Mark 内嵌)。
|
||||||
|
*
|
||||||
|
* 兼容说明:旧调用方可能传入 { nodeId, type, start, end, textPreview },
|
||||||
|
* 这些 v3 字段会被忽略,等阶段 H 改造调用方时再清理。
|
||||||
|
*/
|
||||||
addAnchor: (params: {
|
addAnchor: (params: {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
type: AnchorType;
|
type: AnchorType;
|
||||||
start: number;
|
/** @deprecated v3 字段,v4 已由 Tiptap Mark 承载,忽略 */
|
||||||
|
start?: number;
|
||||||
|
/** @deprecated v3 字段,v4 忽略 */
|
||||||
end?: number;
|
end?: number;
|
||||||
|
/** @deprecated v3 字段,v4 忽略 */
|
||||||
textPreview?: string;
|
textPreview?: string;
|
||||||
}) => string;
|
}) => string;
|
||||||
removeAnchor: (anchorId: string) => void;
|
removeAnchor: (anchorId: string) => void;
|
||||||
updateAnchor: (anchorId: string, patch: Partial<NodeAnchor>) => 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[] {
|
function reindex(nodes: LessonPlanNode[]): LessonPlanNode[] {
|
||||||
@@ -58,11 +57,12 @@ export const createEditorSlice: StateCreator<
|
|||||||
planId: "",
|
planId: "",
|
||||||
title: "",
|
title: "",
|
||||||
doc: {
|
doc: {
|
||||||
version: 3,
|
version: 4,
|
||||||
textbookContentNodeId: "",
|
textbookContentNodeId: "",
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
anchors: [],
|
anchors: [],
|
||||||
|
expandedNodeIds: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
setTitle: (title) => {
|
setTitle: (title) => {
|
||||||
@@ -71,7 +71,7 @@ export const createEditorSlice: StateCreator<
|
|||||||
},
|
},
|
||||||
setPlanId: (planId) => set({ planId }),
|
setPlanId: (planId) => set({ planId }),
|
||||||
|
|
||||||
addNode: (type, position, title) => {
|
addNode: (type, _position, title) => {
|
||||||
const id = createId();
|
const id = createId();
|
||||||
const state = get();
|
const state = get();
|
||||||
state.pushHistory(); // V5-2:撤销/重做
|
state.pushHistory(); // V5-2:撤销/重做
|
||||||
@@ -85,10 +85,7 @@ export const createEditorSlice: StateCreator<
|
|||||||
title: title ?? type,
|
title: title ?? type,
|
||||||
data: defaultDataForType(type),
|
data: defaultDataForType(type),
|
||||||
order: nodeCount,
|
order: nodeCount,
|
||||||
position: position ?? {
|
position: { x: 0, y: 0 }, // V4:不再使用,保留字段兼容
|
||||||
x: 80 + (nodeCount % 4) * 280,
|
|
||||||
y: 80 + Math.floor(nodeCount / 4) * 200,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
doc: { ...s.doc, nodes: [...s.doc.nodes, node] },
|
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) => {
|
removeNode: (id) => {
|
||||||
get().pushHistory(); // V5-2:撤销/重做
|
get().pushHistory(); // V5-2:撤销/重做
|
||||||
set((s) => {
|
set((s) => {
|
||||||
@@ -184,38 +163,19 @@ export const createEditorSlice: StateCreator<
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
addAnchor: ({ nodeId, type, start, end, textPreview }) => {
|
addAnchor: ({ nodeId, type }) => {
|
||||||
const anchorId = createId();
|
const anchorId = createId();
|
||||||
const state = get();
|
get().pushHistory(); // V5-2:撤销/重做
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
doc: {
|
doc: {
|
||||||
...s.doc,
|
...s.doc,
|
||||||
anchors: [...s.doc.anchors, anchor],
|
anchors: [
|
||||||
edges: [...s.doc.edges, edge],
|
...s.doc.anchors,
|
||||||
|
{ id: anchorId, nodeId, type },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return anchorId;
|
return anchorId;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -245,61 +205,4 @@ export const createEditorSlice: StateCreator<
|
|||||||
isDirty: true,
|
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,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user