mta-mcp 2.14.0 → 2.16.0

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.
@@ -1,1567 +1,97 @@
1
- ---
2
- description: 'LogicFlow 流程图组件通用开发代理 - 自定义节点、连线规则、流程校验、Vue 3 集成'
3
- tags: ['logicflow', 'vue', 'vue3', 'typescript']
4
- tools: ['edit', 'search', 'usages', 'vscodeAPI', 'problems', 'runSubagent']
5
- ---
6
-
7
- # LogicFlow 流程图组件通用开发代理
8
-
9
- **适用场景**: LogicFlow 2.1+ 流程图开发、自定义节点、流程图校验、Vue/React 项目集成
10
-
11
- ## ⚠️ 强制工作流
12
-
13
- **在开发 LogicFlow 相关代码前,必须先调用 MCP 工具:**
14
-
15
- ```
16
- get_relevant_standards({ imports: ["@logicflow/core"] })
17
- ```
18
-
19
- 或使用场景匹配:
20
- ```
21
- get_relevant_standards({ scenario: "流程图开发" })
22
- ```
1
+ # LogicFlow 流程图开发代理
23
2
 
24
- ## 🎯 核心能力
25
-
26
- 1. **项目适配优先** - 分析用户项目需求,生成适配的节点类型和规则
27
- 2. **架构理解深入** - 掌握 LogicFlow Model-View-Component 三层架构
28
- 3. **自定义节点开发** - 快速创建符合业务需求的自定义节点
29
- 4. **连线规则设计** - 根据业务逻辑设计节点间的连接约束
30
- 5. **校验系统构建** - 实现前端结构校验和业务逻辑校验
31
- 6. **错误可视化** - 不符合规则的节点和连线自动高亮提示
3
+ > Agent 引导 AI 通过 MCP 工具获取 npm 包中的详细规范
4
+ > 版本: v3.0.0 | 最后更新: 2026-01-16
32
5
 
33
6
  ---
34
7
 
35
- ## 🔍 开发流程
36
-
37
- ### 第一步:理架构(Model-View-Component)
8
+ ## 🔴 问题诊断优先(最高优先级)
38
9
 
39
- LogicFlow 基于三层架构:
40
-
41
- ```typescript
42
- // 节点注册标准模式
43
- import { register } from '@logicflow/vue-node-registry'
10
+ **当用户描述任何问题时,必须首先调用:**
44
11
 
45
- register({
46
- type: 'customNode', // 节点类型标识
47
- model: CustomNodeModel, // 节点逻辑模型(锚点、规则、属性)
48
- view: CustomNodeView, // 节点视图(锚点渲染)
49
- component: CustomNodeComponent // 节点 UI 组件(样式、内容)
50
- }, lfInstance)
51
12
  ```
