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.
- package/agents/_TEMPLATE.md +153 -0
- package/agents/flutter.agent.md +118 -155
- package/agents/i18n.agent.md +50 -105
- package/agents/logicflow.agent.md +55 -1525
- package/agents/vue3.agent.md +84 -248
- package/agents/wechat-miniprogram.agent.md +48 -950
- package/dist/index.js +391 -443
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/standards/core/mandatory-rules.md +103 -0
- package/standards/frameworks/flutter.md +78 -0
- package/standards/libraries/logicflow.md +1007 -0
- package/standards/troubleshooting-cases/flutter/textfield-vertical-centering.md +107 -0
- package/standards/workflows/design-restoration-guide.md +164 -0
- package/standards/workflows/problem-diagnosis.md +68 -0
- package/standards/workflows/textfield-centering-guide.md +157 -0
- package/templates/config-templates/agents-section.md +9 -0
- package/templates/config-templates/custom-section.md +6 -0
- package/templates/config-templates/header.md +29 -0
- package/templates/config-templates/workflow-minimal.md +44 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
# LogicFlow 流程图开发规范
|
|
2
|
+
|
|
3
|
+
> 基于 LogicFlow 2.1+ 的流程图开发标准规范
|
|
4
|
+
> 适用于 Vue 3 + TypeScript 项目集成
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 🎯 核心架构原则
|
|
9
|
+
|
|
10
|
+
LogicFlow 基于 **Model-View-Component 三层架构**:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// 节点注册标准模式
|
|
14
|
+
import { register } from '@logicflow/vue-node-registry'
|
|
15
|
+
|
|
16
|
+
register({
|
|
17
|
+
type: 'customNode', // 节点类型标识
|
|
18
|
+
model: CustomNodeModel, // 节点逻辑模型(锚点、规则、属性)
|
|
19
|
+
view: CustomNodeView, // 节点视图(锚点渲染)
|
|
20
|
+
component: CustomNodeComponent // 节点 UI 组件(样式、内容)
|
|
21
|
+
}, lfInstance)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 核心概念
|
|
25
|
+
|
|
26
|
+
1. **Model(模型层)**:
|
|
27
|
+
- 定义节点的数据结构和行为逻辑
|
|
28
|
+
- 管理锚点位置和连接规则
|
|
29
|
+
- 控制节点的属性和状态
|
|
30
|
+
|
|
31
|
+
2. **View(视图层)**:
|
|
32
|
+
- 渲染节点的 SVG 结构
|
|
33
|
+
- 渲染锚点的视觉表现
|
|
34
|
+
- 处理节点的交互事件
|
|
35
|
+
|
|
36
|
+
3. **Component(组件层)**:
|
|
37
|
+
- 使用 Vue 组件定义节点的 UI
|
|
38
|
+
- 响应节点属性变化
|
|
39
|
+
- 提供丰富的交互体验
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📐 节点模式分类
|
|
44
|
+
|
|
45
|
+
根据业务需求选择合适的节点模式:
|
|
46
|
+
|
|
47
|
+
| 模式 | 锚点配置 | 连线规则 | 典型应用 |
|
|
48
|
+
|-----|---------|---------|---------|
|
|
49
|
+
| **单向流节点** | 左入右出 / 上入下出 | 单入单出 | 数据转换、审批步骤 |
|
|
50
|
+
| **分支节点** | 1入多出(动态锚点) | 单入多出,每个出口限1条 | 条件分支、并行处理 |
|
|
51
|
+
| **汇聚节点** | 多入1出(动态锚点) | 多入单出 | 分支合并、数据汇总 |
|
|
52
|
+
| **起始节点** | 只有出口 | 只能连出,限1条 | 流程开始、数据源 |
|
|
53
|
+
| **终止节点** | 只有入口 | 只能连入 | 流程结束、数据输出 |
|
|
54
|
+
| **双向节点** | 左右 / 上下对称 | 双向连接 | 数据交互、状态切换 |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🔧 节点模型开发
|
|
59
|
+
|
|
60
|
+
### 基础模板
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { RectNodeModel } from '@logicflow/core'
|
|
64
|
+
|
|
65
|
+
// 自定义节点模型基础模板
|
|
66
|
+
export class CustomNodeModel extends RectNodeModel {
|
|
67
|
+
// 是否实时更新(属性变化时重新渲染)
|
|
68
|
+
shouldUpdate() {
|
|
69
|
+
return true // 需要响应式更新时返回 true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 设置节点尺寸和样式
|
|
73
|
+
setAttributes() {
|
|
74
|
+
this.width = 200 // 根据内容调整
|
|
75
|
+
this.height = 80
|
|
76
|
+
|
|
77
|
+
// 可选:根据动态属性调整尺寸
|
|
78
|
+
if (this.properties?.branches) {
|
|
79
|
+
this.height = 60 + this.properties.branches.length * 20
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 连出规则(作为源节点)
|
|
84
|
+
getConnectedSourceRules() {
|
|
85
|
+
const rules = super.getConnectedSourceRules()
|
|
86
|
+
|
|
87
|
+
// 示例规则 1: 限制出口连线数量
|
|
88
|
+
const limitOutgoing = {
|
|
89
|
+
message: '该节点最多只能连出 X 条线',
|
|
90
|
+
validate: () => {
|
|
91
|
+
const { edges } = this.outgoing
|
|
92
|
+
return !(edges && edges.length >= X) // X 根据业务需求设置
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 示例规则 2: 限制连接的目标节点类型
|
|
97
|
+
const allowedTargets = {
|
|
98
|
+
message: '只能连接到特定类型的节点',
|
|
99
|
+
validate: (sourceNode, targetNode) => {
|
|
100
|
+
const allowedTypes = ['typeA', 'typeB']
|
|
101
|
+
return allowedTypes.includes(targetNode.type)
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 示例规则 3: 防止自环
|
|
106
|
+
const noSelfLoop = {
|
|
107
|
+
message: '节点不能连接自己',
|
|
108
|
+
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) =>
|
|
109
|
+
sourceNode.id !== targetNode.id
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 根据业务需求选择规则
|
|
113
|
+
rules.push(limitOutgoing, allowedTargets, noSelfLoop)
|
|
114
|
+
return rules
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 连入规则(作为目标节点)
|
|
118
|
+
getConnectedTargetRules() {
|
|
119
|
+
const rules = super.getConnectedTargetRules()
|
|
120
|
+
|
|
121
|
+
// 示例:限制入口连线数量
|
|
122
|
+
const limitIncoming = {
|
|
123
|
+
message: '该节点最多只能有 Y 条输入',
|
|
124
|
+
validate: () => {
|
|
125
|
+
const { edges } = this.incoming
|
|
126
|
+
return !(edges && edges.length >= Y)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
rules.push(limitIncoming)
|
|
131
|
+
return rules
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 定义锚点位置
|
|
135
|
+
getDefaultAnchor(): { x: number; y: number; id: string }[] {
|
|
136
|
+
const { id, x, y, width, height } = this
|
|
137
|
+
|
|
138
|
+
// 常见模式 1: 上下锚点(垂直流)
|
|
139
|
+
return [
|
|
140
|
+
{ x, y: y - height / 2, id: `${id}_top` }, // 上锚点
|
|
141
|
+
{ x, y: y + height / 2, id: `${id}_bottom` } // 下锚点
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
// 常见模式 2: 左右锚点(水平流)
|
|
145
|
+
// return [
|
|
146
|
+
// { x: x - width / 2, y, id: `${id}_left` },
|
|
147
|
+
// { x: x + width / 2, y, id: `${id}_right` }
|
|
148
|
+
// ]
|
|
149
|
+
|
|
150
|
+
// 常见模式 3: 四向锚点
|
|
151
|
+
// return [
|
|
152
|
+
// { x, y: y - height / 2, id: `${id}_top` },
|
|
153
|
+
// { x, y: y + height / 2, id: `${id}_bottom` },
|
|
154
|
+
// { x: x - width / 2, y, id: `${id}_left` },
|
|
155
|
+
// { x: x + width / 2, y, id: `${id}_right` }
|
|
156
|
+
// ]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 动态锚点节点(分支/并行)
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
export class BranchNodeModel extends RectNodeModel {
|
|
165
|
+
constructor(data: any, graphModel: any) {
|
|
166
|
+
// 自定义文本位置
|
|
167
|
+
data.text = {
|
|
168
|
+
value: typeof data.text === 'string' ? data.text : data?.text?.value,
|
|
169
|
+
x: data?.x + 50,
|
|
170
|
+
y: data?.y,
|
|
171
|
+
}
|
|
172
|
+
super(data, graphModel)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setAttributes() {
|
|
176
|
+
this.width = 120
|
|
177
|
+
this.height = 60
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 动态锚点生成
|
|
181
|
+
getDefaultAnchor(): { x: number; y: number; id: string }[] {
|
|
182
|
+
const { id, x, y, width, height } = this
|
|
183
|
+
const { branches = [] } = this.properties as { branches?: { anchorId: string; index: number }[] }
|
|
184
|
+
|
|
185
|
+
const anchors: { x: number; y: number; id: string }[] = [
|
|
186
|
+
{ x, y: y - height / 2, id: `${id}_top` } // 固定上锚点
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
// 动态生成多个下锚点
|
|
190
|
+
const branchCount = branches.length || 2 // 默认 2 个分支
|
|
191
|
+
const spacing = width / (branchCount + 1)
|
|
192
|
+
|
|
193
|
+
branches.forEach((branch, index) => {
|
|
194
|
+
anchors.push({
|
|
195
|
+
x: x - width / 2 + spacing * (index + 1),
|
|
196
|
+
y: y + height / 2,
|
|
197
|
+
id: `${id}${branch.anchorId}`
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return anchors
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 单个锚点只能连出一条线
|
|
205
|
+
getConnectedSourceRules() {
|
|
206
|
+
const rules = super.getConnectedSourceRules()
|
|
207
|
+
|
|
208
|
+
const onlyUniqueSource = {
|
|
209
|
+
message: '分支锚点只能与一个节点相连',
|
|
210
|
+
validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
|
|
211
|
+
const { edges } = this.outgoing
|
|
212
|
+
const isHaveBranchEdge = edges && edges.some(edge =>
|
|
213
|
+
edge.sourceAnchorId === sourceAnchor.id
|
|
214
|
+
)
|
|
215
|
+
return !isHaveBranchEdge
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
rules.push(onlyUniqueSource)
|
|
220
|
+
return rules
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 🎨 节点组件开发
|
|
228
|
+
|
|
229
|
+
### 通用节点组件模板
|
|
230
|
+
|
|
231
|
+
```vue
|
|
232
|
+
<template>
|
|
233
|
+
<section class="viewport">
|
|
234
|
+
<!-- 普通节点:显示类型和名称 -->
|
|
235
|
+
<div v-if="!isSpecialType"
|
|
236
|
+
:class="['custom-node', `custom-node-${data.type}`, { 'error-node': data.properties?.isError }]">
|
|
237
|
+
<div class="node-header">
|
|
238
|
+
<span class="node-type">{{ getNodeTypeLabel(data.type) }}</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="node-content">
|
|
241
|
+
<span class="node-name">{{ data.properties.name || 'Unnamed' }}</span>
|
|
242
|
+
<span v-if="data.properties.description" class="node-desc">
|
|
243
|
+
{{ data.properties.description }}
|
|
244
|
+
</span>
|
|
245
|
+
</div>
|
|
246
|
+
<!-- 错误状态指示 -->
|
|
247
|
+
<div v-if="data.properties.hasError" class="error-indicator">⚠️</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<!-- 特殊节点:如分支、合并等 -->
|
|
251
|
+
<div v-else :class="`${data.type} custom-node-${data.type}`">
|
|
252
|
+
<div :class="`center-line center-line-${data.type}`"></div>
|
|
253
|
+
<div v-if="needsParallelIndicator(data.type)" class="parallel-indicator"></div>
|
|
254
|
+
</div>
|
|
255
|
+
</section>
|
|
256
|
+
</template>
|
|
257
|
+
|
|
258
|
+
<script setup lang="ts">
|
|
259
|
+
import { ref, computed, onMounted } from 'vue'
|
|
260
|
+
import { EventType } from '@logicflow/core'
|
|
261
|
+
|
|
262
|
+
interface Props {
|
|
263
|
+
node: any
|
|
264
|
+
graph: any
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const props = defineProps<Props>()
|
|
268
|
+
const data = ref({ ...props.graph.getNodeModelById(props.node.id) })
|
|
269
|
+
|
|
270
|
+
// 定义特殊节点类型(需要特殊渲染的)
|
|
271
|
+
const specialTypes = ['branch', 'branchEnd', 'parallel', 'parallelEnd', 'gateway']
|
|
272
|
+
const isSpecialType = computed(() => specialTypes.includes(data.value.type))
|
|
273
|
+
|
|
274
|
+
// 获取节点类型的显示文本(可从配置或i18n获取)
|
|
275
|
+
const getNodeTypeLabel = (type: string) => {
|
|
276
|
+
const labelMap = {
|
|
277
|
+
start: '开始',
|
|
278
|
+
end: '结束',
|
|
279
|
+
task: '任务',
|
|
280
|
+
decision: '决策'
|
|
281
|
+
// 根据项目需求扩展...
|
|
282
|
+
}
|
|
283
|
+
return labelMap[type] || type
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 判断是否需要并行指示器
|
|
287
|
+
const needsParallelIndicator = (type: string) => {
|
|
288
|
+
return type.includes('parallel')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 监听节点属性变化
|
|
292
|
+
onMounted(() => {
|
|
293
|
+
const eventHandler = (eventData: any) => {
|
|
294
|
+
if (eventData.id === props.node.id) {
|
|
295
|
+
data.value.properties = eventData?.properties
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
props.graph.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, eventHandler)
|
|
300
|
+
|
|
301
|
+
return () => {
|
|
302
|
+
props.graph.eventCenter.off(EventType.NODE_PROPERTIES_CHANGE, eventHandler)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
</script>
|
|
306
|
+
|
|
307
|
+
<style scoped>
|
|
308
|
+
.custom-node {
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
background: #fff;
|
|
312
|
+
border: 2px solid #ddd;
|
|
313
|
+
border-radius: 4px;
|
|
314
|
+
padding: 8px;
|
|
315
|
+
transition: border-color 0.3s;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.node-header {
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
color: #666;
|
|
321
|
+
margin-bottom: 4px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.node-content {
|
|
325
|
+
font-size: 14px;
|
|
326
|
+
color: #333;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* 错误状态:红色边框 + 脉冲动画 */
|
|
330
|
+
.error-node {
|
|
331
|
+
border-color: #f56c6c !important;
|
|
332
|
+
animation: error-pulse 1s ease-in-out infinite;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@keyframes error-pulse {
|
|
336
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.4); }
|
|
337
|
+
50% { box-shadow: 0 0 0 6px rgba(245, 108, 108, 0); }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.error-indicator {
|
|
341
|
+
position: absolute;
|
|
342
|
+
top: -8px;
|
|
343
|
+
right: -8px;
|
|
344
|
+
color: #f56c6c;
|
|
345
|
+
}
|
|
346
|
+
</style>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 🔧 流程图主组件集成
|
|
352
|
+
|
|
353
|
+
### 初始化流程
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import LogicFlow from '@logicflow/core'
|
|
357
|
+
import { Control, Menu, DndPanel, SelectionSelect } from '@logicflow/extension'
|
|
358
|
+
import { Dagre } from '@logicflow/layout'
|
|
359
|
+
import { register, getTeleport } from '@logicflow/vue-node-registry'
|
|
360
|
+
|
|
361
|
+
// 注册插件
|
|
362
|
+
LogicFlow.use(Control) // 控制面板(放大/缩小/还原)
|
|
363
|
+
LogicFlow.use(Menu) // 右键菜单
|
|
364
|
+
LogicFlow.use(DndPanel) // 拖拽面板
|
|
365
|
+
LogicFlow.use(SelectionSelect) // 框选
|
|
366
|
+
LogicFlow.use(Dagre) // 自动布局
|
|
367
|
+
|
|
368
|
+
let lf: any = null
|
|
369
|
+
|
|
370
|
+
const renderLf = async () => {
|
|
371
|
+
lf = new LogicFlow({
|
|
372
|
+
container: document.querySelector('.vita-flow') as HTMLElement,
|
|
373
|
+
grid: true, // 显示网格
|
|
374
|
+
plugins: [Dagre],
|
|
375
|
+
keyboard: { enabled: true }, // 启用键盘快捷键
|
|
376
|
+
isSilentMode: props.read, // 只读模式
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// 注册自定义节点
|
|
380
|
+
initFlow(props.render, lf, $t, props.read, menuCallback.value, menuItem.value, addItem.value)
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### 事件监听
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// 定义事件处理器配置
|
|
388
|
+
const setupEventListeners = (lf: any, options: {
|
|
389
|
+
onNodeClick?: (node: any) => void
|
|
390
|
+
onNodeDoubleClick?: (node: any) => void
|
|
391
|
+
onEdgeChange?: () => void
|
|
392
|
+
onConnectionNotAllowed?: (data: any) => void
|
|
393
|
+
shouldAutoValidate?: boolean
|
|
394
|
+
}) => {
|
|
395
|
+
let clickTimer: NodeJS.Timeout | null = null
|
|
396
|
+
|
|
397
|
+
// 连线规则校验失败
|
|
398
|
+
lf.on('connection:not-allowed', (data: any) => {
|
|
399
|
+
if (options.onConnectionNotAllowed) {
|
|
400
|
+
options.onConnectionNotAllowed(data)
|
|
401
|
+
} else {
|
|
402
|
+
console.warn('连接不被允许:', data?.msg)
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// 单击节点
|
|
407
|
+
lf.on('node:click', (e: any) => {
|
|
408
|
+
if (clickTimer) clearTimeout(clickTimer)
|
|
409
|
+
clickTimer = setTimeout(() => {
|
|
410
|
+
clickTimer = null
|
|
411
|
+
console.log('单击节点', e?.data)
|
|
412
|
+
options.onNodeClick?.(e.data)
|
|
413
|
+
}, 200)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// 双击节点(打开配置对话框)
|
|
417
|
+
lf.on('node:dbclick', (e: any) => {
|
|
418
|
+
if (clickTimer) {
|
|
419
|
+
clearTimeout(clickTimer)
|
|
420
|
+
clickTimer = null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
console.log('双击节点', e?.data)
|
|
424
|
+
options.onNodeDoubleClick?.(e.data)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// 连线变化 - 可选的自动校验
|
|
428
|
+
if (options.shouldAutoValidate) {
|
|
429
|
+
lf.on('edge:add', () => {
|
|
430
|
+
setTimeout(() => options.onEdgeChange?.(), 100)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
lf.on('edge:delete', () => {
|
|
434
|
+
setTimeout(() => options.onEdgeChange?.(), 100)
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 使用示例
|
|
440
|
+
setupEventListeners(lf, {
|
|
441
|
+
onNodeClick: (node) => {
|
|
442
|
+
// 处理节点点击
|
|
443
|
+
selectNode(node.id)
|
|
444
|
+
},
|
|
445
|
+
onNodeDoubleClick: (node) => {
|
|
446
|
+
// 打开编辑器
|
|
447
|
+
openNodeEditor(node)
|
|
448
|
+
},
|
|
449
|
+
onEdgeChange: () => {
|
|
450
|
+
// 重新校验流程图
|
|
451
|
+
validateGraph()
|
|
452
|
+
},
|
|
453
|
+
onConnectionNotAllowed: (data) => {
|
|
454
|
+
showMessage({ type: 'warning', message: data?.msg })
|
|
455
|
+
},
|
|
456
|
+
shouldAutoValidate: true
|
|
457
|
+
})
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 节点拖拽监听
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
lf.on('node:dnd-add', (data: any) => {
|
|
464
|
+
const rawData = lf.getGraphRawData()
|
|
465
|
+
const node = lf.graphModel.getNodeModelById(data?.data?.id)
|
|
466
|
+
|
|
467
|
+
// 示例 1: 限制特定节点的数量
|
|
468
|
+
if (needUnique(data.data.type)) {
|
|
469
|
+
const existingNodes = rawData.nodes.filter(n => n.type === data.data.type)
|
|
470
|
+
if (existingNodes.length > 1) {
|
|
471
|
+
lf.deleteNode(data.data.id)
|
|
472
|
+
showMessage({ type: 'warning', message: `只能有一个${data.data.type}节点` })
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 示例 2: 自动命名新节点
|
|
478
|
+
if (needAutoName(data.data.type)) {
|
|
479
|
+
const sameTypeNodes = rawData.nodes.filter(n => n.type === data.data.type)
|
|
480
|
+
const index = sameTypeNodes.length
|
|
481
|
+
node.setProperty('name', `${data.data.type}_${String(index).padStart(3, '0')}`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 示例 3: 初始化节点属性
|
|
485
|
+
if (needInitProperties(data.data.type)) {
|
|
486
|
+
node.setProperty('status', 'pending')
|
|
487
|
+
node.setProperty('createdAt', Date.now())
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## ✅ 流程图校验系统
|
|
495
|
+
|
|
496
|
+
> **核心特性**:校验失败时,不符合规则的节点和连线会自动变红高亮,直观显示错误位置
|
|
497
|
+
|
|
498
|
+
### 校验触发时机
|
|
499
|
+
|
|
500
|
+
1. 点击"校验"按钮 → 执行校验 → 高亮错误
|
|
501
|
+
2. 点击"提交"按钮 → 先校验 → 失败则高亮错误并阻止提交
|
|
502
|
+
3. 添加/删除连线时 → 自动重新校验 → 更新高亮状态
|
|
503
|
+
|
|
504
|
+
### 前端结构校验
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
export interface ValidationResult {
|
|
508
|
+
valid: boolean
|
|
509
|
+
errors: ValidationError[]
|
|
510
|
+
errorEdges: string[] // 错误的边 ID
|
|
511
|
+
errorNodes: string[] // 错误的节点 ID
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export interface ValidationError {
|
|
515
|
+
type: 'isolated_node' | 'isolated_path' | 'circular_path' | 'unconnected_anchor' | 'no_start' | 'no_end' | 'unreachable'
|
|
516
|
+
message: string
|
|
517
|
+
nodeIds?: string[]
|
|
518
|
+
edgeIds?: string[]
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export class FlowValidator {
|
|
522
|
+
private nodes: any[]
|
|
523
|
+
private edges: any[]
|
|
524
|
+
private adjacencyList: Map<string, string[]> // 邻接表
|
|
525
|
+
private reverseAdjacencyList: Map<string, string[]> // 反向邻接表
|
|
526
|
+
private startNode: any | null = null
|
|
527
|
+
private endNode: any | null = null
|
|
528
|
+
|
|
529
|
+
constructor(graphData: GraphData) {
|
|
530
|
+
this.nodes = graphData.nodes || []
|
|
531
|
+
this.edges = graphData.edges || []
|
|
532
|
+
this.buildAdjacencyLists()
|
|
533
|
+
this.findStartEndNodes()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
public validate(): ValidationResult {
|
|
537
|
+
const errors: ValidationError[] = []
|
|
538
|
+
const errorEdges: string[] = []
|
|
539
|
+
const errorNodes: string[] = []
|
|
540
|
+
|
|
541
|
+
// 1. 检查是否存在 start 和 end 节点
|
|
542
|
+
if (!this.startNode) {
|
|
543
|
+
errors.push({ type: 'no_start', message: '缺少开始节点' })
|
|
544
|
+
}
|
|
545
|
+
if (!this.endNode) {
|
|
546
|
+
errors.push({ type: 'no_end', message: '缺少结束节点' })
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!this.startNode || !this.endNode) {
|
|
550
|
+
return { valid: false, errors, errorEdges, errorNodes }
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 2. 检查从 start 可达的节点(BFS)
|
|
554
|
+
const reachableFromStart = this.getReachableNodes(this.startNode.id, this.adjacencyList)
|
|
555
|
+
|
|
556
|
+
// 3. 检查可以到达 end 的节点(反向 BFS)
|
|
557
|
+
const canReachEnd = this.getReachableNodes(this.endNode.id, this.reverseAdjacencyList)
|
|
558
|
+
|
|
559
|
+
// 4. 计算主路径节点(既能从 start 到达,又能到达 end)
|
|
560
|
+
const mainPathNodes = new Set(
|
|
561
|
+
[...reachableFromStart].filter(nodeId => canReachEnd.has(nodeId))
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
// 5. 检查孤立节点(不在主路径上)
|
|
565
|
+
const isolatedNodes = this.nodes.filter(node => {
|
|
566
|
+
if (node.type === 'start' || node.type === 'end') return false
|
|
567
|
+
return !mainPathNodes.has(node.id)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
if (isolatedNodes.length > 0) {
|
|
571
|
+
const isolatedNodeIds = isolatedNodes.map(n => n.id)
|
|
572
|
+
errors.push({
|
|
573
|
+
type: 'isolated_node',
|
|
574
|
+
message: `发现 ${isolatedNodes.length} 个孤立节点(不在 start 到 end 的有效路径上)`,
|
|
575
|
+
nodeIds: isolatedNodeIds,
|
|
576
|
+
})
|
|
577
|
+
errorNodes.push(...isolatedNodeIds)
|
|
578
|
+
|
|
579
|
+
// 找到连接到孤立节点的边
|
|
580
|
+
const isolatedEdges = this.edges.filter(edge =>
|
|
581
|
+
isolatedNodeIds.includes(edge.sourceNodeId) ||
|
|
582
|
+
isolatedNodeIds.includes(edge.targetNodeId)
|
|
583
|
+
)
|
|
584
|
+
errorEdges.push(...isolatedEdges.map(e => e.id))
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 6. 检查回路(DFS)
|
|
588
|
+
const cycles = this.detectCycles(mainPathNodes)
|
|
589
|
+
if (cycles.length > 0) {
|
|
590
|
+
cycles.forEach(cycle => {
|
|
591
|
+
errors.push({
|
|
592
|
+
type: 'circular_path',
|
|
593
|
+
message: `检测到回路:${cycle.join(' → ')}`,
|
|
594
|
+
nodeIds: cycle,
|
|
595
|
+
})
|
|
596
|
+
errorNodes.push(...cycle)
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 7. 检查 end 节点是否可达
|
|
601
|
+
if (!reachableFromStart.has(this.endNode.id)) {
|
|
602
|
+
errors.push({
|
|
603
|
+
type: 'unreachable',
|
|
604
|
+
message: '无法从开始节点到达结束节点',
|
|
605
|
+
nodeIds: [this.endNode.id],
|
|
606
|
+
})
|
|
607
|
+
errorNodes.push(this.endNode.id)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
valid: errors.length === 0,
|
|
612
|
+
errors,
|
|
613
|
+
errorEdges: [...new Set(errorEdges)],
|
|
614
|
+
errorNodes: [...new Set(errorNodes)],
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// BFS 获取可达节点
|
|
619
|
+
private getReachableNodes(startNodeId: string, adjacency: Map<string, string[]>): Set<string> {
|
|
620
|
+
const visited = new Set<string>()
|
|
621
|
+
const queue: string[] = [startNodeId]
|
|
622
|
+
visited.add(startNodeId)
|
|
623
|
+
|
|
624
|
+
while (queue.length > 0) {
|
|
625
|
+
const current = queue.shift()!
|
|
626
|
+
const neighbors = adjacency.get(current) || []
|
|
627
|
+
|
|
628
|
+
neighbors.forEach(neighbor => {
|
|
629
|
+
if (!visited.has(neighbor)) {
|
|
630
|
+
visited.add(neighbor)
|
|
631
|
+
queue.push(neighbor)
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return visited
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// DFS 检测回路
|
|
640
|
+
private detectCycles(validNodes: Set<string>): string[][] {
|
|
641
|
+
const visited = new Set<string>()
|
|
642
|
+
const recursionStack = new Set<string>()
|
|
643
|
+
const cycles: string[][] = []
|
|
644
|
+
|
|
645
|
+
const dfs = (nodeId: string): boolean => {
|
|
646
|
+
visited.add(nodeId)
|
|
647
|
+
recursionStack.add(nodeId)
|
|
648
|
+
|
|
649
|
+
const neighbors = (this.adjacencyList.get(nodeId) || [])
|
|
650
|
+
.filter(n => validNodes.has(n))
|
|
651
|
+
|
|
652
|
+
for (const neighbor of neighbors) {
|
|
653
|
+
if (!visited.has(neighbor)) {
|
|
654
|
+
if (dfs(neighbor)) return true
|
|
655
|
+
} else if (recursionStack.has(neighbor)) {
|
|
656
|
+
// 发现回路
|
|
657
|
+
const cycle: string[] = []
|
|
658
|
+
let current = nodeId
|
|
659
|
+
cycle.push(neighbor)
|
|
660
|
+
while (current !== neighbor) {
|
|
661
|
+
cycle.push(current)
|
|
662
|
+
current = this.getParent(current, neighbor)
|
|
663
|
+
}
|
|
664
|
+
cycles.push(cycle.reverse())
|
|
665
|
+
return true
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
recursionStack.delete(nodeId)
|
|
670
|
+
return false
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
validNodes.forEach(nodeId => {
|
|
674
|
+
if (!visited.has(nodeId)) {
|
|
675
|
+
dfs(nodeId)
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
return cycles
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 调用校验与错误高亮
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
const validateGraph = async () => {
|
|
688
|
+
try {
|
|
689
|
+
if (!lf) return false
|
|
690
|
+
|
|
691
|
+
const rawData = lf.getGraphRawData()
|
|
692
|
+
const { nodes, edges } = rawData
|
|
693
|
+
|
|
694
|
+
// 1. 前端结构校验
|
|
695
|
+
const frontendValidation = validateFlow(rawData)
|
|
696
|
+
|
|
697
|
+
// 核心:高亮错误的边和节点
|
|
698
|
+
highlightErrors(frontendValidation)
|
|
699
|
+
|
|
700
|
+
if (!frontendValidation.valid) {
|
|
701
|
+
const errorMessages = frontendValidation.errors.map(err => err.message).join('\n')
|
|
702
|
+
showMessage({
|
|
703
|
+
type: 'error',
|
|
704
|
+
message: `流程图校验失败:\n${errorMessages}`,
|
|
705
|
+
duration: 5000
|
|
706
|
+
})
|
|
707
|
+
return false
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 2. (可选) 调用后端业务校验
|
|
711
|
+
const backendResult = await callBackendValidation(nodes, edges)
|
|
712
|
+
if (!backendResult.success) {
|
|
713
|
+
showMessage({
|
|
714
|
+
type: 'error',
|
|
715
|
+
message: backendResult.message || '后端校验失败'
|
|
716
|
+
})
|
|
717
|
+
return false
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 3. 校验通过,清除高亮
|
|
721
|
+
clearErrorHighlight()
|
|
722
|
+
showMessage({ type: 'success', message: '校验通过' })
|
|
723
|
+
return true
|
|
724
|
+
} catch (err) {
|
|
725
|
+
console.error('Validation error:', err)
|
|
726
|
+
showMessage({ type: 'error', message: '校验失败' })
|
|
727
|
+
return false
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 核心功能:高亮错误节点和连线
|
|
732
|
+
const highlightErrors = (validation: ValidationResult) => {
|
|
733
|
+
if (!lf) return
|
|
734
|
+
|
|
735
|
+
// 清除之前的高亮
|
|
736
|
+
clearErrorHighlight()
|
|
737
|
+
|
|
738
|
+
// 高亮错误的边(变红色,加粗)
|
|
739
|
+
validation.errorEdges.forEach(edgeId => {
|
|
740
|
+
const edgeModel = lf.graphModel.getEdgeModelById(edgeId)
|
|
741
|
+
if (edgeModel) {
|
|
742
|
+
edgeModel.setProperties({ ...edgeModel.properties, isError: true })
|
|
743
|
+
edgeModel.setAttributes({
|
|
744
|
+
stroke: '#f56c6c', // 红色
|
|
745
|
+
strokeWidth: 2 // 加粗至 2px
|
|
746
|
+
})
|
|
747
|
+
}
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
// 高亮错误的节点(红色边框)
|
|
751
|
+
validation.errorNodes.forEach(nodeId => {
|
|
752
|
+
const nodeModel = lf.graphModel.getNodeModelById(nodeId)
|
|
753
|
+
if (nodeModel) {
|
|
754
|
+
nodeModel.setProperties({ ...nodeModel.properties, isError: true })
|
|
755
|
+
// 注意:节点红色边框通过 CSS 样式实现
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// 清除错误高亮
|
|
761
|
+
const clearErrorHighlight = () => {
|
|
762
|
+
if (!lf) return
|
|
763
|
+
|
|
764
|
+
const rawData = lf.getGraphRawData()
|
|
765
|
+
|
|
766
|
+
// 清除所有边的错误状态
|
|
767
|
+
rawData.edges.forEach((edge: any) => {
|
|
768
|
+
const edgeModel = lf.graphModel.getEdgeModelById(edge.id)
|
|
769
|
+
if (edgeModel && edgeModel.properties?.isError) {
|
|
770
|
+
edgeModel.setProperties({ ...edgeModel.properties, isError: false })
|
|
771
|
+
edgeModel.setAttributes({
|
|
772
|
+
stroke: '#000000', // 恢复默认颜色
|
|
773
|
+
strokeWidth: 1 // 恢复默认宽度
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
// 清除所有节点的错误状态
|
|
779
|
+
rawData.nodes.forEach((node: any) => {
|
|
780
|
+
const nodeModel = lf.graphModel.getNodeModelById(node.id)
|
|
781
|
+
if (nodeModel && nodeModel.properties?.isError) {
|
|
782
|
+
nodeModel.setProperties({ ...nodeModel.properties, isError: false })
|
|
783
|
+
}
|
|
784
|
+
})
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## 🎨 右键菜单与工具栏
|
|
791
|
+
|
|
792
|
+
### 右键菜单配置
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
const setMenu = (lf: any, $t: any, menuCallback: any) => {
|
|
796
|
+
const hidePublic = ['start', 'end']
|
|
797
|
+
const publicMenu = [
|
|
798
|
+
{ text: $t('删除'), callback: (node: any) => lf.deleteNode(node.id) },
|
|
799
|
+
{ text: $t('复制'), callback: (node: any) => lf.graphModel.cloneNode(node.id) }
|
|
800
|
+
]
|
|
801
|
+
|
|
802
|
+
const menus = [
|
|
803
|
+
{
|
|
804
|
+
type: 'start',
|
|
805
|
+
menu: [
|
|
806
|
+
{ text: $t('编辑文本'), callback: (node: any) => lf.graphModel.setElementStateById(node.id, 2) }
|
|
807
|
+
]
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
type: 'branch',
|
|
811
|
+
menu: [
|
|
812
|
+
{ text: $t('添加分支锚点'), callback: (node: any) => menuCallback.addBranch(node) },
|
|
813
|
+
{ text: $t('减少分支锚点'), callback: (node: any) => menuCallback.removeBranch(node) }
|
|
814
|
+
]
|
|
815
|
+
},
|
|
816
|
+
]
|
|
817
|
+
|
|
818
|
+
menus.forEach(item => {
|
|
819
|
+
if (!hidePublic.includes(item.type)) {
|
|
820
|
+
publicMenu.forEach(i => item.menu.push(i))
|
|
821
|
+
}
|
|
822
|
+
lf.setMenuByType(item)
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### 动态锚点管理
|
|
828
|
+
|
|
829
|
+
```typescript
|
|
830
|
+
// logicFunc.ts
|
|
831
|
+
export default {
|
|
832
|
+
// 添加分支锚点
|
|
833
|
+
addBranch: (node: any, lf: any) => {
|
|
834
|
+
const newBranch = {
|
|
835
|
+
anchorId: `_bottom_${(node.properties.branches || []).length + 1}`,
|
|
836
|
+
index: (node.properties.branches || []).length,
|
|
837
|
+
}
|
|
838
|
+
const newBranches = (node.properties.branches || []).concat(newBranch)
|
|
839
|
+
const nodeModel = lf.graphModel.getNodeModelById(node.id)
|
|
840
|
+
const edges = lf.graphModel.getNodeEdges(node.id)
|
|
841
|
+
|
|
842
|
+
nodeModel.setProperty('branches', newBranches)
|
|
843
|
+
|
|
844
|
+
// 更新连线起点位置
|
|
845
|
+
setTimeout(() => {
|
|
846
|
+
edges.forEach(edge => {
|
|
847
|
+
nodeModel?.anchors?.forEach(fil => {
|
|
848
|
+
if (fil.id === edge.sourceAnchorId) {
|
|
849
|
+
edge.updateStartPoint({ x: fil?.x, y: fil?.y })
|
|
850
|
+
}
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
}, 10)
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
// 移除分支锚点
|
|
857
|
+
removeBranch: (node: any, lf: any, $t: any) => {
|
|
858
|
+
const newBranches = (node.properties.branches || [])
|
|
859
|
+
|
|
860
|
+
if (newBranches.length && newBranches.length > 2) {
|
|
861
|
+
const popEdge = newBranches.pop()
|
|
862
|
+
const nodeModel = lf.graphModel.getNodeModelById(node.id)
|
|
863
|
+
|
|
864
|
+
nodeModel.setProperty('branches', newBranches)
|
|
865
|
+
|
|
866
|
+
const edges = lf.graphModel.getNodeEdges(node.id)
|
|
867
|
+
setTimeout(() => {
|
|
868
|
+
edges.forEach(edge => {
|
|
869
|
+
nodeModel?.anchors?.forEach(fil => {
|
|
870
|
+
if (fil.id === edge.sourceAnchorId) {
|
|
871
|
+
edge.updateStartPoint({ x: fil?.x, y: fil?.y })
|
|
872
|
+
}
|
|
873
|
+
if (`${node.id}${popEdge.anchorId}` === edge.sourceAnchorId) {
|
|
874
|
+
lf.graphModel.deleteEdgeById(edge.id)
|
|
875
|
+
}
|
|
876
|
+
})
|
|
877
|
+
})
|
|
878
|
+
}, 10)
|
|
879
|
+
} else {
|
|
880
|
+
ElMessage.warning($t('没有可以删除的分支锚点'))
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
## 📐 自动布局
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
const autoSort = (type = 'LR') => {
|
|
892
|
+
if (!lf) return
|
|
893
|
+
|
|
894
|
+
const newRender = { ...lf.getGraphRawData() }
|
|
895
|
+
|
|
896
|
+
// 先对边排序
|
|
897
|
+
lf.render({
|
|
898
|
+
nodes: newRender?.nodes,
|
|
899
|
+
edges: logicFunc.sortPolylinesByAnchorSuffix(newRender?.edges)
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
// 使用 Dagre 自动布局
|
|
903
|
+
setTimeout(() => {
|
|
904
|
+
lf.extension.dagre.layout({
|
|
905
|
+
rankdir: type, // 'TB' 垂直,'LR' 水平
|
|
906
|
+
ranker: 'longest-path', // 布局算法
|
|
907
|
+
align: undefined, // 对齐方式
|
|
908
|
+
nodesep: 60, // 节点间距
|
|
909
|
+
ranksep: 70, // 层级间距
|
|
910
|
+
isDefaultAnchor: false, // 不使用默认锚点
|
|
911
|
+
})
|
|
912
|
+
}, 100)
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
---
|
|
917
|
+
|
|
918
|
+
## 💾 数据提交
|
|
919
|
+
|
|
920
|
+
```typescript
|
|
921
|
+
const getData = async () => {
|
|
922
|
+
if (!lf) return
|
|
923
|
+
|
|
924
|
+
// 先执行校验
|
|
925
|
+
const isValid = await checkData()
|
|
926
|
+
if (!isValid) {
|
|
927
|
+
ElMessage.warning($t('请先修复流程图中的错误'))
|
|
928
|
+
return
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// 获取画布数据
|
|
932
|
+
const rawData = lf.getGraphRawData()
|
|
933
|
+
emit('flowSubmit', { rawData: rawData })
|
|
934
|
+
}
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## ⚠️ 常见问题与解决方案
|
|
940
|
+
|
|
941
|
+
### 1. 节点属性不更新
|
|
942
|
+
|
|
943
|
+
```typescript
|
|
944
|
+
// ❌ 错误:直接修改 properties
|
|
945
|
+
node.properties.name = 'new name'
|
|
946
|
+
|
|
947
|
+
// ✅ 正确:使用 setProperty
|
|
948
|
+
const nodeModel = lf.graphModel.getNodeModelById(nodeId)
|
|
949
|
+
nodeModel.setProperty('name', 'new name')
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### 2. 锚点位置错乱
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
// ✅ 添加/删除锚点后,必须更新连线起点
|
|
956
|
+
setTimeout(() => {
|
|
957
|
+
edges.forEach(edge => {
|
|
958
|
+
nodeModel?.anchors?.forEach(anchor => {
|
|
959
|
+
if (anchor.id === edge.sourceAnchorId) {
|
|
960
|
+
edge.updateStartPoint({ x: anchor.x, y: anchor.y })
|
|
961
|
+
}
|
|
962
|
+
})
|
|
963
|
+
})
|
|
964
|
+
}, 10)
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### 3. 只读模式下右键菜单仍显示
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
// ✅ 初始化时设置只读模式
|
|
971
|
+
lf = new LogicFlow({
|
|
972
|
+
container: document.querySelector('.flow-container') as HTMLElement,
|
|
973
|
+
grid: true,
|
|
974
|
+
isSilentMode: isReadOnly, // 只读模式
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
// ✅ 只读模式下不设置菜单和拖拽面板
|
|
978
|
+
if (!isReadOnly) {
|
|
979
|
+
setupContextMenu(lf, nodeTypes, callbacks)
|
|
980
|
+
lf.extension.dndPanel.setPatternItems(patternItems)
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
---
|
|
985
|
+
|
|
986
|
+
## 📚 参考资源
|
|
987
|
+
|
|
988
|
+
- **LogicFlow 官方文档**: http://logic-flow.cn/
|
|
989
|
+
- **LogicFlow GitHub**: https://github.com/didi/LogicFlow
|
|
990
|
+
- **LogicFlow API 参考**: http://logic-flow.cn/api/
|
|
991
|
+
- **Vue Node Registry**: https://github.com/Logic-Flow/logicflow-node-registry-vue3
|
|
992
|
+
- **Dagre 布局算法**: https://github.com/dagrejs/dagre
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
**核心原则**:
|
|
997
|
+
1. 📐 **架构清晰**: Model-View-Component 分离
|
|
998
|
+
2. 🔗 **规则完备**: 连接规则明确,校验逻辑严密
|
|
999
|
+
3. 🎨 **视觉反馈**: 错误状态红色高亮,成功状态清晰提示
|
|
1000
|
+
4. 🔧 **可扩展性**: 易于添加新节点类型和校验规则
|
|
1001
|
+
5. 📝 **类型安全**: 完整的 TypeScript 类型定义
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
**维护者**: MTA工作室
|
|
1006
|
+
**创建日期**: 2025-12-16
|
|
1007
|
+
**最后更新**: 2025-12-16
|