@next-bricks/diagram 0.68.7 → 0.68.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/bricks.json +3 -3
  2. package/dist/chunks/1265.177053f1.js +3 -0
  3. package/dist/chunks/1265.177053f1.js.map +1 -0
  4. package/dist/chunks/577.c9c65352.js.map +1 -1
  5. package/dist/chunks/editable-label.667b04d5.js.map +1 -1
  6. package/dist/chunks/eo-diagram.78450578.js.map +1 -1
  7. package/dist/chunks/eo-display-canvas.2a43ce91.js.map +1 -1
  8. package/dist/chunks/eo-draw-canvas.e01342d6.js.map +1 -1
  9. package/dist/chunks/experimental-node.2f4d802a.js.map +1 -1
  10. package/dist/chunks/{main.57e2b94a.js → main.9ba966bb.js} +2 -2
  11. package/dist/chunks/{main.57e2b94a.js.map → main.9ba966bb.js.map} +1 -1
  12. package/dist/examples.json +9 -6
  13. package/dist/{index.cfc9b630.js → index.31ee50ee.js} +2 -2
  14. package/dist/{index.cfc9b630.js.map → index.31ee50ee.js.map} +1 -1
  15. package/dist/manifest.json +220 -81
  16. package/dist/types.json +78 -78
  17. package/dist-types/diagram/index.d.ts +64 -1
  18. package/dist-types/display-canvas/index.d.ts +48 -4
  19. package/dist-types/draw-canvas/index.d.ts +119 -5
  20. package/dist-types/editable-label/index.d.ts +22 -1
  21. package/dist-types/experimental-node/index.d.ts +21 -1
  22. package/docs/editable-label.md +71 -1
  23. package/docs/editable-label.react.md +100 -0
  24. package/docs/eo-diagram.md +54 -87
  25. package/docs/eo-diagram.react.md +399 -0
  26. package/docs/eo-display-canvas.md +60 -21
  27. package/docs/eo-display-canvas.react.md +376 -0
  28. package/docs/eo-draw-canvas.md +95 -40
  29. package/docs/eo-draw-canvas.react.md +989 -0
  30. package/docs/experimental-node.md +156 -0
  31. package/docs/experimental-node.react.md +157 -0
  32. package/package.json +2 -2
  33. package/dist/chunks/1265.55a02b5a.js +0 -3
  34. package/dist/chunks/1265.55a02b5a.js.map +0 -1
  35. /package/dist/chunks/{1265.55a02b5a.js.LICENSE.txt → 1265.177053f1.js.LICENSE.txt} +0 -0