52
-
53
- #### 1.1 常见节点模式分类
54
-
55
- 根据业务需求,节点通常分为以下模式:
56
-
57
- | 模式 | 锚点配置 | 连线规则 | 典型应用 |
58
- |-----|---------|---------|---------|
59
- | **单向流节点** | 左入右出 / 上入下出 | 单入单出 | 数据转换、审批步骤 |
60
- | **分支节点** | 1入多出(动态锚点) | 单入多出,每个出口限1条 | 条件分支、并行处理 |
61
- | **汇聚节点** | 多入1出(动态锚点) | 多入单出 | 分支合并、数据汇总 |
62
- | **起始节点** | 只有出口 | 只能连出,限1条 | 流程开始、数据源 |
63
- | **终止节点** | 只有入口 | 只能连入 | 流程结束、数据输出 |
64
- | **双向节点** | 左右 / 上下对称 | 双向连接 | 数据交互、状态切换 |
65
-
66
- **关键**:根据用户业务需求选择合适的模式,而不是固定的节点类型
13
+ troubleshoot({ problem: "用户描述的问题" })
67
14
  ```
68
15
 
69
- ### 第三步:实现节点模型
70
-
71
- 根据设计快速生成代码...
72
-
73
16
  ---
74
17
 
75
- ## 📐 架构解析
76
-
77
- ### 1. 节点体系
78
-
79
- LogicFlow 基于 **Model-View-Component** 三层架构:
80
-
81
- ```typescript
82
- // 节点注册标准模式
83
- import { register } from '@logicflow/vue-node-registry'
84
-
85
- register({
86
- type: 'unit', // 节点类型
87
- model: UnitNodeModel, // 节点逻辑模型(锚点、规则、属性)
88
- view: BaseNodeView, // 节点视图(锚点渲染)
89
- component: BaseNodeComponent // 节点 UI 组件(样式、内容)
90
- }, lfInstance)
91
- ```
92
-
93
- #### 1.1 节点类型定义
94
-
95
- | 类型 | node_type | 用途 | 锚点配置 | 连线规则 |
96
- |-----|-----------|-----|---------|---------|
97
- | `start` | 0 | 开始节点 | 只有右锚点 | 只能连出 |
98
- | `end` | 1 | 结束节点 | 只有左锚点 | 只能连入 |
99
- | `unit` | 2 | 单元节点 | 上下锚点 | 上入下出,各一条 |
100
- | `operation` | 2 | 操作节点 | 上下锚点 | 上入下出,各一条 |
101
- | `phase` | 2 | 阶段节点 | 上下锚点 | 上入下出,各一条 |
102
- | `action` | 3 | 动作节点 | 上下锚点 | 上入下出,各一条 |
103
- | `transition` | 8 | 条件节点 | 上下锚点 | 上入下出,不能连 transition |
104
- | `branch` | 4 | 分支开始 | 上锚点 + 多下锚点 | 一入多出(动态锚点)|
105
- | `branchEnd` | 5 | 分支结束 | 多上锚点 + 下锚点 | 多入一出 |
106
- | `parallel` | 6 | 并行分支开始 | 上锚点 + 多下锚点 | 一入多出(动态锚点)|
107
- | `parallelEnd` | 7 | 并行分支结束 | 多上锚点 + 下锚点 | 多入一出 |
108
-
109
- #### 1.2 节点模型(Model)核心方法
110
- 开发模板
111
-
112
- ```typescript
113
- import { RectNodeModel } from '@logicflow/core'
114
-
115
- // 自定义节点模型基础模板
116
- export class CustomNodeModel extends RectNodeModel {
117
- // 是否实时更新(属性变化时重新渲染)
118
- shouldUpdate() {
119
- return true // 需要响应式更新时返回 true
120
- }
121
-
122
- // 设置节点尺寸和样式
123
- setAttributes() {
124
- this.width = 200 // 根据内容调整
125
- this.height = 80
126
-
127
- // 可选:根据动态属性调整尺寸
128
- if (this.properties?.branches) {
129
- this.height = 60 + this.properties.branches.length * 20
130
- }
131
- }
132
-
133
- // 连出规则(作为源节点)
134
- getConnectedSourceRules() {
135
- const rules = super.getConnectedSourceRules()
136
-
137
- // 示例规则 1: 限制出口连线数量
138
- const limitOutgoing = {
139
- message: '该节点最多只能连出 X 条线',
140
- validate: () => {
141
- const { edges } = this.outgoing
142
- return !(edges && edges.length >= X) // X 根据业务需求设置
143
- },
144
- }
145
-
146
- // 示例规则 2: 限制连接的目标节点类型
147
- const allowedTargets = {
148
- message: '只能连接到特定类型的节点',
149
- validate: (sourceNode, targetNode) => {
150
- const allowedTypes = ['typeA', 'typeB']
151
- return allowedTypes.includes(targetNode.type)
152
- },
153
- }
154
-
155
- // 示例规则 3: 防止自环
156
- const noSelfLoop = {
157
- message: '节点不能连接自己',
158
- validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) =>
159
- sourceNode.id !== targetNode.id
160
- }
161
-
162
- // 根据业务需求选择规则
163
- rules.push(limitOutgoing, allowedTargets, noSelfLoop)
164
- return rules
165
- }
166
-
167
- // 连入规则(作为目标节点)
168
- getConnectedTargetRules() {
169
- const rules = super.getConnectedTargetRules()
170
-
171
- // 示例:限制入口连线数量
172
- const limitIncoming = {
173
- message: '该节点最多只能有 Y 条输入',
174
- validate: () => {
175
- const { edges } = this.incoming
176
- return !(edges && edges.length >= Y)
177
- },
178
- }
179
-
180
- rules.push(limitIncoming)
181
- return rules
182
- }
183
-
184
- // 定义锚点位置
185
- getDefaultAnchor(): { x: number; y: number; id: string }[] {
186
- const { id, x, y, width, height } = this
187
-
188
- // 常见模式 1: 上下锚点(垂直流)
189
- return [
190
- { x, y: y - height / 2, id: `${id}_top` }, // 上锚点
191
- { x, y: y + height / 2, id: `${id}_bottom` } // 下锚点
192
- ]
193
-
194
- // 常见模式 2: 左右锚点(水平流)
195
- // return [
196
- // { x: x - width / 2, y, id: `${id}_left` },
197
- // { x: x + width / 2, y, id: `${id}_right` }
198
- // ]
199
-
200
- // 常见模式 3: 四向锚点
201
- // return [
202
- // { x, y: y - height / 2, id: `${id}_top` },
203
- // { x, y: y + height / 2, id: `${id}_bottom` },
204
- // { x: x - width / 2, y, id: `${id}_left` },
205
- // { x: x + width / 2, y, id: `${id}_right` }
206
- //
207
- }
208
- ```
209
-
210
- #### 1.3 动态锚点节点(Branch/Parallel)
211
-
212
- ```typescript
213
- export class BranchNodeModel extends BaseNodeModel {
214
- constructor(data: any, graphModel: any) {
215
- // 自定义文本位置
216
- data.text = {
217
- value: typeof data.text === 'string' ? data.text : data?.text?.value,
218
- x: data?.x + 50,
219
- y: data?.y,
220
- }
221
- super(data, graphModel)
222
- }
223
-
224
- setAttributes() {
225
- this.width = 120
226
- this.height = 60
227
- }
18
+ ## 📚 规范获取指引
228
19
 
229
- // 动态锚点生成
230
- getDefaultAnchor(): { x: number; y: number; id: string }[] {
231
- const { id, x, y, width, height } = this
232
- const { branches = [] } = this.properties as { branches?: { anchorId: string; index: number }[] }
20
+ **⚠️ 核心原则:开发 LogicFlow 代码前,必须先获取规范!**
233
21
 
234
- const anchors: { x: number; y: number; id: string }[] = [
235
- { x, y: y - height / 2, id: `${id}_top` } // 固定上锚点
236
- ]
22
+ ### 按场景获取
237
23
 
238
- // 动态生成多个下锚点
239
- const branchCount = branches.length || 2 // 默认 2 个分支
240
- const spacing = width / (branchCount + 1)
241
-
242
- branches.forEach((branch, index) => {
243
- anchors.push({
244
- x: x - width / 2 + spacing * (index + 1),
245
- y: y + height / 2,
246
- id: `${id}${branch.anchorId}`
247
- })
248
- })
24
+ | 场景 | MCP 调用 |
25
+ |------|----------|
26
+ | LogicFlow 基础 | `get_standard_by_id({ id: 'logicflow' })` |
27
+ | 自定义节点 | `get_standard_by_id({ id: 'logicflow' })` |
28
+ | Vue 3 集成 | `get_standard_by_id({ ids: ['logicflow', 'vue3-composition'] })` |
29
+ | TypeScript | `get_standard_by_id({ id: 'typescript-base' })` |
249
30
 
250
- return anchors
251
- }
31
+ ### 智能获取(推荐)
252
32
 
253
- // 单个锚点只能连出一条线
254
- getConnectedSourceRules() {
255
- const rules = super.getConnectedSourceRules()
256
-
257
- const onlyUniqueSource = {
258
- message: '分支锚点只能与一个节点相连',
259
- validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
260
- const { edges } = this.outgoing
261
- const isHaveBranchEdge = edges && edges.some(edge =>
262
- edge.sourceAnchorId === sourceAnchor.id
263
- )
264
- return !isHaveBranchEdge
265
- },
266
- }
267
-
268
- rules.push(onlyUniqueSource)
269
- return rules
270
- }
271
- }
272
33
  ```
273
-
274
- ---
275
-
276
- ### 2. 节点组件(Component)
277
-
278
- **通用节点组件模板**:
279
-
280
- ```vue
281
- <template>
282
- <section class="viewport">
283
- <!-- 普通节点:显示类型和名称 -->
284
- <div v-if="!isSpecialType"
285
- :class="`custom-node custom-node-${data.type}`">
286
- <div class="node-header">
287
- <span class="node-type">{{ getNodeTypeLabel(data.type) }}</span>
288
- </div>
289
- <div class="node-content">
290
- <span class="node-name">{{ data.properties.name || 'Unnamed' }}</span>
291
- <!-- 根据业务需求显示其他属性 -->
292
- <span v-if="data.properties.description" class="node-desc">
293
- {{ data.properties.description }}
294
- </span>
295
- </div>
296
- <!-- 错误状态指示 -->
297
- <div v-if="data.properties.hasError" class="error-indicator">⚠️</div>
298
- </div>
299
-
300
- <!-- 特殊节点:如分支、合并等 -->
301
- <div v-else :class="`${data.type} custom-node-${data.type}`">
302
- <div :class="`center-line center-line-${data.type}`"></div>
303
- <div v-if="needsParallelIndicator(data.type)" class="parallel-indicator"></div>
304
- </div>
305
- </section>
306
- </template>
307
-
308
- <script setup lang="ts">
309
- import { ref, computed, onMounted } from 'vue'
310
- import { EventType } from '@logicflow/core'
311
-
312
- interface Props {
313
- node: any
314
- graph: any
315
- }
316
-
317
- const props = defineProps<Props>()
318
- const data = ref({ ...props.graph.getNodeModelById(props.node.id) })
319
-
320
- // 定义特殊节点类型(需要特殊渲染的)
321
- const specialTypes = ['branch', 'branchEnd', 'parallel', 'parallelEnd', 'gateway']
322
- const isSpecialType = computed(() => specialTypes.includes(data.value.type))
323
-
324
- // 获取节点类型的显示文本(可从配置或i18n获取)
325
- const getNodeTypeLabel = (type: string) => {
326
- const labelMap = {
327
- start: '开始',
328
- end: '结束',
329
- task: '任务',
330
- decision: '决策'
331
- // 根据项目需求扩展...
332
- }
333
- return labelMap[type] || type
334
- }
335
-
336
- // 判断是否需要并行指示器
337
- const needsParallelIndicator = (type: string) => {
338
- return type.includes('parallel')
339
- }
340
-
341
- // 监听节点属性变化
342
- onMounted(() => {
343
- const eventHandler = (eventData: any) => {
344
- if (eventData.id === props.node.id) {
345
- data.value.properties = eventData?.properties
346
- }
347
- }
348
-
349
- props.graph.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, eventHandler)
350
-
351
- return () => {
352
- props.graph.eventCenter.off(EventType.NODE_PROPERTIES_CHANGE, eventHandler)
353
- }
354
- })
355
- </script>
356
-
357
- <style scoped>
358
- .custom-node {
359
- display: flex;
360
- flex-direction: column;
361
- background: #fff;
362
- border: 1px solid #ddd;
363
- border-radius: 4px;
364
- padding: 8px;
365
- }
366
-
367
- .node-header {
368
- font-size: 12px;
369
- color: #666;
370
- margin-bottom: 4px;
371
- }
372
-
373
- .node-content {
374
- font-size: 14px;
375
- color: #333;
376
- }
377
-
378
- .error-indicator {
379
- position: absolute;
380
- top: -8px;
381
- right: -8px;
382
- color: #f56c6c;
383
- }
384
- </style>
385
- ```
386
-
387
- ---
388
-
389
- ### 3. 流程图主组件(Flow.vue)
390
-
391
- #### 3.1 初始化流程
392
-
393
- ```typescript
394
- import LogicFlow from '@logicflow/core'
395
- import { Control, Menu, DndPanel, SelectionSelect } from '@logicflow/extension'
396
- import { Dagre } from '@logicflow/layout'
397
- import { register, getTeleport } from '@logicflow/vue-node-registry'
398
-
399
- // 注册插件
400
- LogicFlow.use(Control) // 控制面板(放大/缩小/还原)
401
- LogicFlow.use(Menu) // 右键菜单
402
- LogicFlow.use(DndPanel) // 拖拽面板
403
- LogicFlow.use(SelectionSelect) // 框选
404
- LogicFlow.use(Dagre) // 自动布局
405
-
406
- let lf: any = null
407
-
408
- const renderLf = async () => {
409
- lf = new LogicFlow({
410
- container: document.querySelector('.vita-flow') as HTMLElement,
411
- grid: true, // 显示网格
412
- plugins: [Dagre],
413
- keyboard: { enabled: true }, // 启用键盘快捷键
414
- isSilentMode: props.read, // 只读模式
415
- })
416
-
417
- // 注册自定义节点
418
- initFlow(props.render, lf, $t, props.read, menuCallback.value, menuItem.value, addItem.value)
419
- }
420
- ```
421
-
422
- #### 3.2 事件监听
423
-
424
- **通用事件监听模板**:
425
-
426
- ```typescript
427
- // 定义事件处理器配置
428
- const setupEventListeners = (lf: any, options: {
429
- onNodeClick?: (node: any) => void
430
- onNodeDoubleClick?: (node: any) => void
431
- onEdgeChange?: () => void
432
- onConnectionNotAllowed?: (data: any) => void
433
- shouldAutoValidate?: boolean
434
- }) => {
435
- let clickTimer: NodeJS.Timeout | null = null
436
-
437
- // 连线规则校验失败
438
- lf.on('connection:not-allowed', (data: any) => {
439
- if (options.onConnectionNotAllowed) {
440
- options.onConnectionNotAllowed(data)
441
- } else {
442
- console.warn('连接不被允许:', data?.msg)
443
- }
444
- })
445
-
446
- // 单击节点
447
- lf.on('node:click', (e: any) => {
448
- if (clickTimer) clearTimeout(clickTimer)
449
- clickTimer = setTimeout(() => {
450
- clickTimer = null
451
- console.log('单击节点', e?.data)
452
- options.onNodeClick?.(e.data)
453
- }, 200)
454
- })
455
-
456
- // 双击节点(打开配置对话框)
457
- lf.on('node:dbclick', (e: any) => {
458
- if (clickTimer) {
459
- clearTimeout(clickTimer)
460
- clickTimer = null
461
- }
462
-
463
- console.log('双击节点', e?.data)
464
- options.onNodeDoubleClick?.(e.data)
465
- })
466
-
467
- // 连线变化 - 可选的自动校验
468
- if (options.shouldAutoValidate) {
469
- lf.on('edge:add', () => {
470
- setTimeout(() => options.onEdgeChange?.(), 100)
471
- })
472
-
473
- lf.on('edge:delete', () => {
474
- setTimeout(() => options.onEdgeChange?.(), 100)
475
- })
476
- }
477
- }
478
-
479
- // 使用示例
480
- setupEventListeners(lf, {
481
- onNodeClick: (node) => {
482
- // 处理节点点击
483
- selectNode(node.id)
484
- },
485
- onNodeDoubleClick: (node) => {
486
- // 打开编辑器
487
- openNodeEditor(node)
488
- },
489
- onEdgeChange: () => {
490
- // 重新校验流程图
491
- validateGraph()
492
- },
493
- onConnectionNotAllowed: (data) => {
494
- showMessage({ type: 'warning', message: data?.msg })
495
- },
496
- shouldAutoValidate: true
497
- })
498
- ```
499
-
500
- #### 3.3 节点拖拽监听
501
-
502
- ```typescript
503
- lf.on('node:dnd-add', (data: any) => {
504
- const rawData = lf.getGraphRawData()
505
- const node = lf.graphModel.getNodeModelById(data?.data?.id)
506
-
507
- // 示例 1: 限制特定节点的数量
508
- if (needUnique(data.data.type)) {
509
- const existingNodes = rawData.nodes.filter(n => n.type === data.data.type)
510
- if (existingNodes.length > 1) {
511
- lf.deleteNode(data.data.id)
512
- showMessage({ type: 'warning', message: `只能有一个${data.data.type}节点` })
513
- return
514
- }
515
- }
516
-
517
- // 示例 2: 自动命名新节点
518
- if (needAutoName(data.data.type)) {
519
- const sameTypeNodes = rawData.nodes.filter(n => n.type === data.data.type)
520
- const index = sameTypeNodes.length
521
- node.setProperty('name', `${data.data.type}_${String(index).padStart(3, '0')}`)
522
- }
523
-
524
- // 示例 3: 初始化节点属性
525
- if (needInitProperties(data.data.type)) {
526
- node.setProperty('status', 'pending')
527
- node.setProperty('createdAt', Date.now())
528
- }
34
+ // 根据当前文件自动匹配
35
+ get_compact_standards({
36
+ currentFile: "xxx.vue",
37
+ imports: ["@logicflow/core"]
529
38
  })
530
39
  ```
531
40
 
532
41
  ---
533
42
 
534
- ### 4. 流程图校验系统
535
-
536
- > **🔴 重要特性**:校验失败时,不符合规则的节点和连线会自动变红高亮,直观显示错误位置
43
+ ## 🎯 快速提示
537
44
 
538
- **校验触发时机**:
539
- 1. 点击"校验"按钮 → 执行校验 → 高亮错误
540
- 2. 点击"提交"按钮 → 先校验 → 失败则高亮错误并阻止提交
541
- 3. 添加/删除连线时 → 自动重新校验 → 更新高亮状态
45
+ > 以下是简要提示,**详细规范请通过上述 MCP 工具获取**
542
46
 
543
- #### 4.1 前端结构校验(flowValidator.ts
47
+ ### 核心架构(Model-View-Component
544
48
 
545
49
  ```typescript
546
- export interface ValidationResult {
547
- valid: boolean
548
- errors: ValidationError[]
549
- errorEdges: string[] // 错误的边 ID
550
- errorNodes: string[] // 错误的节点 ID
551
- }
552
-
553
- export interface ValidationError {
554
- type: 'isolated_node' | 'isolated_path' | 'circular_path' | 'unconnected_anchor' | 'no_start' | 'no_end' | 'unreachable'
555
- message: string
556
- nodeIds?: string[]
557
- edgeIds?: string[]
558
- }
559
-
560
- export class FlowValidator {
561
- private nodes: any[]
562
- private edges: any[]
563
- private adjacencyList: Map<string, string[]> // 邻接表
564
- private reverseAdjacencyList: Map<string, string[]> // 反向邻接表
565
- private startNode: any | null = null
566
- private endNode: any | null = null
567
-
568
- constructor(graphData: GraphData) {
569
- this.nodes = graphData.nodes || []
570
- this.edges = graphData.edges || []
571
- this.buildAdjacencyLists()
572
- this.findStartEndNodes()
573
- }
574
-
575
- public validate(): ValidationResult {
576
- const errors: ValidationError[] = []
577
- const errorEdges: string[] = []
578
- const errorNodes: string[] = []
579
-
580
- // 1. 检查是否存在 start 和 end 节点
581
- if (!this.startNode) {
582
- errors.push({ type: 'no_start', message: '缺少开始节点' })
583
- }
584
- if (!this.endNode) {
585
- errors.push({ type: 'no_end', message: '缺少结束节点' })
586
- }
587
-
588
- if (!this.startNode || !this.endNode) {
589
- return { valid: false, errors, errorEdges, errorNodes }
590
- }
591
-
592
- // 2. 检查从 start 可达的节点(BFS)
593
- const reachableFromStart = this.getReachableNodes(this.startNode.id, this.adjacencyList)
594
-
595
- // 3. 检查可以到达 end 的节点(反向 BFS)
596
- const canReachEnd = this.getReachableNodes(this.endNode.id, this.reverseAdjacencyList)
597
-
598
- // 4. 计算主路径节点(既能从 start 到达,又能到达 end)
599
- const mainPathNodes = new Set(
600
- [...reachableFromStart].filter(nodeId => canReachEnd.has(nodeId))
601
- )
602
-
603
- // 5. 检查孤立节点(不在主路径上)
604
- const isolatedNodes = this.nodes.filter(node => {
605
- if (node.type === 'start' || node.type === 'end') return false
606
- return !mainPathNodes.has(node.id)
607
- })
608
-
609
- if (isolatedNodes.length > 0) {
610
- const isolatedNodeIds = isolatedNodes.map(n => n.id)
611
- errors.push({
612
- type: 'isolated_node',
613
- message: `发现 ${isolatedNodes.length} 个孤立节点(不在 start 到 end 的有效路径上)`,
614
- nodeIds: isolatedNodeIds,
615
- })
616
- errorNodes.push(...isolatedNodeIds)
617
-
618
- // 找到连接到孤立节点的边
619
- const isolatedEdges = this.edges.filter(edge =>
620
- isolatedNodeIds.includes(edge.sourceNodeId) ||
621
- isolatedNodeIds.includes(edge.targetNodeId)
622
- )
623
- errorEdges.push(...isolatedEdges.map(e => e.id))
624
- }
625
-
626
- // 6. 检查回路(DFS)
627
- const cycles = this.detectCycles(mainPathNodes)
628
- if (cycles.length > 0) {
629
- cycles.forEach(cycle => {
630
- errors.push({
631
- type: 'circular_path',
632
- message: `检测到回路:${cycle.join(' → ')}`,
633
- nodeIds: cycle,
634
- })
635
- errorNodes.push(...cycle)
636
- })
637
- }
638
-
639
- // 7. 检查 end 节点是否可达
640
- if (!reachableFromStart.has(this.endNode.id)) {
641
- errors.push({
642
- type: 'unreachable',
643
- message: '无法从开始节点到达结束节点',
644
- nodeIds: [this.endNode.id],
645
- })
646
- errorNodes.push(this.endNode.id)
647
- }
648
-
649
- return {
650
- valid: errors.length === 0,
651
- errors,
652
- errorEdges: [...new Set(errorEdges)],
653
- errorNodes: [...new Set(errorNodes)],
654
- }
655
- }
656
-
657
- // BFS 获取可达节点
658
- private getReachableNodes(startNodeId: string, adjacency: Map<string, string[]>): Set<string> {
659
- const visited = new Set<string>()
660
- const queue: string[] = [startNodeId]
661
- visited.add(startNodeId)
662
-
663
- while (queue.length > 0) {
664
- const current = queue.shift()!
665
- const neighbors = adjacency.get(current) || []
666
-
667
- neighbors.forEach(neighbor => {
668
- if (!visited.has(neighbor)) {
669
- visited.add(neighbor)
670
- queue.push(neighbor)
671
- }
672
- })
673
- }
674
-
675
- return visited
676
- }
677
-
678
- // DFS 检测回路
679
- private detectCycles(validNodes: Set<string>): string[][] {
680
- const visited = new Set<string>()
681
- const recursionStack = new Set<string>()
682
- const cycles: string[][] = []
683
-
684
- const dfs = (nodeId: string): boolean => {
685
- visited.add(nodeId)
686
- recursionStack.add(nodeId)
687
-
688
- const neighbors = (this.adjacencyList.get(nodeId) || [])
689
- .filter(n => validNodes.has(n))
690
-
691
- for (const neighbor of neighbors) {
692
- if (!visited.has(neighbor)) {
693
- if (dfs(neighbor)) return true
694
- } else if (recursionStack.has(neighbor)) {
695
- // 发现回路
696
- const cycle: string[] = []
697
- let current = nodeId
698
- cycle.push(neighbor)
699
- while (current !== neighbor) {
700
- cycle.push(current)
701
- current = this.getParent(current, neighbor)
702
- }
703
- cycles.push(cycle.reverse())
704
- return true
705
- }
706
- }
707
-
708
- recursionStack.delete(nodeId)
709
- return false
710
- }
711
-
712
- validNodes.forEach(nodeId => {
713
- if (!visited.has(nodeId)) {
714
- dfs(nodeId)
715
- }
716
- })
717
-
718
- return cycles
719
- }
720
- }
721
- ```
722
-
723
- #### 4.2 调用校验与错误高亮
724
-
725
- **核心功能:校验失败的节点和连线自动变红**
726
-
727
- ```typescript
728
- const validateGraph = async () => {
729
- try {
730
- if (!lf) return false
731
-
732
- const rawData = lf.getGraphRawData()
733
- const { nodes, edges } = rawData
734
-
735
- // 1. 前端结构校验
736
- const frontendValidation = validateFlow(rawData)
737
-
738
- // 🔴 核心:高亮错误的边和节点
739
- highlightErrors(frontendValidation)
740
-
741
- if (!frontendValidation.valid) {
742
- const errorMessages = frontendValidation.errors.map(err => err.message).join('\n')
743
- showMessage({
744
- type: 'error',
745
- message: `流程图校验失败:\n${errorMessages}`,
746
- duration: 5000
747
- })
748
- return false
749
- }
750
-
751
- // 2. (可选) 调用后端业务校验
752
- const backendResult = await callBackendValidation(nodes, edges)
753
- if (!backendResult.success) {
754
- showMessage({
755
- type: 'error',
756
- message: backendResult.message || '后端校验失败'
757
- })
758
- return false
759
- }
760
-
761
- // 3. 校验通过,清除高亮
762
- clearErrorHighlight()
763
- showMessage({ type: 'success', message: '校验通过' })
764
- return true
765
- } catch (err) {
766
- console.error('Validation error:', err)
767
- showMessage({ type: 'error', message: '校验失败' })
768
- return false
769
- }
770
- }
771
-
772
- // 🔴 核心功能:高亮错误节点和连线
773
- const highlightErrors = (validation: ValidationResult) => {
774
- if (!lf) return
775
-
776
- // 清除之前的高亮
777
- clearErrorHighlight()
778
-
779
- // 🔴 高亮错误的边(变红色,加粗)
780
- validation.errorEdges.forEach(edgeId => {
781
- const edgeModel = lf.graphModel.getEdgeModelById(edgeId)
782
- if (edgeModel) {
783
- edgeModel.setProperties({ ...edgeModel.properties, isError: true })
784
- edgeModel.setAttributes({
785
- stroke: '#f56c6c', // 红色
786
- strokeWidth: 2 // 加粗至 2px
787
- })
788
- }
789
- })
790
-
791
- // 🔴 高亮错误的节点(红色边框)
792
- validation.errorNodes.forEach(nodeId => {
793
- const nodeModel = lf.graphModel.getNodeModelById(nodeId)
794
- if (nodeModel) {
795
- nodeModel.setProperties({ ...nodeModel.properties, isError: true })
796
- // 注意:节点红色边框通过 CSS 样式实现:
797
- // .custom-node[data-error="true"] { border-color: #f56c6c !important; }
798
- }
799
- })
800
- }
801
-
802
- // 🟢 清除错误高亮
803
- const clearErrorHighlight = () => {
804
- if (!lf) return
805
-
806
- const rawData = lf.getGraphRawData()
807
-
808
- // 清除所有边的错误状态
809
- rawData.edges.forEach((edge: any) => {
810
- const edgeModel = lf.graphModel.getEdgeModelById(edge.id)
811
- if (edgeModel && edgeModel.properties?.isError) {
812
- edgeModel.setProperties({ ...edgeModel.properties, isError: false })
813
- edgeModel.setAttributes({
814
- stroke: '#000000', // 恢复默认颜色
815
- strokeWidth: 1 // 恢复默认宽度
816
- })
817
- }
818
- })
819
-
820
- // 清除所有节点的错误状态
821
- rawData.nodes.forEach((node: any) => {
822
- const nodeModel = lf.graphModel.getNodeModelById(node.id)
823
- if (nodeModel && nodeModel.properties?.isError) {
824
- nodeModel.setProperties({ ...nodeModel.properties, isError: false })
825
- }
826
- })
827
- }
828
-
829
- // (可选) 后端校验接口调用
830
- const callBackendValidation = async (nodes: any[], edges: any[]) => {
831
- // 根据实际项目的后端 API 调整
832
- const params = {
833
- nodes: nodes.map(node => ({
834
- id: node.id,
835
- type: node.type,
836
- name: node.properties?.name || '',
837
- // 其他必要属性...
838
- })),
839
- edges: edges.map(edge => ({
840
- source: edge.sourceNodeId,
841
- target: edge.targetNodeId,
842
- sourceAnchor: edge.sourceAnchorId,
843
- targetAnchor: edge.targetAnchorId
844
- }))
845
- }
846
-
847
- // return await api.validateGraph(params)
848
- return { success: true } // 示例
849
- }
850
- ```
851
-
852
- **在节点组件中应用错误样式**:
853
-
854
- ```vue
855
- <template>
856
- <div :class="['custom-node', { 'error-node': data.properties?.isError }]">
857
- <!-- 节点内容 -->
858
- </div>
859
- </template>
860
-
861
- <style scoped>
862
- .custom-node {
863
- border: 2px solid #ddd;
864
- transition: border-color 0.3s;
865
- }
866
-
867
- /* 🔴 错误状态:红色边框 */
868
- .custom-node.error-node {
869
- border-color: #f56c6c !important;
870
- animation: error-pulse 1s ease-in-out infinite;
871
- }
872
-
873
- @keyframes error-pulse {
874
- 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.4); }
875
- 50% { box-shadow: 0 0 0 6px rgba(245, 108, 108, 0); }
876
- }
877
- </style>
878
- ```
879
-
880
- ---
881
-
882
- ### 5. 右键菜单与工具栏
883
-
884
- #### 5.1 右键菜单配置
885
-
886
- ```typescript
887
- const setMenu = (lf: any, $t: any, menuCallback: any) => {
888
- const hidePublic = ['start', 'end']
889
- const publicMenu = [
890
- { text: $t('删除'), callback: (node: any) => lf.deleteNode(node.id) },
891
- { text: $t('复制'), callback: (node: any) => lf.graphModel.cloneNode(node.id) }
892
- ]
893
-
894
- const menus = [
895
- {
896
- type: 'start',
897
- menu: [
898
- { text: $t('编辑文本'), callback: (node: any) => lf.graphModel.setElementStateById(node.id, 2) }
899
- ]
900
- },
901
- {
902
- type: 'unit',
903
- menu: [
904
- { text: $t('编辑参数'), callback: (node: any) => menuCallback.openDrawer(node) }
905
- ]
906
- },
907
- {
908
- type: 'operation',
909
- menu: [
910
- { text: $t('编辑参数'), callback: (node: any) => menuCallback.openDrawer(node) }
911
- ]
912
- },
913
- {
914
- type: 'phase',
915
- menu: [
916
- { text: $t('编辑参数'), callback: (node: any) => menuCallback.openDrawer(node) }
917
- ]
918
- },
919
- {
920
- type: 'branch',
921
- menu: [
922
- { text: $t('添加分支锚点'), callback: (node: any) => menuCallback.addBranch(node) },
923
- { text: $t('减少分支锚点'), callback: (node: any) => menuCallback.removeBranch(node) },
924
- { text: $t('编辑文本'), callback: (node: any) => lf.graphModel.setElementStateById(node.id, 2) }
925
- ]
926
- },
927
- {
928
- type: 'parallel',
929
- menu: [
930
- { text: $t('添加分支锚点'), callback: (node: any) => menuCallback.addBranch(node) },
931
- { text: $t('减少分支锚点'), callback: (node: any) => menuCallback.removeBranch(node) },
932
- { text: $t('编辑文本'), callback: (node: any) => lf.graphModel.setElementStateById(node.id, 2) }
933
- ]
934
- },
935
- ]
936
-
937
- menus.forEach(item => {
938
- if (!hidePublic.includes(item.type)) {
939
- publicMenu.forEach(i => item.menu.push(i))
940
- }
941
- lf.setMenuByType(item)
942
- })
943
- }
944
- ```
945
-
946
- #### 5.2 动态锚点管理
947
-
948
- ```typescript
949
- // logicFunc.ts
950
- export default {
951
- // 添加分支锚点
952
- addBranch: (node: any, lf: any, createUuid: any) => {
953
- const newBranch = {
954
- anchorId: `_bottom_${(node.properties.branches || []).length + 1}`,
955
- index: (node.properties.branches || []).length,
956
- }
957
- const newBranches = (node.properties.branches || []).concat(newBranch)
958
- const nodeModel = lf.graphModel.getNodeModelById(node.id)
959
- const edges = lf.graphModel.getNodeEdges(node.id)
960
-
961
- nodeModel.setProperty('branches', newBranches)
962
-
963
- // 更新连线起点位置
964
- setTimeout(() => {
965
- edges.forEach(edge => {
966
- nodeModel?.anchors?.forEach(fil => {
967
- if (fil.id === edge.sourceAnchorId) {
968
- edge.updateStartPoint({ x: fil?.x, y: fil?.y })
969
- }
970
- })
971
- })
972
- }, 10)
973
- },
974
-
975
- // 移除分支锚点
976
- removeBranch: (node: any, lf: any, $t: any) => {
977
- const newBranches = (node.properties.branches || [])
978
-
979
- if (newBranches.length && newBranches.length > 2) {
980
- const popEdge = newBranches.pop()
981
- const nodeModel = lf.graphModel.getNodeModelById(node.id)
982
-
983
- nodeModel.setProperty('branches', newBranches)
984
-
985
- const edges = lf.graphModel.getNodeEdges(node.id)
986
- setTimeout(() => {
987
- edges.forEach(edge => {
988
- nodeModel?.anchors?.forEach(fil => {
989
- if (fil.id === edge.sourceAnchorId) {
990
- edge.updateStartPoint({ x: fil?.x, y: fil?.y })
991
- }
992
- if (`${node.id}${popEdge.anchorId}` === edge.sourceAnchorId) {
993
- lf.graphModel.deleteEdgeById(edge.id)
994
- }
995
- })
996
- })
997
- }, 10)
998
- } else {
999
- ElMessage.warning($t('没有可以删除的分支锚点'))
1000
- }
1001
- },
1002
-
1003
- // 边排序(按锚点后缀数字排序)
1004
- sortPolylinesByAnchorSuffix: (polylines: any) => {
1005
- const getAnchorSuffix = (anchorId: any) => {
1006
- const match = anchorId.match(/_(\d+)(?:_|$)/)
1007
- return match ? parseInt(match[1], 10) : 0
1008
- }
1009
-
1010
- const nodeGroups = {}
1011
- polylines.forEach((polyline: any) => {
1012
- if (!nodeGroups[polyline.sourceNodeId]) {
1013
- nodeGroups[polyline.sourceNodeId] = []
1014
- }
1015
- nodeGroups[polyline.sourceNodeId].push(polyline)
1016
- })
1017
-
1018
- for (const nodeId in nodeGroups) {
1019
- nodeGroups[nodeId].sort((a, b) => {
1020
- const aSuffix = getAnchorSuffix(a.sourceAnchorId)
1021
- const bSuffix = getAnchorSuffix(b.sourceAnchorId)
1022
- return aSuffix - bSuffix
1023
- })
1024
- }
1025
-
1026
- const result = []
1027
- for (const nodeId in nodeGroups) {
1028
- result.push(...nodeGroups[nodeId])
1029
- }
1030
-
1031
- return result
1032
- }
1033
- }
1034
- ```
1035
-
1036
- ---
1037
-
1038
- ### 6. 自动布局(Dagre)
1039
-
1040
- ```typescript
1041
- const autoSort = (type = 'LR') => {
1042
- if (!lf) return
1043
-
1044
- const newRender = { ...lf.getGraphRawData() }
1045
-
1046
- // 先对边排序
1047
- lf.render({
1048
- nodes: newRender?.nodes,
1049
- edges: logicFunc.sortPolylinesByAnchorSuffix(newRender?.edges)
1050
- })
1051
-
1052
- // 使用 Dagre 自动布局
1053
- setTimeout(() => {
1054
- lf.extension.dagre.layout({
1055
- rankdir: type, // 'TB' 垂直,'LR' 水平
1056
- ranker: 'longest-path', // 布局算法
1057
- align: undefined, // 对齐方式
1058
- nodesep: 60, // 节点间距
1059
- ranksep: 70, // 层级间距
1060
- isDefaultAnchor: false, // 不使用默认锚点
1061
- })
1062
- }, 100)
1063
- }
1064
- ```
1065
-
1066
- ---
1067
-
1068
- ### 7. 数据提交
1069
-
1070
- ```typescript
1071
- const getData = async () => {
1072
- if (!lf) return
1073
-
1074
- // 先执行校验
1075
- const isValid = await checkData()
1076
- if (!isValid) {
1077
- ElMessage.warning($t('请先修复流程图中的错误'))
1078
- return
1079
- }
1080
-
1081
- // 获取画布数据
1082
- const rawData = lf.getGraphRawData()
1083
- emit('flowSubmit', { rawData: rawData })
1084
- }
1085
-
1086
- // 父组件处理提交
1087
- const flowSubmit = async (data) => {
1088
- try {
1089
- flowLoading.value = true
1090
- const { nodes, edges } = data.rawData
1091
-
1092
- const params = {
1093
- id: checkNodeNow.value?.id || 0,
1094
- front_flow_chart_data: JSON.stringify(data.rawData),
1095
- pre_flow_node_rels: edges.map(m => ({
1096
- current_node_code: m.targetAnchorId,
1097
- pre_node_code: m.sourceAnchorId
1098
- })),
1099
- flow_node_data_list: nodes.map(m => ({
1100
- recipe_version_id: props.render?.recipe_install_id,
1101
- node_code: m.id,
1102
- name: m.properties?.name || '',
1103
- description: m.properties?.description || '',
1104
- node_type: m?.properties?.node_type,
1105
- ...m?.properties,
1106
- })),
1107
- }
1108
-
1109
- const agin = await api.$submitRecipeFlowChartInfo(params)
1110
- if (agin.success) {
1111
- ElMessage.success($t('提交成功'))
1112
- getTree()
1113
- }
1114
- } catch (err) {
1115
- console.error(err)
1116
- } finally {
1117
- flowLoading.value = false
1118
- }
1119
- }
1120
- ```
1121
-
1122
- ---
1123
-
1124
- ## ⚠️ 常见问题与解决方案
1125
-
1126
- ### 1. 节点属性不更新
1127
-
1128
- ```typescript
1129
- // ❌ 错误:直接修改 properties
1130
- node.properties.name = 'new name'
1131
-
1132
- // ✅ 正确:使用 setProperty
1133
- const nodeModel = lf.graphModel.getNodeModelById(nodeId)
1134
- nodeModel.setProperty('name', 'new name')
1135
- ```
1136
-
1137
- ### 2. 锚点位置错乱
1138
-
1139
- ```typescript
1140
- // ✅ 添加/删除锚点后,必须更新连线起点
1141
- setTimeout(() => {
1142
- edges.forEach(edge => {
1143
- nodeModel?.anchors?.forEach(anchor => {
1144
- if (anchor.id === edge.sourceAnchorId) {
1145
- edge.updateStartPoint({ x: anchor.x, y: anchor.y })
1146
- }
1147
- })
1148
- })
1149
- }, 10)
1150
- ```
1151
-
1152
- ### 3. 校验后高亮不消失
1153
-
1154
- ```typescript
1155
- // ✅ 正确的清除高亮函数
1156
- const clearErrorHighlight = () => {
1157
- if (!lf) return
1158
-
1159
- const rawData = lf.getGraphRawData()
1160
-
1161
- // 🟢 恢复所有边的正常样式(黑色,细线)
1162
- rawData.edges.forEach((edge: any) => {
1163
- const edgeModel = lf.graphModel.getEdgeModelById(edge.id)
1164
- if (edgeModel && edgeModel.properties?.isError) {
1165
- edgeModel.setProperties({ ...edgeModel.properties, isError: false })
1166
- edgeModel.setAttributes({
1167
- stroke: '#000000', // 恢复默认颜色
1168
- strokeWidth: 1 // 恢复默认粗细
1169
- })
1170
- }
1171
- })
1172
-
1173
- // 🟢 恢复所有节点的正常样式
1174
- rawData.nodes.forEach((node: any) => {
1175
- const nodeModel = lf.graphModel.getNodeModelById(node.id)
1176
- if (nodeModel && nodeModel.properties?.isError) {
1177
- nodeModel.setProperty('isError', false)
1178
- }
1179
- })
1180
- }
1181
- ```
1182
-
1183
- ### 4. 只读模式下右键菜单仍显示
1184
-
1185
- ```typescript
1186
- // ✅ 初始化时设置只读模式
1187
- lf = new LogicFlow({
1188
- container: document.querySelector('.flow-container') as HTMLElement,
1189
- grid: true,
1190
- isSilentMode: isReadOnly, // 只读模式
1191
- })
1192
-
1193
- // ✅ 只读模式下不设置菜单和拖拽面板
1194
- if (!isReadOnly) {
1195
- setupContextMenu(lf, nodeTypes, callbacks)
1196
- lf.extension.dndPanel.setPatternItems(patternItems)
1197
- }
1198
- ```
1199
-
1200
- ---
1201
-
1202
- ## 📋 代码审查清单
1203
-
1204
- 生成 LogicFlow 代码前确认:
1205
-
1206
- ### 节点模型
1207
- - [ ] 继承 `BaseNodeModel` 或对应的基类
1208
- - [ ] 实现 `shouldUpdate()` 返回 true(需要实时更新的节点)
1209
- - [ ] 实现 `setAttributes()` 设置节点尺寸
1210
- - [ ] 实现 `getDefaultAnchor()` 定义锚点位置
1211
- - [ ] 实现 `getConnectedSourceRules()` 定义连出规
1212
- - [ ] 支持错误状态样式(红色边框):`.custom-node-wrap` 需根据 `properties.isError` 应用红色边框则
1213
- - [ ] 实现 `getConnectedTargetRules()` 定义连入规则
1214
- - [ ] 连接规则的 `message` 使用国际化文本或普通字符串
1215
-
1216
- ### 节点组件
1217
- - [ ] 使用 `EventType.NODE_PROPERTIES_CHANGE` 监听属性变化
1218
- - [ ] 在 `onMounted` 中注册监听器,返回清理函数
1219
- - [ ] 使用 `props.graph.getNodeModelById()` 获取节点数据
1220
- - [ ] 样式类名使用 `custom-node-${type}` 格式
1221
- - [ ] 支持错误状态显示(`error-node` 类名)
1222
-
1223
- ### 主组件
1224
- - [ ] 所有插件在 `onMounted` 之前注册(`LogicFlow.use`)
1225
- - [ ] 事件监听统一管理(通过 `setupEventListeners`)
1226
- - [ ] 校验失败时高亮错误节点和边(红色)
1227
- - [ ] 提交前必须执行校验(`validateGraph()`)
1228
- - [ ] 只读模式下禁用菜单和拖拽面板
1229
-
1230
- ### 校验系统
1231
- - [ ] 前端结构校验先执行
1232
- - [ ] 前端校验通过后再调用后端业务校验(如需要)
1233
- - [ ] 错误信息清晰明确
1234
- - [ ] 高亮错误节点和边
1235
- - [ ] 清除高亮时恢复默认样式
1236
-
1237
- ### 数据保存
1238
- - [ ] 保存完整的流程图数据(JSON)
1239
- - [ ] 节点包含所有必要属性(id, type, properties等)
1240
- - [ ] 边包含源/目标节点和锚点信息
1241
- - [ ] 节点属性完整传递(不丢失自定义字段)
1242
-
1243
- ---
1244
-
1245
- ## 🚀 最佳实践
1246
-
1247
- ### 1. 新增节点类型的完整流程
1248
-
1249
- ```typescript
1250
- // 步骤1: 创建节点模型文件
1251
- // src/flow/models/CustomTaskNodeModel.ts
1252
- import { RectNode, RectNodeModel } from '@logicflow/core'
1253
-
1254
- export class CustomTaskNodeModel extends RectNodeModel {
1255
- shouldUpdate() { return true } // 允许实时更新
1256
-
1257
- setAttributes() {
1258
- this.width = 180
1259
- this.height = 80
1260
- this.radius = 8 // 圆角
1261
- }
1262
-
1263
- // 定义锚点
1264
- getDefaultAnchor() {
1265
- const { id, x, y, width, height } = this
1266
- return [
1267
- { x, y: y - height / 2, id: `${id}_top` },
1268
- { x: x - width / 2, y, id: `${id}_left` },
1269
- { x: x + width / 2, y, id: `${id}_right` },
1270
- { x, y: y + height / 2, id: `${id}_bottom` }
1271
- ]
1272
- }
1273
-
1274
- // 连接规则
1275
- getConnectedSourceRules() {
1276
- const rules = super.getConnectedSourceRules()
1277
-
1278
- // 添加自定义规则:只能连向特定类型的节点
1279
- rules.push({
1280
- message: '任务节点只能连接到决策节点或结束节点',
1281
- validate: (sourceNode: any, targetNode: any) => {
1282
- return ['decision', 'end'].includes(targetNode.type)
1283
- }
1284
- })
1285
-
1286
- return rules
1287
- }
1288
- }
1289
-
1290
- // 步骤2: 创建节点组件(可选,使用通用组件也可以)
1291
- // src/flow/components/CustomTaskNode.vue
1292
- <template>
1293
- <div :class="['task-node', { 'error': data.properties?.isError }]">
1294
- <div class="task-header">
1295
- <span class="task-icon">📋</span>
1296
- <span class="task-title">{{ data.properties.name || 'Untitled Task' }}</span>
1297
- </div>
1298
- <div v-if="data.properties.description" class="task-desc">
1299
- {{ data.properties.description }}
1300
- </div>
1301
- </div>
1302
- </template>
1303
-
1304
- <script setup lang="ts">
1305
- import { ref, onMounted } from 'vue'
1306
- import { EventType } from '@logicflow/core'
1307
-
1308
- const props = defineProps<{ node: any, graph: any }>()
1309
- const data = ref({ ...props.graph.getNodeModelById(props.node.id) })
1310
-
1311
- onMounted(() => {
1312
- const handler = (e: any) => {
1313
- if (e.id === props.node.id) {
1314
- data.value.properties = e.properties
1315
- }
1316
- }
1317
- props.graph.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, handler)
1318
-
1319
- return () => {
1320
- props.graph.eventCenter.off(EventType.NODE_PROPERTIES_CHANGE, handler)
1321
- }
1322
- })
1323
- </script>
1324
-
1325
- <style scoped>
1326
- .task-node {
1327
- background: #fff;
1328
- border: 2px solid #409eff;
1329
- border-radius: 8px;
1330
- padding: 12px;
1331
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1332
- }
1333
-
1334
- .task-node.error {
1335
- border-color: #f56c6c !important;
1336
- animation: error-pulse 1s ease-in-out infinite;
1337
- }
1338
- </style>
1339
-
1340
- // 步骤3: 注册节点
1341
- // src/flow/registerNodes.ts
1342
- import { register } from '@logicflow/vue-node-registry'
1343
- import { CustomTaskNodeModel } from './models/CustomTaskNodeModel'
1344
- import CustomTaskNode from './components/CustomTaskNode.vue'
1345
-
1346
- export function registerCustomNodes(lf: any) {
1347
- register({
1348
- type: 'customTask',
1349
- model: CustomTaskNodeModel,
1350
- component: CustomTaskNode
1351
- }, lf)
1352
- }
1353
-
1354
- // 步骤4: 在主组件中初始化
1355
- // src/FlowChart.vue
1356
- import { registerCustomNodes } from './flow/registerNodes'
1357
-
1358
- onMounted(() => {
1359
- lf = new LogicFlow({ /* config */ })
1360
-
1361
- // 注册自定义节点
1362
- registerCustomNodes(lf)
1363
-
1364
- // 添加到拖拽面板
1365
- lf.extension.dndPanel.setPatternItems([
1366
- {
1367
- type: 'customTask',
1368
- text: '任务节点',
1369
- label: '任务',
1370
- icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz4='
1371
- }
1372
- ])
1373
- })
1374
- ```
1375
-
1376
- ### 2. 添加自定义校验规则
1377
-
1378
- ```typescript
1379
- // 扩展 FlowValidator 类
1380
- export class FlowValidator {
1381
- // ... 基础校验方法 ...
1382
-
1383
- // 添加业务相关的校验
1384
- private validateBusinessRules(): ValidationError[] {
1385
- const errors: ValidationError[] = []
1386
-
1387
- // 示例1: 检查任务节点是否设置了执行人
1388
- this.nodes
1389
- .filter(node => node.type === 'customTask')
1390
- .forEach(node => {
1391
- if (!node.properties?.assignee) {
1392
- errors.push({
1393
- type: 'isolated_node',
1394
- message: `任务节点 "${node.properties?.name}" 未指定执行人`,
1395
- nodeIds: [node.id]
1396
- })
1397
- }
1398
- })
1399
-
1400
- // 示例2: 检查决策节点是否配置了条件表达式
1401
- this.nodes
1402
- .filter(node => node.type === 'decision')
1403
- .forEach(node => {
1404
- if (!node.properties?.expression) {
1405
- errors.push({
1406
- type: 'unconnected_anchor',
1407
- message: `决策节点 "${node.properties?.name}" 未配置条件表达式`,
1408
- nodeIds: [node.id]
1409
- })
1410
- }
1411
- })
1412
-
1413
- return errors
1414
- }
1415
-
1416
- // 在主校验方法中调用
1417
- public validate(): ValidationResult {
1418
- const errors: ValidationError[] = []
1419
- const errorNodes: string[] = []
1420
- const errorEdges: string[] = []
1421
-
1422
- // 基础结构校验
1423
- errors.push(...this.validateStructure())
1424
-
1425
- // 业务规则校验
1426
- errors.push(...this.validateBusinessRules())
1427
-
1428
- // 收集错误节点
1429
- errors.forEach(error => {
1430
- if (error.nodeIds) errorNodes.push(...error.nodeIds)
1431
- if (error.edgeIds) errorEdges.push(...error.edgeIds)
1432
- })
1433
-
1434
- return {
1435
- valid: errors.length === 0,
1436
- errors,
1437
- errorEdges: [...new Set(errorEdges)],
1438
- errorNodes: [...new Set(errorNodes)]
1439
- }
1440
- }
1441
- }
1442
- ```
1443
-
1444
- ### 3. 实现节点间数据传递
1445
-
1446
- ```typescript
1447
- // 场景:分支节点需要知道有多少条路径
1448
-
1449
- // 方法1: 通过节点属性传递
1450
- const updateBranchInfo = (branchNodeId: string) => {
1451
- const nodeModel = lf.graphModel.getNodeModelById(branchNodeId)
1452
- const outgoingEdges = lf.graphModel.getNodeOutgoingEdge(branchNodeId)
1453
-
1454
- // 保存分支数量到节点属性
1455
- nodeModel.setProperty('branchCount', outgoingEdges.length)
1456
- }
1457
-
1458
- // 方法2: 通过自定义事件传递
1459
- lf.on('edge:add', (data) => {
1460
- if (data.data.sourceNodeId) {
1461
- const sourceNode = lf.graphModel.getNodeModelById(data.data.sourceNodeId)
1462
- if (sourceNode.type === 'branch') {
1463
- updateBranchInfo(data.data.sourceNodeId)
1464
- }
1465
- }
1466
- })
1467
-
1468
- // 方法3: 在校验时读取
1469
- const validateBranchPaths = () => {
1470
- const branchNodes = lf.getGraphRawData().nodes.filter(n => n.type === 'branch')
1471
-
1472
- branchNodes.forEach(branchNode => {
1473
- const paths = lf.graphModel.getNodeOutgoingEdge(branchNode.id)
1474
- console.log(`分支节点 ${branchNode.id} 有 ${paths.length} 条路径`)
1475
- })
1476
- }
50
+ // 节点注册标准模式
51
+ register({
52
+ type: 'customNode',
53
+ model: CustomNodeModel, // 节点逻辑
54
+ view: CustomNodeView, // 锚点渲染
55
+ component: CustomNodeComponent // UI 组件
56
+ }, lfInstance)
1477
57
  ```
1478
58
 
1479
- ### 4. 动态调整节点样式
59
+ ### 常见节点模式
1480
60
 
1481
- ```typescript
1482
- // 根据节点状态动态改变样式
1483
- const updateNodeStyle = (nodeId: string, status: 'pending' | 'running' | 'completed' | 'error') => {
1484
- const nodeModel = lf.graphModel.getNodeModelById(nodeId)
1485
-
1486
- const styleMap = {
1487
- pending: { fill: '#e3f2fd', stroke: '#2196f3' },
1488
- running: { fill: '#fff3e0', stroke: '#ff9800' },
1489
- completed: { fill: '#e8f5e9', stroke: '#4caf50' },
1490
- error: { fill: '#ffebee', stroke: '#f56c6c' }
1491
- }
1492
-
1493
- const style = styleMap[status]
1494
- nodeModel.setProperties({ ...nodeModel.properties, status })
1495
- nodeModel.setAttributes(style)
1496
- }
61
+ | 模式 | 锚点配置 | 典型应用 |
62
+ |-----|---------|---------|
63
+ | 单向流 | 左入右出 | 审批步骤 |
64
+ | 分支 | 1入多出 | 条件分支 |
65
+ | 汇聚 | 多入1出 | 分支合并 |
66
+ | 起始 | 只有出口 | 流程开始 |
67
+ | 终止 | 只有入口 | 流程结束 |
1497
68
 
