@blueking/flow-canvas 0.0.1-beta.1 → 0.0.1-beta.3

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.
package/README.md CHANGED
@@ -10,31 +10,67 @@
10
10
  npm install @blueking/flow-canvas
11
11
  ```
12
12
 
13
- ### 必需的对等依赖
13
+ ### 项目中另外还需要安装以下前置依赖
14
14
 
15
15
  ```bash
16
- npm install @antv/x6 @antv/x6-plugin-minimap @antv/x6-plugin-selection @antv/x6-plugin-snapline @antv/x6-vue-shape vue
16
+ npm install @antv/x6 @antv/x6-plugin-minimap @antv/x6-plugin-selection @antv/x6-plugin-snapline @antv/x6-plugin-dnd @antv/x6-vue-shape vue
17
17
  ```
18
18
 
19
- ### 可选依赖
19
+ ## 快速开始
20
20
 
21
- ```bash
22
- npm install @antv/x6-plugin-dnd # 拖拽建节点(DND)功能需要
21
+ ### 零配置启动
22
+
23
+ 使用 `createDefaultSchema` 可零配置启动一个可拖拽、可连线的画布:
24
+
25
+ ```vue
26
+ <template>
27
+ <CanvasLayout :editor="editor" :palette-items="paletteItems">
28
+ <CanvasRuntime :editor="editor" @ui-event="handleUiEvent" />
29
+ <CanvasToolbar :editor="editor" />
30
+ </CanvasLayout>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import {
35
+ useCanvasEditor,
36
+ CanvasRuntime,
37
+ CanvasLayout,
38
+ CanvasToolbar,
39
+ createDefaultSchema,
40
+ createEmptyFlowModel,
41
+ selectionPlugin,
42
+ snaplinePlugin,
43
+ type CanvasUiEvent,
44
+ } from '@blueking/flow-canvas';
45
+ import '@blueking/flow-canvas/style';
46
+
47
+ const { schema, paletteItems } = createDefaultSchema();
48
+
49
+ const editor = useCanvasEditor({
50
+ initialFlowModel: createEmptyFlowModel(),
51
+ schema,
52
+ plugins: [selectionPlugin(), snaplinePlugin()],
53
+ });
54
+
55
+ function handleUiEvent(event: CanvasUiEvent) {
56
+ console.log('UI 事件:', event.type);
57
+ }
58
+ </script>
23
59
  ```
24
60
 
25
- ## 快速开始
61
+ 传入 `editor` prop 后,`CanvasLayout` 会自动在侧边栏渲染内置节点面板 `CanvasNodePalette`,用户可直接拖拽节点到画布。
62
+
63
+ ### 自定义 Schema
64
+
65
+ 如需完全控制节点/边类型,可手动定义 `CanvasSchema`:
26
66
 
27
67
  ```vue
28
68
  <template>
29
69
  <CanvasLayout>
30
70
  <template #sidebar>
31
- <!-- 自定义侧边栏 -->
71
+ <MySidebar :editor="editor" />
32
72
  </template>
33
- <CanvasRuntime :editor="editor" @ui-event="handleUiEvent">
34
- <template #node-overlay="{ node, screenAnchors, api }">
35
- <!-- 节点悬浮操作按钮 -->
36
- </template>
37
- </CanvasRuntime>
73
+ <CanvasRuntime :editor="editor" @ui-event="handleUiEvent" />
38
74
  <CanvasToolbar :editor="editor" />
39
75
  </CanvasLayout>
40
76
  </template>
@@ -116,7 +152,7 @@ npm install @antv/x6-plugin-dnd # 拖拽建节点(DND)功能需要
116
152
  </script>
117
153
  ```
118
154
 
119
- ---
155
+ <br>
120
156
 
121
157
  ## 核心 API
122
158
 
@@ -151,13 +187,13 @@ npm install @antv/x6-plugin-dnd # 拖拽建节点(DND)功能需要
151
187
  | replaceFlowModel(model) | `(FlowModel) => void` | 整体替换流程模型 |
152
188
  | setMode(mode) | `(CanvasMode) => void` | 切换模式 |
153
189
 
154
- ---
190
+ <br>
155
191
 
156
192
  ## FlowModel 数据结构
157
193
 
158
194
  ```typescript
159
195
  interface FlowModel {
160
- version: '1.0';
196
+ version?: '1.0'; // 数据模型版本标识,可选,未传时引擎自动补全为 '1.0'
161
197
  nodes: Record<string, FlowNodeModel>; // 节点字典,key = nodeId
162
198
  edges: Record<string, FlowEdgeModel>; // 边字典,key = edgeId
163
199
  meta?: Record<string, unknown>; // 全局元数据
@@ -185,7 +221,7 @@ interface FlowEdgeModel {
185
221
  }
186
222
  ```
187
223
 
188
- ---
224
+ <br>
189
225
 
190
226
  ## CanvasSchema 配置
191
227
 
@@ -197,7 +233,6 @@ interface CanvasNodeDefinition {
197
233
  getSize: (node) => { width; height };
198
234
  getPorts?: (node) => FlowPortModel[];
199
235
  getBehavior?: (node, ctx) => NodeBehaviorConfig;
200
- getOverlayAnchors?: (node) => NodeOverlayAnchors;
201
236
  x6CellConfig?: Record<string, unknown>; // 透传给 X6 的额外配置
202
237
  }