@@ -0,0 +1,989 @@
1
+ ---
2
+ tagName: eo-draw-canvas
3
+ displayName: WrappedEoDrawCanvas
4
+ description: "用于手工绘图的画布构件,支持节点拖放、连线绘制、元素移动/缩放/删除等交互操作,配合展示画布(eo-display-canvas)使用。"
5
+ category: diagram
6
+ source: "@next-bricks/diagram"
7
+ ---
8
+
9
+ # WrappedEoDrawCanvas
10
+
11
+ > 用于手工绘图的画布构件,支持节点拖放、连线绘制、元素移动/缩放/删除等交互操作,配合展示画布(eo-display-canvas)使用。
12
+
13
+ ## 导入
14
+
15
+ ```tsx
16
+ import { WrappedEoDrawCanvas } from "@easyops/wrapped-components";
17
+ ```
18
+
19
+ ## Props
20
+
21
+ | 属性 | 类型 | 必填 | 默认值 | 说明 |
22
+ | ----------------------------------- | ------------------------------ | ---- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | cells | `InitialCell[]` | - | - | 初始化画布单元格数据,包含节点(node)、边(edge)和装饰器(decorator)。仅当初始化时使用,渲染后重新设置 `cells` 将无效,请使用 `updateCells` 方法代替。 |
24
+ | layout | `LayoutType` | ✅ | - | 画布布局类型,支持 `manual`(手动定位)、`force`(力导向)、`dagre`(层次有向图)。 |
25
+ | layoutOptions | `LayoutOptions` | - | - | 布局算法选项,根据 layout 类型不同,支持不同参数(如 dagre 的 ranksep/nodesep 等)。 |
26
+ | defaultNodeSize | `SizeTuple` | - | `[20, 20]` | 节点默认尺寸,格式为 `[width, height]`,在节点未指定尺寸时使用。 |
27
+ | defaultNodeBricks | `NodeBrickConf[]` | - | - | 节点默认砖块配置,指定渲染节点的自定义构件,可按节点类型匹配不同配置。 |
28
+ | degradedThreshold | `number` | - | - | 当节点数量达到或超过 `degradedThreshold` 时,节点将被降级展示。 |
29
+ | degradedNodeLabel | `string` | - | - | 设置节点将降级展示时显示的名称。 |
30
+ | defaultEdgeLines | `EdgeLineConf[]` | - | - | 使用条件判断设置默认的边对应的连线。在 `if` 表达式中 `DATA` 为 `{ edge }`。 |
31
+ | activeTarget | `ActiveTarget \| null` | - | - | 当前激活目标,可以是节点、边或装饰器,为 null 表示无激活目标。 |
32
+ | fadeUnrelatedCells | `boolean` | - | - | 当 `activeTarget` 不为 `null` 时,隐藏其他跟该 `activeTarget` 无关的元素,高亮相关节点和边。 |
33
+ | zoomable | `boolean` | - | `true` | 是否允许通过鼠标滚轮或触控板捏合手势缩放画布,默认为 true。 |
34
+ | scrollable | `boolean` | - | `true` | 是否允许通过滚轮平移画布(非捏合手势),默认为 true。 |
35
+ | pannable | `boolean` | - | `true` | 是否允许通过鼠标拖拽平移画布,默认为 true。 |
36
+ | allowEdgeToArea | `boolean` | - | `false` | 是否允许将边连接到区域(area)装饰器,默认为 false。 |
37
+ | dragBehavior | `DragBehavior` | - | - | 按住鼠标拖动时的行为:`none`(无)、`lasso`(绘制选区)、`grab`(拖动画布)。 |
38
+ | ctrlDragBehavior | `CtrlDragBehavior` | - | - | 按住 ctrl 键并按住鼠标拖动时的行为:`none`(无)、`grab`(拖动画布)。 |
39
+ | scaleRange | `RangeTuple` | - | - | 缩放比例范围,格式为 `[min, max]`,默认范围由内部常量决定。 |
40
+ | lineSettings | `LineSettings` | - | - | 连线设置,包含连线类型、箭头等属性,用于新建连线时的默认样式。 |
41
+ | lineConnector | `LineConnecterConf \| boolean` | - | - | 连线连接器配置,设置为 `true` 或配置对象以启用智能连线功能,允许从节点边缘拖出连线。 |
42
+ | doNotResetActiveTargetForSelector | `string` | - | - | 选择器,点击该选择器对应的元素时不重置 `activeTarget`。 |
43
+ | doNotResetActiveTargetOutsideCanvas | `boolean` | - | - | 在画布外点击时不重置 `activeTarget`。 |
44
+
45
+ ## Events
46
+
47
+ | 事件 | detail | 说明 |
48
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
49
+ | onActiveTargetChange | `ActiveTarget \| null` — 当前激活目标,节点/边/装饰器对象或 null | 激活目标变化时触发,当用户点击节点、边或装饰器使其激活,或点击空白处取消激活时触发。 |
50
+ | onNodeMove | `MoveCellPayload` — 移动的节点信息,包含节点 id 和新位置 | 节点被拖拽移动后触发(已废弃,请使用 `onCellMove`)。 |
51
+ | onCellMove | `MoveCellPayload` — 移动的单元格信息,包含单元格 id、类型和新位置 | 单个单元格(节点或装饰器)被拖拽移动后触发。 |
52
+ | onCellsMove | `MoveCellPayload[]` — 移动的多个单元格信息列表 | 多个单元格(通过框选后拖拽)同时被移动后触发。 |
53
+ | onCellResize | `ResizeCellPayload` — 调整大小的单元格信息,包含单元格 id 和新尺寸 | 单元格(节点或装饰器)被手动调整大小后触发。 |
54
+ | onNodeDelete | `Cell` — 被删除的节点 cell 对象 | 节点被删除时触发(已废弃,请使用 `onCellDelete`)。 |
55
+ | onCellDelete | `Cell` — 被删除的单元格对象,包含节点、边或装饰器 | 单个单元格被删除时触发(用户按 Delete 键或通过菜单删除)。 |
56
+ | onCellsDelete | `Cell[]` — 被批量删除的单元格对象列表 | 多个单元格被同时删除时触发(框选后批量删除)。 |
57
+ | onCellContextmenu | `CellContextMenuDetail` — 右键菜单详情,包含 `{ cell: 对应的单元格, clientX: 鼠标X坐标, clientY: 鼠标Y坐标 }` | 用户右键点击节点、边或装饰器时触发,常用于弹出上下文菜单。 |
58
+ | onEdgeAdd | `ConnectNodesDetail` — 新边详情,包含 `{ source: 起始节点 id, target: 目标节点 id }` | 通过画布绘图的方式添加边时触发(手动调用 `addEdge` 方法不会触发该事件)。 |
59
+ | onEdgeViewChange | `EdgeViewChangePayload` — 边视图变更详情,包含边 id 和新的视图属性 | 用户通过拖拽手柄修改连线路径或形状时触发。 |
60
+ | onDecoratorViewChange | `DecoratorViewChangePayload` — 装饰器视图变更详情,包含装饰器 id 和新的位置/尺寸 | 装饰器(area、container、text 等)被移动或调整大小时触发。 |
61
+ | onDecoratorTextChange | `DecoratorTextChangeDetail` — 文本变更详情,包含装饰器 id 和新的文本内容 | 装饰器文本(area/container/text 的文字)被编辑并确认后触发。 |
62
+ | onNodeContainerChange | `MoveCellPayload[]` — 节点与容器关系变更详情列表,有 containerCell 则为新增关系,否则为删除关系 | 节点与容器组(container 装饰器)的包含关系发生变化时触发,包括拖入、拖出容器。 |
63
+ | onDecoratorGroupPlusClick | `DecoratorCell` — 被点击加号按钮的分组容器 cell 对象 | 分组容器(group 装饰器)的加号按钮被点击时触发,用于触发在组内添加新节点的逻辑。 |
64
+ | onScaleChange | `number` — 当前缩放比例值(如 1.0 表示 100%) | 画布缩放比例变化时触发,从素材库拖拽元素进画布时,拖拽图像应设置对应的缩放比例。 |
65
+ | onCanvasContextmenu | `CanvasContextMenuDetail` — 右键菜单详情,包含 `{ clientX: 鼠标X坐标, clientY: 鼠标Y坐标, view: 画布坐标 { x, y } }` | 用户在画布空白处右键点击时触发,常用于弹出画布级别的上下文菜单。 |
66
+ | onCanvasCopy | `void` | 用户触发复制操作(Ctrl+C)时触发,外部需自行处理复制逻辑。 |
67
+ | onCanvasPaste | `void` | 用户触发粘贴操作(Ctrl+V)时触发,外部需自行处理粘贴逻辑。 |
68
+ | onCanvasGroup | `void` | 用户触发分组操作(Ctrl+G)时触发,外部需自行处理分组逻辑。 |
69
+ | onCanvasUngroup | `void` | 用户触发取消分组操作(Ctrl+Shift+G)时触发,外部需自行处理解组逻辑。 |
70
+
71
+ ## Methods
72
+
73
+ | 方法 | 参数 | 返回值 | 说明 |
74
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
75
+ | dropNode | <ul><li>`info: DropNodeInfo` - 拖放节点信息,包含节点 id、拖放位置(clientX/clientY)、尺寸和数据</li></ul> | `Promise<NodeCell \| null>` | 将一个节点拖放到画布中指定位置。如果放置位置不在画布内,则返回 null。 |
76
+ | dropDecorator | <ul><li>`info: DropDecoratorInfo` - 拖放装饰器信息,包含装饰器类型、拖放位置、文本和方向等</li></ul> | `Promise<DecoratorCell \| null>` | 将一个装饰器(area、container、text、line 等)拖放到画布中指定位置。如果放置位置不在画布内,则返回 null。 |
77
+ | addNodes | <ul><li>`nodes: AddNodeInfo[]` - 要添加的节点信息列表,每项包含 id、数据、尺寸等</li></ul> | `Promise<NodeCell[]>` | 批量添加节点到画布,节点位置由布局算法自动计算。 |
78
+ | addEdge | <ul><li>`info: AddEdgeInfo` - 边信息,包含 source(起始节点 id)、target(目标节点 id)和可选的 data</li></ul> | `Promise<EdgeCell>` | 以编程方式添加一条边(连线)到画布。注意:此方法不会触发 `onEdgeAdd` 事件。 |
79
+ | manuallyConnectNodes | <ul><li>`source: NodeId` - 起始节点的 id</li></ul> | `Promise<ConnectNodesDetail>` | 以编程方式启动从指定源节点到目标节点的手动连线流程,等待用户在画布上点击目标节点后返回连线详情。 |
80
+ | updateCells | <ul><li>`cells: InitialCell[]` - 新的单元格数据列表</li><li>`ctx?: UpdateCellsContext` - 可选的更新上下文,用于指定更新原因和位置参考节点</li></ul> | `Promise<{ updated: Cell[] }>` | 更新画布中的单元格数据,支持增量更新(新增、修改),已渲染的画布使用此方法代替直接设置 `cells` 属性。 |
81
+ | reCenter | - | `void` | 将画布视图重置并居中,使所有单元格重新显示在视口中央。 |
82
+ | toggleLock | <ul><li>`target: ActiveTarget` - 当前选中的目标</li></ul> | `Promise<Cell[] \| null>` | 切换锁定状态。如果目标中包含未锁定且可以锁定的元素,则将这些元素锁定;否则,如果目标中包含已锁定且可以解锁的元素,则将这些元素解锁。 |
83
+ | lock | <ul><li>`target: ActiveTarget` - 当前选中的目标</li></ul> | `Promise<Cell[] \| null>` | 锁定选中的目标。规则类似 `toggleLock`,但仅执行锁定操作。 |
84
+ | unlock | <ul><li>`target: ActiveTarget` - 当前选中的目标</li></ul> | `Promise<Cell[] \| null>` | 解锁选中的目标。规则类似 `toggleLock`,但仅执行解锁操作。 |
85
+
86
+ ## Examples
87
+
88
+ ### Basic
89
+
90
+ 基础绘图画布示例,展示手动布局(manual)下的节点拖放、连线绘制、装饰器添加、右键菜单等交互功能。
91
+
92
+ ```tsx
93
+ import { useState, useRef, useCallback } from "react";
94
+ import {
95
+ WrappedEoDrawCanvas,
96
+ WrappedDiagramExperimentalNode,
97
+ WrappedEoButton,
98
+ WrappedEoContextMenu,
99
+ } from "@easyops/wrapped-components";
100
+
101
+ function DrawCanvasBasicExample() {
102
+ const canvasRef = useRef<any>();
103
+ const contextMenuRef = useRef<any>();
104
+ const [activeTarget, setActiveTarget] = useState<any>(null);
105
+ const [contextMenuDetail, setContextMenuDetail] = useState<any>(null);
106
+ const [dragging, setDragging] = useState<any>(null);
107
+ const [scale, setScale] = useState(1);
108
+
109
+ const initialCells = [
110
+ {
111
+ type: "decorator",
112
+ id: "container-1",
113
+ decorator: "container",
114
+ view: { direction: "left", text: "上层服务", level: 1 },
115
+ },
116
+ {
117
+ type: "decorator",
118
+ id: "container-2",
119
+ decorator: "container",
120
+ view: { direction: "left", text: "应用", level: 2 },
121
+ },
122
+ {
123
+ type: "decorator",
124
+ id: "group-1",
125
+ decorator: "group",
126
+ containerId: "container-2",
127
+ view: { usePlus: true },
128
+ },
129
+ {
130
+ type: "node",
131
+ id: "G",
132
+ groupId: "group-1",
133
+ data: { name: "Node G" },
134
+ view: { width: 60, height: 60 },
135
+ },
136
+ { type: "edge", source: "X", target: "Y" },
137
+ { type: "edge", source: "Z", target: "W" },
138
+ { type: "edge", source: "X", target: "Z", data: { virtual: true } },
139
+ ...["A", "B", "C", "S", "D", "F", "X", "Y", "Z", "W"].map((id) => ({
140
+ type: "node",
141
+ id,
142
+ containerId: ["W", "Z", "X", "Y"].includes(id)
143
+ ? "container-1"
144
+ : ["A", "B"].includes(id)
145
+ ? "container-2"
146
+ : null,
147
+ groupId: ["C", "S", "D", "F"].includes(id) ? "group-1" : null,
148
+ data: { name: `Node ${id}` },
149
+ view: {
150
+ x: ["A", "B", "C", "S", "D", "F", "Z", "X", "Y"].includes(id)
151
+ ? null
152
+ : Math.round(300 + Math.random() * 300),
153
+ y: ["A", "B", "C", "S", "D", "F", "Z", "X", "Y"].includes(id)
154
+ ? null
155
+ : Math.round(300 + Math.random() * 200),
156
+ width: 60,
157
+ height: 60,
158
+ },
159
+ })),
160
+ {
161
+ type: "decorator",
162
+ id: "text-1",
163
+ decorator: "text",
164
+ view: {
165
+ x: 300,
166
+ y: 120,
167
+ width: 100,
168
+ height: 20,
169
+ text: "Hello\nWorld!",
170
+ style: { writingMode: "vertical-rl" },
171
+ },
172
+ },
173
+ ];
174
+
175
+ const contextMenuActions = !contextMenuDetail
176
+ ? []
177
+ : contextMenuDetail.target?.type === "multi"
178
+ ? [{ text: "锁定/取消锁定", event: "toggle-lock" }]
179
+ : [
180
+ ...(contextMenuDetail.locked
181
+ ? []
182
+ : [
183
+ { text: "添加边", event: "add-edge" },
184
+ { text: "移除", event: "remove" },
185
+ ]),
186
+ { text: "锁定/取消锁定", event: "toggle-lock" },
187
+ ].filter(
188
+ (action) =>
189
+ contextMenuDetail.cell.type === "node" ||
190
+ (contextMenuDetail.cell.type === "decorator" &&
191
+ contextMenuDetail.cell.decorator === "area") ||
192
+ action.event !== "add-edge"
193
+ );
194
+
195
+ return (
196
+ <div style={{ display: "flex", height: 600, gap: "1em" }}>
197
+ <div
198
+ style={{
199
+ width: 200,
200
+ display: "flex",
201
+ flexDirection: "column",
202
+ gap: "1em",
203
+ borderRight: "1px solid var(--palette-gray-6)",
204
+ overflow: "scroll",
205
+ }}
206
+ >
207
+ <WrappedEoButton
208
+ textContent="Add random nodes"
209
+ onClick={() => {
210
+ canvasRef.current?.addNodes(
211
+ [1, 2, 3].map(() => ({
212
+ id: Math.round(Math.random() * 1e6),
213
+ data: { name: String(Math.round(Math.random() * 1e6)) },
214
+ }))
215
+ );
216
+ }}
217
+ />
218
+ <WrappedEoButton
219
+ textContent="Re-center"
220
+ onClick={() => canvasRef.current?.reCenter()}
221
+ />
222
+ <WrappedEoButton
223
+ textContent="Add edge: Y => Z"
224
+ onClick={() =>
225
+ canvasRef.current?.addEdge({
226
+ source: "Y",
227
+ target: "Z",
228
+ data: { virtual: true },
229
+ })
230
+ }
231
+ />
232
+ </div>
233
+ <div style={{ flex: 1, minWidth: 0 }}>
234
+ <WrappedEoDrawCanvas
235
+ ref={canvasRef}
236
+ style={{ width: "100%", height: "100%" }}
237
+ activeTarget={activeTarget}
238
+ fadeUnrelatedCells={true}
239
+ dragBehavior="lasso"
240
+ layoutOptions={{ snap: { object: true } }}
241
+ defaultNodeSize={[60, 60]}
242
+ defaultNodeBricks={[
243
+ {
244
+ useBrick: {
245
+ brick: "diagram.experimental-node",
246
+ properties: { textContent: "<% `Node ${DATA.node.id}` %>" },
247
+ },
248
+ },
249
+ ]}
250
+ defaultEdgeLines={[
251
+ { if: "<% DATA.edge.data?.virtual %>", dashed: true },
252
+ {
253
+ if: "<% !DATA.edge.data?.virtual %>",
254
+ dotted: true,
255
+ showStartArrow: true,
256
+ markers: [
257
+ { placement: "start", type: "circle" },
258
+ { placement: "end", type: "arrow" },
259
+ ],
260
+ },
261
+ ]}
262
+ cells={initialCells}
263
+ lineConnector={true}
264
+ lineSettings={{ type: "polyline" }}
265
+ onActiveTargetChange={(e: any) => setActiveTarget(e.detail)}
266
+ onCellsMove={(e: any) =>
267
+ console.log(`You just moved ${e.detail.length} cells`)
268
+ }
269
+ onCellResize={(e: any) =>
270
+ console.log(
271
+ `You just resized ${e.detail.type} ${e.detail.id} to (${Math.round(e.detail.width)}, ${Math.round(e.detail.height)})`
272
+ )
273
+ }
274
+ onCellsDelete={(e: any) =>
275
+ console.log(`You wanna delete ${e.detail.length} cells?`)
276
+ }
277
+ onCellContextmenu={(e: any) => {
278
+ contextMenuRef.current?.open({
279
+ position: [e.detail.clientX, e.detail.clientY],
280
+ });
281
+ setContextMenuDetail(e.detail);
282
+ }}
283
+ onEdgeAdd={(e: any) =>
284
+ console.log(`Added an nice edge: ${JSON.stringify(e.detail)}`)
285
+ }
286
+ onEdgeViewChange={(e: any) =>
287
+ console.log(`Edge view changed: ${JSON.stringify(e.detail)}`)
288
+ }
289
+ onDecoratorViewChange={(e: any) =>
290
+ console.log(`Decorator view changed: ${JSON.stringify(e.detail)}`)
291
+ }
292
+ onDecoratorTextChange={(e: any) =>
293
+ console.log(JSON.stringify(e.detail))
294
+ }
295
+ onNodeContainerChange={(e: any) =>
296
+ console.log(JSON.stringify(e.detail))
297
+ }
298
+ onScaleChange={(e: any) => setScale(e.detail)}
299
+ />
300
+ </div>
301
+ <WrappedDiagramExperimentalNode
302
+ usage="dragging"
303
+ textContent={
304
+ dragging?.type === "decorator"
305
+ ? dragging.decorator === "text"
306
+ ? "Text"
307
+ : null
308
+ : dragging?.data.name
309
+ }
310
+ decorator={dragging?.type === "decorator" ? dragging.decorator : null}
311
+ style={{
312
+ left: `${dragging?.position[0]}px`,
313
+ top: `${dragging?.position[1]}px`,
314
+ transform: `scale(${scale})`,
315
+ transformOrigin: "0 0",
316
+ padding: dragging?.decorator === "text" ? "0.5em" : "0",
317
+ }}
318
+ hidden={!dragging}
319
+ />
320
+ <WrappedEoContextMenu
321
+ ref={contextMenuRef}
322
+ actions={contextMenuActions}
323
+ onRemove={() => {
324
+ canvasRef.current?.updateCells(
325
+ initialCells.filter(
326
+ (cell: any) =>
327
+ !(contextMenuDetail.cell.type === "edge"
328
+ ? cell.type === "edge" &&
329
+ contextMenuDetail.cell.source === cell.source &&
330
+ contextMenuDetail.cell.target === cell.target
331
+ : cell.id === contextMenuDetail.cell.id ||
332
+ (cell.type === "edge" &&
333
+ (contextMenuDetail.cell.id === cell.source ||
334
+ contextMenuDetail.cell.id === cell.target)))
335
+ )
336
+ );
337
+ }}
338
+ onAddEdge={async () => {
339
+ const detail = await canvasRef.current?.manuallyConnectNodes(
340
+ contextMenuDetail.cell.id
341
+ );
342
+ if (detail) {
343
+ canvasRef.current?.addEdge({
344
+ source: detail.source.id,
345
+ target: detail.target.id,
346
+ });
347
+ }
348
+ }}
349
+ onToggleLock={async () => {
350
+ const result = await canvasRef.current?.toggleLock(
351
+ contextMenuDetail.target
352
+ );
353
+ console.log("Updated cells after toggle lock:", result);
354
+ }}
355
+ />
356
+ </div>
357
+ );
358
+ }
359
+ ```
360
+
361
+ ### Line labels
362
+
363
+ 设置连线文字。
364
+
365
+ ```tsx
366
+ import { useState, useRef } from "react";
367
+ import { WrappedEoDrawCanvas } from "@easyops/wrapped-components";
368
+
369
+ function DrawCanvasLineLabelExample() {
370
+ const [activeTarget, setActiveTarget] = useState<any>(null);
371
+ const [scale, setScale] = useState(1);
372
+ const [initialCells, setInitialCells] = useState([
373
+ {
374
+ type: "edge",
375
+ source: "X",
376
+ target: "Y",
377
+ description: "X->Y",
378
+ placement: "end",
379
+ view: { type: "polyline" },
380
+ },
381
+ { type: "edge", source: "X", target: "Z" },
382
+ {
383
+ type: "node",
384
+ id: "X",
385
+ data: { name: "Node X" },
386
+ view: { x: 100, y: 100, width: 60, height: 60 },
387
+ },
388
+ {
389
+ type: "node",
390
+ id: "Y",
391
+ data: { name: "Node Y" },
392
+ view: { x: 0, y: 300, width: 60, height: 60 },
393
+ },
394
+ {
395
+ type: "node",
396
+ id: "Z",
397
+ data: { name: "Node Z" },
398
+ view: { x: 300, y: 200, width: 60, height: 60 },
399
+ },
400
+ ]);
401
+
402
+ return (
403
+ <div
404
+ style={{
405
+ display: "flex",
406
+ flexDirection: "column",
407
+ height: 600,
408
+ gap: "1em",
409
+ }}
410
+ >
411
+ <WrappedEoDrawCanvas
412
+ style={{ width: "100%", height: "100%" }}
413
+ activeTarget={activeTarget}
414
+ fadeUnrelatedCells={true}
415
+ dragBehavior="lasso"
416
+ layoutOptions={{ snap: { object: true } }}
417
+ defaultNodeSize={[60, 60]}
418
+ defaultNodeBricks={[
419
+ {
420
+ useBrick: {
421
+ brick: "diagram.experimental-node",
422
+ properties: { textContent: "<% `Node ${DATA.node.id}` %>" },
423
+ },
424
+ },
425
+ ]}
426
+ cells={initialCells}
427
+ lineConnector={true}
428
+ defaultEdgeLines={[
429
+ {
430
+ callLabelOnDoubleClick: "enableEditing",
431
+ label: {
432
+ placement: "<% DATA.edge.placement %>",
433
+ offset: 10,
434
+ useBrick: {
435
+ brick: "diagram.editable-label",
436
+ properties: {
437
+ label: "<% DATA.edge.description %>",
438
+ type: "line",
439
+ },
440
+ },
441
+ },
442
+ },
443
+ ]}
444
+ onActiveTargetChange={(e: any) => setActiveTarget(e.detail)}
445
+ onCellDelete={(e: any) =>
446
+ console.log(
447
+ `You wanna delete ${e.detail.type} ${e.detail.type === "edge" ? `(${e.detail.source} => ${e.detail.target})` : e.detail.id}?`
448
+ )
449
+ }
450
+ onScaleChange={(e: any) => setScale(e.detail)}
451
+ />
452
+ </div>
453
+ );
454
+ }
455
+ ```
456
+
457
+ ### Line settings
458
+
459
+ 设置属性 `lineSettings` 来调整新的连线的样式,例如使用折线或直线。注意,该设置不影响已有的 edge 的连线样式。
460
+
461
+ ```tsx
462
+ import { useState, useRef } from "react";
463
+ import {
464
+ WrappedEoDrawCanvas,
465
+ WrappedEoRadio,
466
+ WrappedDiagramExperimentalNode,
467
+ WrappedEoContextMenu,
468
+ } from "@easyops/wrapped-components";
469
+
470
+ function DrawCanvasLineSettingsExample() {
471
+ const canvasRef = useRef<any>();
472
+ const contextMenuRef = useRef<any>();
473
+ const [activeTarget, setActiveTarget] = useState<any>(null);
474
+ const [targetCell, setTargetCell] = useState<any>(null);
475
+ const [dragging, setDragging] = useState<any>(null);
476
+ const [scale, setScale] = useState(1);
477
+ const [lineType, setLineType] = useState("polyline");
478
+
479
+ const initialCells = [
480
+ {
481
+ type: "decorator",
482
+ decorator: "line",
483
+ id: "line-1",
484
+ view: {
485
+ source: { x: 200, y: 200 },
486
+ target: { x: 250, y: 150 },
487
+ vertices: [{ x: 180, y: 125 }],
488
+ markers: [{ placement: "end", type: "arrow" }],
489
+ },
490
+ },
491
+ { type: "edge", source: "X", target: "Y" },
492
+ {
493
+ type: "node",
494
+ id: "X",
495
+ data: { name: "Node X" },
496
+ view: { x: 100, y: 100, width: 60, height: 60 },
497
+ },
498
+ {
499
+ type: "node",
500
+ id: "Y",
501
+ data: { name: "Node Y" },
502
+ view: { x: 0, y: 300, width: 60, height: 60 },
503
+ },
504
+ {
505
+ type: "node",
506
+ id: "Z",
507
+ data: { name: "Node Z" },
508
+ view: { x: 300, y: 200, width: 60, height: 60 },
509
+ },
510
+ ];
511
+
512
+ return (
513
+ <div
514
+ style={{
515
+ display: "flex",
516
+ flexDirection: "column",
517
+ height: 600,
518
+ gap: "1em",
519
+ }}
520
+ >
521
+ <div>
522
+ <WrappedEoRadio
523
+ type="button"
524
+ value="polyline"
525
+ options={["polyline", "curve", "straight"]}
526
+ onChange={(e: any) => setLineType(e.detail.value)}
527
+ />
528
+ </div>
529
+ <div style={{ flex: 1, minHeight: 0 }}>
530
+ <WrappedEoDrawCanvas
531
+ ref={canvasRef}
532
+ style={{ width: "100%", height: "100%" }}
533
+ activeTarget={activeTarget}
534
+ fadeUnrelatedCells={true}
535
+ dragBehavior="lasso"
536
+ layoutOptions={{ snap: { object: true } }}
537
+ defaultNodeSize={[60, 60]}
538
+ defaultNodeBricks={[
539
+ {
540
+ useBrick: {
541
+ brick: "diagram.experimental-node",
542
+ properties: { textContent: "<% `Node ${DATA.node.id}` %>" },
543
+ },
544
+ },
545
+ ]}
546
+ cells={initialCells}
547
+ defaultEdgeLines={[{ jumps: true }]}
548
+ lineConnector={true}
549
+ lineSettings={{ type: lineType }}
550
+ onActiveTargetChange={(e: any) => setActiveTarget(e.detail)}
551
+ onCellContextmenu={(e: any) => {
552
+ contextMenuRef.current?.open({
553
+ position: [e.detail.clientX, e.detail.clientY],
554
+ });
555
+ setTargetCell(e.detail.cell);
556
+ }}
557
+ onEdgeAdd={(e: any) =>
558
+ console.log(`Added an nice edge: ${JSON.stringify(e.detail)}`)
559
+ }
560
+ onEdgeViewChange={(e: any) =>
561
+ console.log(`Edge view changed: ${JSON.stringify(e.detail)}`)
562
+ }
563
+ onScaleChange={(e: any) => setScale(e.detail)}
564
+ />
565
+ </div>
566
+ <WrappedDiagramExperimentalNode
567
+ usage="dragging"
568
+ textContent={
569
+ dragging?.type === "decorator"
570
+ ? dragging.decorator === "text"
571
+ ? "Text"
572
+ : null
573
+ : dragging?.data.name
574
+ }
575
+ decorator={dragging?.type === "decorator" ? dragging.decorator : null}
576
+ style={{
577
+ left: `${dragging?.position[0]}px`,
578
+ top: `${dragging?.position[1]}px`,
579
+ transform: `scale(${scale})`,
580
+ transformOrigin: "0 0",
581
+ padding: dragging?.decorator === "text" ? "0.5em" : "0",
582
+ }}
583
+ hidden={!dragging}
584
+ />
585
+ <WrappedEoContextMenu
586
+ ref={contextMenuRef}
587
+ actions={
588
+ ["node"].includes(targetCell?.type) ||
589
+ targetCell?.decorator === "area"
590
+ ? [{ text: "添加边", event: "add-edge" }]
591
+ : [
592
+ {
593
+ text: `Test ${targetCell?.type}`,
594
+ event: `test-${targetCell?.type}`,
595
+ },
596
+ ]
597
+ }
598
+ onAddEdge={async () => {
599
+ const detail = await canvasRef.current?.manuallyConnectNodes(
600
+ targetCell.id
601
+ );
602
+ if (detail) {
603
+ canvasRef.current?.addEdge({
604
+ source: detail.source.id,
605
+ target: detail.target.id,
606
+ });
607
+ }
608
+ }}
609
+ />
610
+ </div>
611
+ );
612
+ }
613
+ ```
614
+
615
+ ### Force layout
616
+
617
+ 使用力导向(force)布局模式,节点位置由力导向算法自动计算。
618
+
619
+ ```tsx
620
+ import { useState, useRef } from "react";
621
+ import {
622
+ WrappedEoDrawCanvas,
623
+ WrappedEoButton,
624
+ WrappedDiagramExperimentalNode,
625
+ WrappedEoContextMenu,
626
+ } from "@easyops/wrapped-components";
627
+
628
+ function DrawCanvasForceLayoutExample() {
629
+ const canvasRef = useRef<any>();
630
+ const contextMenuRef = useRef<any>();
631
+ const [activeTarget, setActiveTarget] = useState<any>(null);
632
+ const [targetCell, setTargetCell] = useState<any>(null);
633
+ const [dragging, setDragging] = useState<any>(null);
634
+ const [scale, setScale] = useState(1);
635
+
636
+ const initialCells = [
637
+ {
638
+ type: "decorator",
639
+ id: "area-1",
640
+ decorator: "area",
641
+ view: { x: 10, y: 20, width: 400, height: 300 },
642
+ },
643
+ {
644
+ type: "decorator",
645
+ id: "container-1",
646
+ decorator: "container",
647
+ view: {
648
+ x: 50,
649
+ y: 400,
650
+ width: 280,
651
+ height: 120,
652
+ direction: "top",
653
+ text: " 上层服务",
654
+ },
655
+ },
656
+ { type: "edge", source: "X", target: "Y" },
657
+ { type: "edge", source: "X", target: "Z", data: { virtual: true } },
658
+ ...["X", "Y", "Z", "W"].map((id) => ({
659
+ type: "node",
660
+ id,
661
+ containerId: ["X", "Y", "Z"].includes(id) ? "container-1" : undefined,
662
+ data: { name: `Node ${id}` },
663
+ view: { width: 60, height: 60 },
664
+ })),
665
+ {
666
+ type: "decorator",
667
+ id: "text-1",
668
+ decorator: "text",
669
+ view: { x: 100, y: 120, width: 100, height: 20, text: "Hello!" },
670
+ },
671
+ ];
672
+
673
+ return (
674
+ <div style={{ display: "flex", height: 600, gap: "1em" }}>
675
+ <div
676
+ style={{
677
+ width: 180,
678
+ display: "flex",
679
+ flexDirection: "column",
680
+ gap: "1em",
681
+ }}
682
+ >
683
+ <WrappedEoButton
684
+ textContent="Add random nodes"
685
+ onClick={() => {
686
+ canvasRef.current?.addNodes(
687
+ [1, 2, 3].map(() => ({
688
+ id: Math.round(Math.random() * 1e6),
689
+ data: { name: String(Math.round(Math.random() * 1e6)) },
690
+ }))
691
+ );
692
+ }}
693
+ />
694
+ <WrappedEoButton
695
+ textContent="Add edge: Y => Z"
696
+ onClick={() =>
697
+ canvasRef.current?.addEdge({
698
+ source: "Y",
699
+ target: "Z",
700
+ data: { virtual: true },
701
+ })
702
+ }
703
+ />
704
+ </div>
705
+ <div style={{ flex: 1, minWidth: 0 }}>
706
+ <WrappedEoDrawCanvas
707
+ ref={canvasRef}
708
+ style={{ width: "100%", height: "100%" }}
709
+ activeTarget={activeTarget}
710
+ fadeUnrelatedCells={true}
711
+ layout="force"
712
+ defaultNodeSize={[60, 60]}
713
+ defaultNodeBricks={[
714
+ {
715
+ useBrick: {
716
+ brick: "diagram.experimental-node",
717
+ properties: { textContent: "<% `Node ${DATA.node.id}` %>" },
718
+ },
719
+ },
720
+ ]}
721
+ defaultEdgeLines={[
722
+ { if: "<% DATA.edge.data?.virtual %>", dashed: true },
723
+ ]}
724
+ cells={initialCells}
725
+ onActiveTargetChange={(e: any) => setActiveTarget(e.detail)}
726
+ onCellMove={(e: any) =>
727
+ console.log(
728
+ `You just moved ${e.detail.type} ${e.detail.id} to (${Math.round(e.detail.x)}, ${Math.round(e.detail.y)})`
729
+ )
730
+ }
731
+ onCellResize={(e: any) =>
732
+ console.log(
733
+ `You just resized ${e.detail.type} ${e.detail.id} to (${Math.round(e.detail.width)}, ${Math.round(e.detail.height)})`
734
+ )
735
+ }
736
+ onCellDelete={(e: any) =>
737
+ console.log(
738
+ `You wanna delete ${e.detail.type} ${e.detail.type === "edge" ? `(${e.detail.source} => ${e.detail.target})` : e.detail.id}?`
739
+ )
740
+ }
741
+ onCellContextmenu={(e: any) => {
742
+ contextMenuRef.current?.open({
743
+ position: [e.detail.clientX, e.detail.clientY],
744
+ });
745
+ setTargetCell(e.detail.cell);
746
+ }}
747
+ onDecoratorTextChange={(e: any) =>
748
+ console.log(JSON.stringify(e.detail))
749
+ }
750
+ onScaleChange={(e: any) => setScale(e.detail)}
751
+ />
752
+ </div>
753
+ <WrappedDiagramExperimentalNode
754
+ usage="dragging"
755
+ textContent={
756
+ dragging?.type === "decorator"
757
+ ? dragging.decorator === "text"
758
+ ? "Text"
759
+ : null
760
+ : dragging?.data.name
761
+ }
762
+ decorator={dragging?.type === "decorator" ? dragging.decorator : null}
763
+ style={{
764
+ left: `${dragging?.position[0]}px`,
765
+ top: `${dragging?.position[1]}px`,
766
+ transform: `scale(${scale})`,
767
+ transformOrigin: "0 0",
768
+ padding: dragging?.decorator === "text" ? "0.5em" : "0",
769
+ }}
770
+ hidden={!dragging}
771
+ />
772
+ <WrappedEoContextMenu
773
+ ref={contextMenuRef}
774
+ actions={
775
+ targetCell?.type === "node"
776
+ ? [{ text: "添加边", event: "add-edge" }]
777
+ : [
778
+ {
779
+ text: `Test ${targetCell?.type}`,
780
+ event: `test-${targetCell?.type}`,
781
+ },
782
+ ]
783
+ }
784
+ onAddEdge={async () => {
785
+ const detail = await canvasRef.current?.manuallyConnectNodes(
786
+ targetCell.id
787
+ );
788
+ if (detail) {
789
+ canvasRef.current?.addEdge({
790
+ source: detail.source.id,
791
+ target: detail.target.id,
792
+ });
793
+ }
794
+ }}
795
+ />
796
+ </div>
797
+ );
798
+ }
799
+ ```
800
+
801
+ ### Dagre layout
802
+
803
+ 使用层次有向图(dagre)布局模式,节点位置由 dagre 算法自动计算,适合展示有向依赖关系。
804
+
805
+ ```tsx
806
+ import { useState, useRef } from "react";
807
+ import {
808
+ WrappedEoDrawCanvas,
809
+ WrappedEoButton,
810
+ WrappedDiagramExperimentalNode,
811
+ WrappedEoContextMenu,
812
+ } from "@easyops/wrapped-components";
813
+
814
+ function DrawCanvasDagreLayoutExample() {
815
+ const canvasRef = useRef<any>();
816
+ const contextMenuRef = useRef<any>();
817
+ const [activeTarget, setActiveTarget] = useState<any>(null);
818
+ const [targetCell, setTargetCell] = useState<any>(null);
819
+ const [dragging, setDragging] = useState<any>(null);
820
+ const [scale, setScale] = useState(1);
821
+
822
+ const initialCells = [
823
+ {
824
+ type: "decorator",
825
+ id: "area-1",
826
+ decorator: "area",
827
+ view: { x: 10, y: 20, width: 400, height: 300 },
828
+ },
829
+ {
830
+ type: "decorator",
831
+ id: "container-1",
832
+ decorator: "container",
833
+ view: {
834
+ x: 50,
835
+ y: 400,
836
+ width: 280,
837
+ height: 120,
838
+ direction: "top",
839
+ text: " 上层服务",
840
+ },
841
+ },
842
+ { type: "edge", source: "X", target: "Y" },
843
+ { type: "edge", source: "X", target: "Z", data: { virtual: true } },
844
+ { type: "edge", source: "Z", target: "W" },
845
+ ...["X", "Y", "Z", "W"].map((id) => ({
846
+ type: "node",
847
+ id,
848
+ containerId: ["W", "Z"].includes(id) ? "container-1" : undefined,
849
+ data: { name: `Node ${id}` },
850
+ view: { width: 60, height: 60 },
851
+ })),
852
+ ];
853
+
854
+ return (
855
+ <div style={{ display: "flex", height: 600, gap: "1em" }}>
856
+ <div
857
+ style={{
858
+ width: 180,
859
+ display: "flex",
860
+ flexDirection: "column",
861
+ gap: "1em",
862
+ }}
863
+ >
864
+ <WrappedEoButton
865
+ textContent="Add random nodes"
866
+ onClick={() => {
867
+ canvasRef.current?.addNodes(
868
+ [1, 2, 3].map(() => ({
869
+ id: Math.round(Math.random() * 1e6),
870
+ data: { name: String(Math.round(Math.random() * 1e6)) },
871
+ }))
872
+ );
873
+ }}
874
+ />
875
+ <WrappedEoButton
876
+ textContent="Add edge: Y => Z"
877
+ onClick={() =>
878
+ canvasRef.current?.addEdge({
879
+ source: "Y",
880
+ target: "Z",
881
+ data: { virtual: true },
882
+ })
883
+ }
884
+ />
885
+ </div>
886
+ <div style={{ flex: 1, minWidth: 0 }}>
887
+ <WrappedEoDrawCanvas
888
+ ref={canvasRef}
889
+ style={{ width: "100%", height: "100%" }}
890
+ activeTarget={activeTarget}
891
+ fadeUnrelatedCells={true}
892
+ layout="dagre"
893
+ defaultNodeSize={[60, 60]}
894
+ defaultNodeBricks={[
895
+ {
896
+ useBrick: {
897
+ brick: "diagram.experimental-node",
898
+ properties: { textContent: "<% `Node ${DATA.node.id}` %>" },
899
+ },
900
+ },
901
+ ]}
902
+ defaultEdgeLines={[
903
+ {
904
+ dashed: "<% !!DATA.edge.data?.virtual %>",
905
+ strokeColor: "var(--palette-blue-6)",
906
+ overrides: {
907
+ active: {
908
+ strokeWidth: "<% 2 * (DATA.edge?.data?.strokeWidth ?? 1) %>",
909
+ strokeColor: "cyan",
910
+ },
911
+ },
912
+ },
913
+ ]}
914
+ cells={initialCells}
915
+ onActiveTargetChange={(e: any) => setActiveTarget(e.detail)}
916
+ onCellMove={(e: any) =>
917
+ console.log(
918
+ `You just moved ${e.detail.type} ${e.detail.id} to (${Math.round(e.detail.x)}, ${Math.round(e.detail.y)})`
919
+ )
920
+ }
921
+ onCellResize={(e: any) =>
922
+ console.log(
923
+ `You just resized ${e.detail.type} ${e.detail.id} to (${Math.round(e.detail.width)}, ${Math.round(e.detail.height)})`
924
+ )
925
+ }
926
+ onCellDelete={(e: any) =>
927
+ console.log(
928
+ `You wanna delete ${e.detail.type} ${e.detail.type === "edge" ? `(${e.detail.source} => ${e.detail.target})` : e.detail.id}?`
929
+ )
930
+ }
931
+ onCellContextmenu={(e: any) => {
932
+ contextMenuRef.current?.open({
933
+ position: [e.detail.clientX, e.detail.clientY],
934
+ });
935
+ setTargetCell(e.detail.cell);
936
+ }}
937
+ onDecoratorTextChange={(e: any) =>
938
+ console.log(JSON.stringify(e.detail))
939
+ }
940
+ onScaleChange={(e: any) => setScale(e.detail)}
941
+ />
942
+ </div>
943
+ <WrappedDiagramExperimentalNode
944
+ usage="dragging"
945
+ textContent={
946
+ dragging?.type === "decorator"
947
+ ? dragging.decorator === "text"
948
+ ? "Text"
949
+ : null
950
+ : dragging?.data.name
951
+ }
952
+ decorator={dragging?.type === "decorator" ? dragging.decorator : null}
953
+ style={{
954
+ left: `${dragging?.position[0]}px`,
955
+ top: `${dragging?.position[1]}px`,
956
+ transform: `scale(${scale})`,
957
+ transformOrigin: "0 0",
958
+ padding: dragging?.decorator === "text" ? "0.5em" : "0",
959
+ }}
960
+ hidden={!dragging}
961
+ />
962
+ <WrappedEoContextMenu
963
+ ref={contextMenuRef}
964
+ actions={
965
+ targetCell?.type === "node"
966
+ ? [{ text: "添加边", event: "add-edge" }]
967
+ : [
968
+ {
969
+ text: `Test ${targetCell?.type}`,
970
+ event: `test-${targetCell?.type}`,
971
+ },
972
+ ]
973
+ }
974
+ onAddEdge={async () => {
975
+ const detail = await canvasRef.current?.manuallyConnectNodes(
976
+ targetCell.id
977
+ );
978
+ if (detail) {
979
+ canvasRef.current?.addEdge({
980
+ source: detail.source.id,
981
+ target: detail.target.id,
982
+ });
983
+ }
984
+ }}
985
+ />
986
+ </div>
987
+ );
988
+ }
989
+ ```