1498
- // 在流程执行时调用
1499
- lf.on('node:click', ({ data }) => {
1500
- // 模拟执行
1501
- updateNodeStyle(data.id, 'running')
1502
-
1503
- setTimeout(() => {
1504
- updateNodeStyle(data.id, 'completed')
1505
- }, 2000)
1506
- })
1507
- ```
69
+ ### 必须遵守
1508
70
 
1509
- ---
71
+ - ✅ 使用 Model-View-Component 三层架构
72
+ - ✅ 自定义节点继承 RectNodeModel
73
+ - ✅ 使用 `getConnectedSourceRules` 定义连线规则
1510
74
 
1511
- ## 📚 参考资源
75
+ ### 禁止
1512
76
 
1513
- - **LogicFlow 官方文档**: http://logic-flow.cn/
1514
- - **LogicFlow GitHub**: https://github.com/didi/LogicFlow
1515
- - **LogicFlow API 参考**: http://logic-flow.cn/api/
1516
- - **Vue Node Registry**: https://github.com/Logic-Flow/logicflow-node-registry-vue3
1517
- - **Dagre 布局算法**: https://github.com/dagrejs/dagre
77
+ - 直接操作 DOM
78
+ - 跳过 Model 层直接修改视图
79
+ - 硬编码节点尺寸(应根据内容动态计算)
1518
80
 
1519
81
  ---
1520
82
 
1521
- ## 🎯 开发流程指南
1522
-
1523
- **当用户提出 LogicFlow 相关需求时,请按以下步骤进行:**
1524
-
1525
- ### 步骤1: 理解需求
1526
- - 确认是新增节点类型、修改连接规则、还是校验逻辑
1527
- - 确认项目已有的节点类型和业务场景
1528
- - 确认是否需要特殊的视觉效果(如错误高亮)
1529
-
1530
- ### 步骤2: 分析现有代码
1531
- - 检查现有的节点模型定义
1532
- - 查看现有的校验规则
1533
- - 了解现有的事件监听机制
83
+ ## 📋 可用规范列表
1534
84
 
1535
- ### 步骤3: 设计方案
1536
- - 确定节点的锚点数量和位置
1537
- - 设计连接规则(能连什么,不能连什么)
1538
- - 设计校验规则(什么情况下报错)
1539
- - 设计节点组件的样式和交互
85
+ 通过 `get_standard_by_id({ id: 'xxx' })` 获取:
1540
86
 
1541
- ### 步骤4: 实现代码
1542
- - 创建节点模型(Model)
1543
- - 创建节点组件(Component,可选)
1544
- - 注册节点到 LogicFlow
1545
- - 添加到拖拽面板
1546
- - 实现事件监听
1547
- - 实现校验逻辑
1548
-
1549
- ### 步骤5: 测试验证
1550
- - 测试节点拖拽
1551
- - 测试连接规则
1552
- - 测试校验功能
1553
- - 测试错误高亮
1554
- - 测试只读模式
1555
-
1556
- ---
87
+ **核心规范**
88
+ - `logicflow` - LogicFlow 完整开发规范(含自定义节点、连线规则、校验系统)
1557
89
 
1558
- **核心原则**:
1559
- 1. 📐 **架构清晰**: Model-View-Component 分离
1560
- 2. 🔗 **规则完备**: 连接规则明确,校验逻辑严密
1561
- 3. 🎨 **视觉反馈**: 错误状态红色高亮,成功状态清晰提示
1562
- 4. 🔧 **可扩展性**: 易于添加新节点类型和校验规则
1563
- 5. 📝 **类型安全**: 完整的 TypeScript 类型定义
90
+ **配套规范**
91
+ - `vue3-composition` - Vue 3 集成
92
+ - `typescript-base` - TypeScript 基础
1564
93
 
1565
94
  ---
1566
95
 
1567
- **记住**:LogicFlow 的核心是图论 + 规则引擎。节点是顶点,边是有向边,校验是图的遍历和规则检查。理解这个本质,就能灵活应对各种需求。
96
+ **维护团队**: MTA工作室
97
+ **设计理念**: Agent 只提供获取指引,详细规范由 MCP 工具从 npm 包动态获取