203
238
  ```
@@ -223,29 +258,322 @@ interface CanvasEdgeDefinition {
223
258
 
224
259
  > **注意:** `labelRenderer` 字段在类型定义中存在但标记为 `@experimental`,当前版本尚未完整实现。如需自定义边标签样式,建议通过 `x6EdgeConfig.defaultLabel` 配置 X6 原生标签 markup。
225
260
 
226
- > **注意:** `labelRenderer` 字段在类型定义中存在但标记为 `@experimental`,当前版本尚未完整实现。如需自定义边标签样式,建议通过 `x6EdgeConfig.defaultLabel` 配置 X6 原生标签 markup。
261
+ <br>
262
+
263
+ ## createDefaultSchema — 开箱即用的 Schema 工厂
227
264
 
228
- ---
265
+ `createDefaultSchema` 提供内置的节点类型和边类型定义,可零配置快速启动画布。
266
+
267
+ ```typescript
268
+ import { createDefaultSchema } from '@blueking/flow-canvas';
269
+
270
+ const { schema, paletteItems } = createDefaultSchema(options?);
271
+ ```
272
+
273
+ ### DefaultSchemaOptions
274
+
275
+ | 参数 | 类型 | 默认 | 说明 |
276
+ | --------------- | --------------------------------------- | ------------- | ---------------------------------------------- |
277
+ | nodeTypes | `Record<string, DefaultNodeTypeConfig>` | 7 种内置类型 | 自定义节点类型列表,传入后替换内置类型 |
278
+ | defaultEdgeType | `string` | `'manhattan'` | 默认连线类型,内置 `'manhattan'` 和 `'bezier'` |
279
+ | edgeTypes | `Record<string, CanvasEdgeDefinition>` | - | 额外/覆盖的边类型定义,会与内置类型合并 |
280
+
281
+ ### 返回值
282
+
283
+ | 属性 | 类型 | 说明 |
284
+ | ------------ | ------------------- | -------------------------------------------- |
285
+ | schema | `CanvasSchema` | 可直接传给 `useCanvasEditor` 的 Schema 定义 |
286
+ | paletteItems | `NodePaletteItem[]` | 可传给 `CanvasLayout` 的 `paletteItems` prop |
287
+
288
+ ### 内置节点类型
289
+
290
+ 不传 `nodeTypes` 参数时,默认包含 7 种节点:
291
+
292
+ | 类型 | 标签 | 尺寸 |
293
+ | ------------------------------ | ------------ | -------- |
294
+ | `start` | 开始 | 88 × 40 |
295
+ | `end` | 结束 | 88 × 40 |
296
+ | `empty` | 空节点 | 240 × 48 |
297
+ | `parallel-gateway` | 并行网关 | 64 × 64 |
298
+ | `branch-gateway` | 分支网关 | 64 × 64 |
299
+ | `converge-gateway` | 汇聚网关 | 64 × 64 |
300
+ | `conditional-parallel-gateway` | 条件并行网关 | 64 × 64 |
301
+
302
+ 每种节点均使用内置 `DefaultNode` 组件渲染,自带上/右/下/左四个连接端口。
303
+
304
+ 自定义节点类型列表:
305
+
306
+ ```typescript
307
+ const { schema, paletteItems } = createDefaultSchema({
308
+ nodeTypes: {
309
+ task: { label: '任务节点', width: 180, height: 60 },
310
+ approval: { label: '审批节点', icon: 'my-icon-class', width: 200, height: 60 },
311
+ notification: { label: '通知节点', width: 160, height: 50 },
312
+ },
313
+ });
314
+ ```
315
+
316
+ ### 连线类型配置
317
+
318
+ `createDefaultSchema` 内置两种连线类型:
319
+
320
+ | 类型 | 路由 | 连接器 | 说明 |
321
+ | ------------------- | ----------- | ---------------------- | --------------------- |
322
+ | `manhattan`(默认) | `manhattan` | `rounded`(radius: 8) | 直角折线,圆角转弯 |
323
+ | `bezier` | 无 | `smooth` | 贝塞尔曲线,平滑 S 形 |
324
+
325
+ **切换为贝塞尔曲线连线:**
326
+
327
+ ```typescript
328
+ const { schema, paletteItems } = createDefaultSchema({
329
+ defaultEdgeType: 'bezier',
330
+ });
331
+ ```
332
+
333
+ **使用自定义边类型:**
334
+
335
+ ```typescript
336
+ const { schema, paletteItems } = createDefaultSchema({
337
+ defaultEdgeType: 'myEdge',
338
+ edgeTypes: {
339
+ myEdge: {
340
+ connector: 'normal',
341
+ style: (_edge, state) => ({
342
+ stroke: state.hovered ? 'red' : 'gray',
343
+ strokeWidth: 2,
344
+ }),
345
+ },
346
+ },
347
+ });
348
+ ```
349
+
350
+ **手动定义边类型(不使用 `createDefaultSchema`):**
351
+
352
+ 在手动编写 `CanvasSchema` 时,同样可以通过 `edgeTypes` 和 `defaultEdgeType` 配置连线样式:
353
+
354
+ ```typescript
355
+ const schema: CanvasSchema = {
356
+ nodeTypes: {
357
+ /* ... */
358
+ },
359
+ edgeTypes: {
360
+ bezier: {
361
+ connector: { name: 'smooth' },
362
+ style: (_edge, state) => ({
363
+ stroke: state.hovered ? '#3a84ff' : '#abb5cc',
364
+ strokeWidth: 2,
365
+ }),
366
+ x6EdgeConfig: {
367
+ attrs: {
368
+ line: {
369
+ stroke: '#abb5cc',
370
+ strokeWidth: 2,
371
+ targetMarker: { name: 'block', width: 8, height: 8 },
372
+ },
373
+ },
374
+ },
375
+ },
376
+ manhattan: {
377
+ router: { name: 'manhattan', args: { padding: 10, maxDirectionChange: 90 } },
378
+ connector: { name: 'rounded', args: { radius: 8 } },
379
+ style: (_edge, state) => ({
380
+ stroke: state.hovered ? '#3a84ff' : '#abb5cc',
381
+ strokeWidth: 2,
382
+ }),
383
+ },
384
+ },
385
+ defaultEdgeType: 'bezier',
386
+ };
387
+ ```
388
+
389
+ ### 默认连线与端口样式
390
+
391
+ - **端口**:半径 6px,背景 `#3a84ff`,1px 白色描边。默认隐藏(`visibility: hidden`),hover 节点时显示;拖拽连线时自动显示所有节点端口。
392
+ - **连线**:宽度 2px,颜色 `#abb5cc`,hover 变 `#3a84ff`,平角箭头(`block` marker)。`zIndex: -1` 确保在节点下方,不阻挡端口交互。
393
+ - **连接点**:`connectionPoint: 'anchor'` + `anchor: 'center'`,连线直达端口中心,紧贴节点。
394
+
395
+ <br>
229
396
 
230
397
  ## 命令系统
231
398
 
232
- 所有 FlowModel 变更必须通过命令执行:
399
+ 所有 FlowModel 变更必须通过命令执行。每条命令通过 `CommandEnvelope` 包装,一个信封可包含多条原子命令,作为一次事务整体执行(撤销时整体回退)。
233
400
 
234
401
  ```typescript
235
402
  import { generateId } from '@blueking/flow-canvas';
403
+ ```
404
+
405
+ **添加节点和连线:**
236
406
 
407
+ ```typescript
237
408
  editor.executeCommand({
238
409
  id: generateId(),
239
410
  source: 'user:panel',
240
411
  label: '添加节点',
241
412
  timestamp: Date.now(),
242
413
  commands: [
243
- { type: 'node.add', node: { id: 'n1', type: 'task', position: { x: 100, y: 100 } } },
244
- { type: 'edge.add', edge: { id: 'e1', source: { nodeId: 'n0' }, target: { nodeId: 'n1' } } },
414
+ { type: 'node.add', node: { id: 'n1', type: 'task', position: { x: 100, y: 100 }, label: '审批' } },
415
+ {
416
+ type: 'edge.add',
417
+ edge: { id: 'e1', source: { nodeId: 'n0', portId: 'right' }, target: { nodeId: 'n1', portId: 'left' } },
418
+ },
419
+ ],
420
+ });
421
+ ```
422
+
423
+ **删除节点(自动级联删除关联边):**
424
+
425
+ ```typescript
426
+ editor.executeCommand({
427
+ id: generateId(),
428
+ source: 'user:panel',
429
+ label: '删除节点',
430
+ timestamp: Date.now(),
431
+ commands: [{ type: 'node.remove', nodeId: 'n1' }],
432
+ });
433
+ ```
434
+
435
+ **删除连线:**
436
+
437
+ ```typescript
438
+ editor.executeCommand({
439
+ id: generateId(),
440
+ source: 'user:panel',
441
+ label: '删除连线',
442
+ timestamp: Date.now(),
443
+ commands: [{ type: 'edge.remove', edgeId: 'e1' }],
444
+ });
445
+ ```
446
+
447
+ **移动节点:**
448
+
449
+ ```typescript
450
+ editor.executeCommand({
451
+ id: generateId(),
452
+ source: 'user:panel',
453
+ label: '移动节点',
454
+ timestamp: Date.now(),
455
+ commands: [{ type: 'node.move', nodeId: 'n1', position: { x: 300, y: 200 } }],
456
+ });
457
+ ```
458
+
459
+ **更新节点结构字段(label、type、ports 等,不含 payload/extensions):**
460
+
461
+ ```typescript
462
+ editor.executeCommand({
463
+ id: generateId(),
464
+ source: 'user:panel',
465
+ label: '重命名节点',
466
+ timestamp: Date.now(),
467
+ commands: [{ type: 'node.update', nodeId: 'n1', patch: { label: '新名称' } }],
468
+ });
469
+ ```
470
+
471
+ **按路径更新节点 payload(业务数据):**
472
+
473
+ ```typescript
474
+ // 设置 payload.config.timeout = 30
475
+ editor.executeCommand({
476
+ id: generateId(),
477
+ source: 'user:panel',
478
+ label: '更新超时配置',
479
+ timestamp: Date.now(),
480
+ commands: [{ type: 'node.set-payload', nodeId: 'n1', path: ['config', 'timeout'], value: 30 }],
481
+ });
482
+
483
+ // 设置整个 payload.config 对象
484
+ editor.executeCommand({
485
+ id: generateId(),
486
+ source: 'user:panel',
487
+ label: '更新节点配置',
488
+ timestamp: Date.now(),
489
+ commands: [{ type: 'node.set-payload', nodeId: 'n1', path: ['config'], value: { timeout: 30, retryable: true } }],
490
+ });
491
+
492
+ // 删除 payload 中的某个字段(value 传 undefined)
493
+ editor.executeCommand({
494
+ id: generateId(),
495
+ source: 'user:panel',
496
+ label: '清除超时配置',
497
+ timestamp: Date.now(),
498
+ commands: [{ type: 'node.set-payload', nodeId: 'n1', path: ['config', 'timeout'], value: undefined }],
499
+ });
500
+ ```
501
+
502
+ **重连边端点:**
503
+
504
+ ```typescript
505
+ editor.executeCommand({
506
+ id: generateId(),
507
+ source: 'user:panel',
508
+ label: '重连边',
509
+ timestamp: Date.now(),
510
+ commands: [
511
+ {
512
+ type: 'edge.reconnect',
513
+ edgeId: 'e1',
514
+ target: { nodeId: 'n2', portId: 'left' }, // 只改 target,source 不变
515
+ },
516
+ ],
517
+ });
518
+ ```
519
+
520
+ **更新边标签:**
521
+
522
+ ```typescript
523
+ editor.executeCommand({
524
+ id: generateId(),
525
+ source: 'user:panel',
526
+ label: '更新边标签',
527
+ timestamp: Date.now(),
528
+ commands: [{ type: 'edge.label.update', edgeId: 'e1', labelId: 'label1', patch: { text: '条件成立' } }],
529
+ });
530
+ ```
531
+
532
+ **更新全局 meta:**
533
+
534
+ ```typescript
535
+ editor.executeCommand({
536
+ id: generateId(),
537
+ source: 'user:panel',
538
+ label: '更新全局变量',
539
+ timestamp: Date.now(),
540
+ commands: [{ type: 'model.set-meta', path: ['variables', 'env'], value: 'production' }],
541
+ });
542
+ ```
543
+
544
+ **批量操作(一个信封包含多条命令,一次性执行):**
545
+
546
+ ```typescript
547
+ editor.executeCommand({
548
+ id: generateId(),
549
+ source: 'user:panel',
550
+ label: '批量删除选中',
551
+ timestamp: Date.now(),
552
+ commands: [
553
+ { type: 'edge.remove', edgeId: 'e1' },
554
+ { type: 'edge.remove', edgeId: 'e2' },
555
+ { type: 'node.remove', nodeId: 'n1' },
556
+ { type: 'node.remove', nodeId: 'n2' },
245
557
  ],
246
558
  });
247
559
  ```
248
560
 
561
+ **处理命令执行结果:**
562
+
563
+ ```typescript
564
+ const result = editor.executeCommand({
565
+ /* ... */
566
+ });
567
+
568
+ if (result.status === 'applied') {
569
+ console.log('执行成功,新模型:', result.flowModel);
570
+ } else if (result.status === 'rejected') {
571
+ console.warn('被插件拒绝:', result.error?.reason);
572
+ } else if (result.status === 'invalid') {
573
+ console.error('违反约束:', result.error?.reason);
574
+ }
575
+ ```
576
+
249
577
  ### 命令类型一览
250
578
 
251
579
  | 命令 | 说明 |
@@ -270,7 +598,7 @@ editor.executeCommand({
270
598
 
271
599
  `'user:drag'` | `'user:keyboard'` | `'user:toolbar'` | `'user:quick-add'` | `'user:panel'` | `'plugin'` | `'system'` | `'system:replace'`
272
600
 
273
- ---
601
+ <br>
274
602
 
275
603
  ## 组件
276
604
 
@@ -291,7 +619,6 @@ editor.executeCommand({
291
619
 
292
620
  | Slot | Props | 说明 |
293
621
  | --------------- | ------------------------------------------------ | ----------------------------------------------------------- |
294
- | node-overlay | `{ node, screenAnchors, api }` | 节点悬浮层,鼠标移入节点时显示 |
295
622
  | quick-add-panel | `{ node, api, insertNodeToRight, closePopover }` | 快捷添加弹层内容,点击右侧"+"按钮时显示(默认显示提示文字) |
296
623
 
297
624
  #### 节点快捷操作工具栏
@@ -317,25 +644,26 @@ editor.executeCommand({
317
644
 
318
645
  通过 schema `getBehavior` 返回值控制每个节点类型的工具栏行为:
319
646
 
320
- | 属性 | 说明 |
321
- | -------------------- | -------------------------------------------------------- |
322
- | `showActions` | `false` → 该节点类型不显示整个工具栏 |
323
- | `deletable` | `false` → 隐藏删除按钮 |
324
- | `copyable` | `false` → 隐藏复制和复制并插入按钮 |
325
- | `disconnectable` | `false` → 隐藏断开连线按钮 |
326
- | `debuggable` | `false` → 隐藏调试按钮(需全局 `showDebug` 也为 `true`) |
327
- | `deleteDisabled` | `true` → 删除按钮可见但置灰不可点击 |
328
- | `copyDisabled` | `true` → 复制按钮置灰 |
329
- | `copyInsertDisabled` | `true` → 复制并插入按钮置灰 |
330
- | `disconnectDisabled` | `true` → 断开连线按钮置灰 |
331
- | `debugDisabled` | `true` → 调试按钮置灰 |
647
+ | 属性 | 说明 |
648
+ | -------------------- | --------------------------------------------------------------------------------- |
649
+ | `showActions` | `false` → 该节点类型不显示整个工具栏 |
650
+ | `deletable` | `false` → 隐藏删除按钮 |
651
+ | `copyable` | `false` → 隐藏复制和复制并插入按钮 |
652
+ | `disconnectable` | `false` → 隐藏断开连线按钮 |
653
+ | `debuggable` | `false` → 隐藏调试按钮(需全局 `showDebug` 也为 `true`) |
654
+ | `deleteDisabled` | `true` → 删除按钮可见但置灰不可点击 |
655
+ | `copyDisabled` | `true` → 复制按钮置灰 |
656
+ | `copyInsertDisabled` | `true` → 复制并插入按钮置灰 |
657
+ | `disconnectDisabled` | `true` → 断开连线按钮置灰 |
658
+ | `debugDisabled` | `true` → 调试按钮置灰 |
659
+ | `actionsOffset` | `{ x?: number; y?: number }` — 工具栏位置偏移(px),相对于默认位置(节点右下角) |
332
660
 
333
661
  按钮有效可见性 = 全局 `show*` AND 逐节点 `*able !== false`;
334
662
  按钮禁用状态 = 逐节点 `*Disabled === true`。
335
663
 
336
664
  操作触发后通过 `@ui-event` 发出对应事件:`node.action.delete` / `node.action.copy` / `node.action.copy-insert` / `node.action.disconnect` / `node.action.debug`。
337
665
 
338
- #### 节点快捷添加按钮
666
+ #### 节点快捷添加按钮(Quick Add)
339
667
 
340
668
  hover 节点时,右侧端口显示为"+"按钮,支持点击弹出面板和拖拽发起连线。通过 `quickAdd` prop 进行全局配置:
341
669
 
@@ -373,22 +701,223 @@ hover 节点时,右侧端口显示为"+"按钮,支持点击弹出面板和
373
701
 
374
702
  操作触发后通过 `@ui-event` 发出对应事件:`node.quick-add`(弹层打开时)/ `node.action.quick-insert`(插入成功时)。
375
703
 
704
+ **完整的 quick-add-panel 使用示例:**
705
+
706
+ ```vue
707
+ <template>
708
+ <CanvasLayout :editor="editor" :palette-items="paletteItems">
709
+ <CanvasRuntime :editor="editor" :quick-add="{ enabled: true }" @ui-event="handleUiEvent">
710
+ <template #quick-add-panel="{ node, insertNodeToRight, closePopover }">
711
+ <div class="quick-add-menu">
712
+ <div class="quick-add-menu__title">选择节点类型</div>
713
+ <div
714
+ v-for="item in quickAddNodeList"
715
+ :key="item.type"
716
+ class="quick-add-menu__item"
717
+ @click="handleQuickAdd(item, insertNodeToRight, closePopover)">
718
+ <i :class="item.icon" />
719
+ <span>{{ item.label }}</span>
720
+ </div>
721
+ </div>
722
+ </template>
723
+ </CanvasRuntime>
724
+ <CanvasToolbar :editor="editor" />
725
+ </CanvasLayout>
726
+ </template>
727
+
728
+ <script setup lang="ts">
729
+ import {
730
+ useCanvasEditor,
731
+ CanvasRuntime,
732
+ CanvasLayout,
733
+ CanvasToolbar,
734
+ createDefaultSchema,
735
+ createEmptyFlowModel,
736
+ generateId,
737
+ type FlowNodeModel,
738
+ } from '@blueking/flow-canvas';
739
+ import '@blueking/flow-canvas/style';
740
+
741
+ const { schema, paletteItems } = createDefaultSchema();
742
+ const editor = useCanvasEditor({ initialFlowModel: createEmptyFlowModel(), schema });
743
+
744
+ const quickAddNodeList = [
745
+ { type: 'empty', label: '空节点', icon: 'flow-canvas-icon canvas-jiedi' },
746
+ { type: 'parallel-gateway', label: '并行网关', icon: 'flow-canvas-icon canvas-bingxingwangguan' },
747
+ ];
748
+
749
+ function handleQuickAdd(
750
+ item: { type: string; label: string },
751
+ insertNodeToRight: (node: Omit<FlowNodeModel, 'position'>) => void,
752
+ closePopover: () => void,
753
+ ) {
754
+ insertNodeToRight({
755
+ id: generateId(),
756
+ type: item.type,
757
+ label: item.label,
758
+ });
759
+ closePopover();
760
+ }
761
+ </script>
762
+ ```
763
+
764
+ 通过 `getBehavior` 可动态控制哪些节点显示"+"按钮:
765
+
766
+ ```typescript
767
+ const schema: CanvasSchema = {
768
+ nodeTypes: {
769
+ end: {
770
+ component: EndNode,
771
+ getSize: () => ({ width: 88, height: 40 }),
772
+ getBehavior: () => ({
773
+ quickAddEnabled: false, // 结束节点不显示"+"按钮
774
+ deletable: false,
775
+ }),
776
+ },
777
+ task: {
778
+ component: TaskNode,
779
+ getSize: () => ({ width: 180, height: 60 }),
780
+ getBehavior: (node, ctx) => {
781
+ const hasOutEdge = Object.values(ctx.flowModel.edges).some((e) => e.source.nodeId === node.id);
782
+ return {
783
+ quickAddEnabled: !hasOutEdge, // 已有出边时隐藏"+"按钮
784
+ };
785
+ },
786
+ },
787
+ },
788
+ };
789
+ ```
790
+
376
791
  ### CanvasLayout
377
792
 
378
- 可选的布局骨架组件。
793
+ 可选的布局骨架组件,提供侧边栏 + 主画布 + 底栏的三栏布局。
794
+
795
+ | Prop | 类型 | 默认 | 说明 |
796
+ | ---------------- | --------------------- | ----------- | ---------------------------------------------------- |
797
+ | sidebarCollapsed | `boolean` | `false` | 侧边栏是否收起 |
798
+ | sidebarWidth | `number` | `260` | 侧边栏宽度(px) |
799
+ | hideSidebar | `boolean` | `false` | 是否隐藏侧边栏 |
800
+ | hideFooter | `boolean` | `false` | 是否隐藏底部栏 |
801
+ | editor | `CanvasEditorContext` | `undefined` | 编辑器实例,传入后自动渲染内置 `CanvasNodePalette` |
802
+ | paletteItems | `NodePaletteItem[]` | `undefined` | 节点面板项列表,配合 `editor` 使用自定义面板节点列表 |
803
+
804
+ | Emit | 参数 | 说明 |
805
+ | ----------------------- | --------- | ------------------ |
806
+ | update:sidebarCollapsed | `boolean` | 侧边栏收起状态变更 |
807
+
808
+ | Slot | 说明 |
809
+ | ------- | --------------------------------------------------------- |
810
+ | sidebar | 侧边栏内容(传入后覆盖内置 `CanvasNodePalette` 默认面板) |
811
+ | default | 主画布区域(放置 `CanvasRuntime` + `CanvasToolbar`) |
812
+ | footer | 底部栏 |
379
813
 
380
- | Prop | 类型 | 默认 | 说明 |
381
- | ---------------- | --------- | ------- | -------------- |
382
- | sidebarCollapsed | `boolean` | `false` | 侧边栏是否收起 |
383
- | sidebarWidth | `number` | `260` | 侧边栏宽度 |
384
- | hideSidebar | `boolean` | `false` | 是否隐藏侧边栏 |
385
- | hideFooter | `boolean` | `false` | 是否隐藏底部栏 |
814
+ #### 侧边栏的三种使用方式
386
815
 
387
- | Slot | 说明 |
388
- | ------- | ------------------------------------------------ |
389
- | sidebar | 侧边栏内容 |
390
- | default | 主画布区域(放置 CanvasRuntime + CanvasToolbar) |
391
- | footer | 底部栏 |
816
+ **方式一:自动渲染内置节点面板**
817
+
818
+ 传入 `editor` prop 且不提供 `sidebar` slot,`CanvasLayout` 自动在侧边栏渲染 `CanvasNodePalette`,用户可从面板拖拽节点到画布:
819
+
820
+ ```vue
821
+ <CanvasLayout :editor="editor" :palette-items="paletteItems">
822
+ <CanvasRuntime :editor="editor" />
823
+ </CanvasLayout>
824
+ ```
825
+
826
+ 通过 `paletteItems` prop 自定义面板中显示的节点类型列表:
827
+
828
+ ```vue
829
+ <script setup lang="ts">
830
+ import { createDefaultSchema, useCanvasEditor, createEmptyFlowModel } from '@blueking/flow-canvas';
831
+
832
+ const { schema, paletteItems } = createDefaultSchema();
833
+ const editor = useCanvasEditor({ initialFlowModel: createEmptyFlowModel(), schema });
834
+
835
+ // 也可以手动指定面板项,只展示部分节点类型
836
+ const customPaletteItems = [
837
+ { type: 'start', label: '开始节点', icon: 'flow-canvas-icon canvas-kaishi' },
838
+ { type: 'end', label: '结束节点', icon: 'flow-canvas-icon canvas-stop' },
839
+ { type: 'empty', label: '空节点', icon: 'flow-canvas-icon canvas-jiedi' },
840
+ ];
841
+ </script>
842
+
843
+ <template>
844
+ <CanvasLayout :editor="editor" :palette-items="customPaletteItems">
845
+ <CanvasRuntime :editor="editor" />
846
+ </CanvasLayout>
847
+ </template>
848
+ ```
849
+
850
+ **方式二:通过 sidebar slot 完全自定义侧边栏**
851
+
852
+ ```vue
853
+ <template>
854
+ <CanvasLayout>
855
+ <template #sidebar>
856
+ <div class="my-sidebar">
857
+ <h3>节点库</h3>
858
+ <div v-for="item in nodeItems" :key="item.type" ref="dndRefs" class="my-sidebar__item" :data-type="item.type">
859
+ {{ item.label }}
860
+ </div>
861
+ </div>
862
+ </template>
863
+ <CanvasRuntime :editor="editor" />
864
+ </CanvasLayout>
865
+ </template>
866
+
867
+ <script setup lang="ts">
868
+ import { ref, watch } from 'vue';
869
+ import { useCanvasEditor, CanvasRuntime, CanvasLayout, generateId } from '@blueking/flow-canvas';
870
+
871
+ const nodeItems = [
872
+ { type: 'task', label: '任务' },
873
+ { type: 'approval', label: '审批' },
874
+ ];
875
+
876
+ // 通过 api.registerDndSource 注册拖拽源
877
+ watch(
878
+ () => editor.api.value,
879
+ (api) => {
880
+ if (!api) return;
881
+ for (const item of nodeItems) {
882
+ const el = document.querySelector(`[data-type="${item.type}"]`);
883
+ if (el) {
884
+ api.registerDndSource(el as HTMLElement, () => ({
885
+ id: generateId(),
886
+ type: item.type,
887
+ label: item.label,
888
+ position: { x: 0, y: 0 },
889
+ }));
890
+ }
891
+ }
892
+ },
893
+ );
894
+ </script>
895
+ ```
896
+
897
+ **方式三:隐藏侧边栏**
898
+
899
+ ```vue
900
+ <CanvasLayout hide-sidebar>
901
+ <CanvasRuntime :editor="editor" />
902
+ </CanvasLayout>
903
+ ```
904
+
905
+ ### CanvasNodePalette
906
+
907
+ 内置的节点面板组件,支持拖拽节点到画布(DnD)。通常由 `CanvasLayout` 自动渲染,也可以单独使用。
908
+
909
+ | Prop | 类型 | 必填 | 说明 |
910
+ | ------ | --------------------- | ---- | ---------------------------------------- |
911
+ | editor | `CanvasEditorContext` | 是 | 编辑器实例 |
912
+ | items | `NodePaletteItem[]` | 否 | 节点列表,不传时从 schema.nodeTypes 生成 |
913
+
914
+ ```typescript
915
+ interface NodePaletteItem {
916
+ type: string; // 节点类型,对应 schema.nodeTypes 的 key
917
+ label: string; // 显示名称
918
+ icon?: string; // CSS 图标类名
919
+ }
920
+ ```
392
921
 
393
922
  ### CanvasToolbar
394
923
 
@@ -404,12 +933,20 @@ hover 节点时,右侧端口显示为"+"按钮,支持点击弹出面板和
404
933
 
405
934
  工厂函数,用于获取默认工具栏项列表,便于自定义组合。
406
935
 
936
+ 撤销/重做(`undo` / `redo`)默认隐藏,可通过 `include` 恢复显示。
937
+
407
938
  ```typescript
408
939
  import { createDefaultToolbarItems } from '@blueking/flow-canvas';
409
940
 
410
- // 排除搜索,追加自定义项
411
- const items = [
412
- ...createDefaultToolbarItems({ exclude: ['search'] }),
941
+ // 默认不含撤销/重做
942
+ const items = createDefaultToolbarItems();
943
+
944
+ // 恢复显示撤销/重做
945
+ const itemsWithHistory = createDefaultToolbarItems({ include: ['undo', 'redo'] });
946
+
947
+ // 恢复撤销/重做,同时排除搜索,追加自定义项
948
+ const customItems = [
949
+ ...createDefaultToolbarItems({ include: ['undo', 'redo'], exclude: ['search'] }),
413
950
  { id: 'my-tool', type: 'custom', icon: 'my-icon-class', onClick: handleClick },
414
951
  ];
415
952
  ```
@@ -426,7 +963,115 @@ const items = [
426
963
  | description | `string` | 操作说明,有值时 hover 显示 tooltip 气泡 |
427
964
  | onClick | `(ctx: CanvasCallbackContext) => void` | 点击回调 |
428
965
 
429
- ---
966
+ <br>
967
+
968
+ ## 自定义节点组件
969
+
970
+ 节点的渲染由 `CanvasNodeDefinition.component` 指定的 Vue 组件控制。组件内可通过 `inject('getNode')` 获取 X6 Node 实例,读取 FlowNodeModel 数据。
971
+
972
+ ### 基础节点组件
973
+
974
+ ```vue
975
+ <!-- my-task-node.vue -->
976
+ <template>
977
+ <div class="my-task-node" :class="{ 'is-selected': selected }">
978
+ <i v-if="icon" :class="icon" class="my-task-node__icon" />
979
+ <span class="my-task-node__label">{{ label }}</span>
980
+ </div>
981
+ </template>
982
+
983
+ <script setup lang="ts">
984
+ import { inject, ref, onMounted, onBeforeUnmount, computed } from 'vue';
985
+ import type { FlowNodeModel } from '@blueking/flow-canvas';
986
+
987
+ const getNode = inject<() => any>('getNode')!;
988
+
989
+ const nodeModel = ref<FlowNodeModel>();
990
+ const selected = ref(false);
991
+
992
+ const label = computed(() => nodeModel.value?.label ?? '');
993
+ const icon = computed(() => (nodeModel.value?.extensions as any)?.icon ?? '');
994
+
995
+ onMounted(() => {
996
+ const node = getNode();
997
+ nodeModel.value = node.getData() as FlowNodeModel;
998
+
999
+ node.on('change:data', ({ current }: { current: FlowNodeModel }) => {
1000
+ nodeModel.value = current;
1001
+ });
1002
+ });
1003
+ </script>
1004
+
1005
+ <style scoped>
1006
+ .my-task-node {
1007
+ display: flex;
1008
+ align-items: center;
1009
+ gap: 8px;
1010
+ width: 100%;
1011
+ height: 100%;
1012
+ padding: 0 16px;
1013
+ background: #fff;
1014
+ border: 1px solid #dcdee5;
1015
+ border-radius: 4px;
1016
+ box-sizing: border-box;
1017
+ cursor: pointer;
1018
+ }
1019
+ .my-task-node.is-selected {
1020
+ border-color: #3a84ff;
1021
+ }
1022
+ </style>
1023
+ ```
1024
+
1025
+ ### 在 Schema 中注册节点组件
1026
+
1027
+ ```typescript
1028
+ import MyTaskNode from './components/my-task-node.vue';
1029
+ import MyGatewayNode from './components/my-gateway-node.vue';
1030
+
1031
+ const schema: CanvasSchema = {
1032
+ nodeTypes: {
1033
+ task: {
1034
+ component: MyTaskNode,
1035
+ getSize: () => ({ width: 180, height: 60 }),
1036
+ getPorts: () => [
1037
+ { id: 'left', group: 'left' },
1038
+ { id: 'right', group: 'right' },
1039
+ ],
1040
+ getBehavior: (node, ctx) => ({
1041
+ deletable: true,
1042
+ copyable: true,
1043
+ quickAddEnabled: true,
1044
+ }),
1045
+ },
1046
+ gateway: {
1047
+ component: MyGatewayNode,
1048
+ getSize: () => ({ width: 64, height: 64 }),
1049
+ getPorts: () => [
1050
+ { id: 'top', group: 'top' },
1051
+ { id: 'right', group: 'right' },
1052
+ { id: 'bottom', group: 'bottom' },
1053
+ { id: 'left', group: 'left' },
1054
+ ],
1055
+ getBehavior: () => ({
1056
+ copyable: false,
1057
+ showActions: true,
1058
+ }),
1059
+ },
1060
+ },
1061
+ edgeTypes: {
1062
+ default: {
1063
+ connector: { name: 'smooth' },
1064
+ style: (_edge, state) => ({
1065
+ stroke: state.hovered ? '#3a84ff' : '#abb5cc',
1066
+ strokeWidth: 2,
1067
+ }),
1068
+ },
1069
+ },
1070
+ defaultEdgeType: 'default',
1071
+ };
1072
+ ```
1073
+
1074
+ <br>
430
1075
 
431
1076
  ## CanvasApi
432
1077
 
@@ -456,7 +1101,7 @@ const items = [
456
1101
  | onGraphEvent(event, handler) | 监听 X6 底层事件,返回取消函数 |
457
1102
  | unsafeGetGraph() | 获取 X6 Graph 实例(谨慎使用) |
458
1103
 
459
- ---
1104
+ <br>
460
1105
 
461
1106
  ## 内置插件
462
1107
 
@@ -512,7 +1157,7 @@ minimapPlugin({
512
1157
 
513
1158
  Cmd/Ctrl+C 复制选中节点/边,Cmd/Ctrl+V 粘贴(生成新 ID、偏移位置)。
514
1159
 
515
- ---
1160
+ <br>
516
1161
 
517
1162
  ## 自定义插件
518
1163
 
@@ -574,7 +1219,7 @@ const myPlugin: CanvasPlugin = {
574
1219
  };
575
1220
  ```
576
1221
 
577
- ---
1222
+ <br>
578
1223
 
579
1224
  ## 工具函数与底层 API
580
1225
 
@@ -594,7 +1239,7 @@ const emptyModel = createEmptyFlowModel();
594
1239
  // { version: '1.0', nodes: {}, edges: {} }
595
1240
  ```
596
1241
 
597
- ---
1242
+ <br>
598
1243
 
599
1244
  ## 撤销/重做
600
1245
 
@@ -606,7 +1251,7 @@ editor.history.canRedo.value; // boolean
606
1251
  editor.history.clear();
607
1252
  ```
608
1253
 
609
- ---
1254
+ <br>
610
1255
 
611
1256
  ## 事件类型 (CanvasUiEvent)
612
1257
 
@@ -628,7 +1273,7 @@ editor.history.clear();
628
1273
  | node.quick-add | nodeId, position | 快捷添加弹层打开 |
629
1274
  | node.action.quick-insert | sourceNodeId, newNodeId | 快捷插入节点 |
630
1275
 
631
- ---
1276
+ <br>
632
1277
 
633
1278
  ## 公开类型参考
634
1279
 
@@ -649,7 +1294,7 @@ editor.history.clear();
649
1294
  | 类型 | 说明 |
650
1295
  | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
651
1296
  | `CanvasSchema` | 画布配置(nodeTypes / edgeTypes) |
652
- | `CanvasNodeDefinition` | 节点类型定义(component / getSize / getPorts / getBehavior / getOverlayAnchors |
1297
+ | `CanvasNodeDefinition` | 节点类型定义(component / getSize / getPorts / getBehavior) |
653
1298
  | `CanvasEdgeDefinition` | 边类型定义(router / connector / style / labelDraggable)。`labelRenderer` 字段为 `@experimental`,当前未实现 |
654
1299
  | `CanvasCallbackContext` | 运行时上下文(api / flowModel / history / mode) |
655
1300
  | `CanvasMode` | 模式:`'edit'` / `'readonly'` / `'thumbnail'` |
@@ -698,6 +1343,16 @@ editor.history.clear();
698
1343
  | `ConnectionValidator` | 连接校验函数类型 |
699
1344
  | `ConnectionValidateContext` | 连接校验上下文 |
700
1345
  | `ConnectionValidateResult` | 连接校验结果 |
1346
+ | `NodePaletteItem` | 节点面板项(type / label / icon) |
1347
+
1348
+ ### 默认 Schema
1349
+
1350
+ | 类型 | 说明 |
1351
+ | ---------------------------- | ---------------------------------- |
1352
+ | `DefaultNodeTypeConfig` | `createDefaultSchema` 的节点配置项 |
1353
+ | `DefaultSchemaOptions` | `createDefaultSchema` 的选项 |
1354
+ | `DefaultSchemaResult` | `createDefaultSchema` 的返回值 |
1355
+ | `DefaultToolbarItemsOptions` | `createDefaultToolbarItems` 的选项 |
701
1356
 
702
1357
  ### 历史与 Overlay
703
1358
 
@@ -706,8 +1361,6 @@ editor.history.clear();
706
1361
  | `CanvasHistory` | 历史管理器接口 |
707
1362
  | `CanvasHistoryOptions` | 历史配置(maxHistorySize) |
708
1363
  | `OverlayManager` | Overlay 管理器接口 |
709
- | `NodeOverlayAnchors` | 节点 Overlay 锚点定义 |
710
- | `OverlayAnchor` | 单个锚点坐标 |
711
1364
 
712
1365
  ### 编辑器
713
1366