# LogicFlow 流程图开发规范

> 基于 LogicFlow 2.1+ 的流程图开发标准规范
> 适用于 Vue 3 + TypeScript 项目集成

---

## 🎯 核心架构原则

LogicFlow 基于 **Model-View-Component 三层架构**：

```typescript
// 节点注册标准模式
import { register } from '@logicflow/vue-node-registry'

register({
  type: 'customNode',              // 节点类型标识
  model: CustomNodeModel,          // 节点逻辑模型（锚点、规则、属性）
  view: CustomNodeView,            // 节点视图（锚点渲染）
  component: CustomNodeComponent   // 节点 UI 组件（样式、内容）
}, lfInstance)
```

### 核心概念

1. **Model（模型层）**：
   - 定义节点的数据结构和行为逻辑
   - 管理锚点位置和连接规则
   - 控制节点的属性和状态

2. **View（视图层）**：
   - 渲染节点的 SVG 结构
   - 渲染锚点的视觉表现
   - 处理节点的交互事件

3. **Component（组件层）**：
   - 使用 Vue 组件定义节点的 UI
   - 响应节点属性变化
   - 提供丰富的交互体验

---

## 📐 节点模式分类

根据业务需求选择合适的节点模式：

| 模式 | 锚点配置 | 连线规则 | 典型应用 |
|-----|---------|---------|---------|
| **单向流节点** | 左入右出 / 上入下出 | 单入单出 | 数据转换、审批步骤 |
| **分支节点** | 1入多出（动态锚点） | 单入多出，每个出口限1条 | 条件分支、并行处理 |
| **汇聚节点** | 多入1出（动态锚点） | 多入单出 | 分支合并、数据汇总 |
| **起始节点** | 只有出口 | 只能连出，限1条 | 流程开始、数据源 |
| **终止节点** | 只有入口 | 只能连入 | 流程结束、数据输出 |
| **双向节点** | 左右 / 上下对称 | 双向连接 | 数据交互、状态切换 |

---

## 🔧 节点模型开发

### 基础模板

```typescript
import { RectNodeModel } from '@logicflow/core'

// 自定义节点模型基础模板
export class CustomNodeModel extends RectNodeModel {
  // 是否实时更新（属性变化时重新渲染）
  shouldUpdate() { 
    return true  // 需要响应式更新时返回 true
  }

  // 设置节点尺寸和样式
  setAttributes() {
    this.width = 200    // 根据内容调整
    this.height = 80
    
    // 可选：根据动态属性调整尺寸
    if (this.properties?.branches) {
      this.height = 60 + this.properties.branches.length * 20
    }
  }

  // 连出规则（作为源节点）
  getConnectedSourceRules() {
    const rules = super.getConnectedSourceRules()
    
    // 示例规则 1: 限制出口连线数量
    const limitOutgoing = {
      message: '该节点最多只能连出 X 条线',
      validate: () => {
        const { edges } = this.outgoing
        return !(edges && edges.length >= X)  // X 根据业务需求设置
      },
    }
    
    // 示例规则 2: 限制连接的目标节点类型
    const allowedTargets = {
      message: '只能连接到特定类型的节点',
      validate: (sourceNode, targetNode) => {
        const allowedTypes = ['typeA', 'typeB']
        return allowedTypes.includes(targetNode.type)
      },
    }
    
    // 示例规则 3: 防止自环
    const noSelfLoop = {
      message: '节点不能连接自己',
      validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => 
        sourceNode.id !== targetNode.id
    }
    
    // 根据业务需求选择规则
    rules.push(limitOutgoing, allowedTargets, noSelfLoop)
    return rules
  }

  // 连入规则（作为目标节点）
  getConnectedTargetRules() {
    const rules = super.getConnectedTargetRules()
    
    // 示例：限制入口连线数量
    const limitIncoming = {
      message: '该节点最多只能有 Y 条输入',
      validate: () => {
        const { edges } = this.incoming
        return !(edges && edges.length >= Y)
      },
    }
    
    rules.push(limitIncoming)
    return rules
  }

  // 定义锚点位置
  getDefaultAnchor(): { x: number; y: number; id: string }[] {
    const { id, x, y, width, height } = this
    
    // 常见模式 1: 上下锚点（垂直流）
    return [
      { x, y: y - height / 2, id: `${id}_top` },      // 上锚点
      { x, y: y + height / 2, id: `${id}_bottom` }    // 下锚点
    ]
    
    // 常见模式 2: 左右锚点（水平流）
    // return [
    //   { x: x - width / 2, y, id: `${id}_left` },
    //   { x: x + width / 2, y, id: `${id}_right` }
    // ]
    
    // 常见模式 3: 四向锚点
    // return [
    //   { x, y: y - height / 2, id: `${id}_top` },
    //   { x, y: y + height / 2, id: `${id}_bottom` },
    //   { x: x - width / 2, y, id: `${id}_left` },
    //   { x: x + width / 2, y, id: `${id}_right` }
    // ]
  }
}
```

### 动态锚点节点（分支/并行）

```typescript
export class BranchNodeModel extends RectNodeModel {
  constructor(data: any, graphModel: any) {
    // 自定义文本位置
    data.text = {
      value: typeof data.text === 'string' ? data.text : data?.text?.value,
      x: data?.x + 50,
      y: data?.y,
    }
    super(data, graphModel)
  }

  setAttributes() {
    this.width = 120
    this.height = 60
  }

  // 动态锚点生成
  getDefaultAnchor(): { x: number; y: number; id: string }[] {
    const { id, x, y, width, height } = this
    const { branches = [] } = this.properties as { branches?: { anchorId: string; index: number }[] }

    const anchors: { x: number; y: number; id: string }[] = [
      { x, y: y - height / 2, id: `${id}_top` }  // 固定上锚点
    ]

    // 动态生成多个下锚点
    const branchCount = branches.length || 2  // 默认 2 个分支
    const spacing = width / (branchCount + 1)
    
    branches.forEach((branch, index) => {
      anchors.push({
        x: x - width / 2 + spacing * (index + 1),
        y: y + height / 2,
        id: `${id}${branch.anchorId}`
      })
    })

    return anchors
  }

  // 单个锚点只能连出一条线
  getConnectedSourceRules() {
    const rules = super.getConnectedSourceRules()
    
    const onlyUniqueSource = {
      message: '分支锚点只能与一个节点相连',
      validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
        const { edges } = this.outgoing
        const isHaveBranchEdge = edges && edges.some(edge => 
          edge.sourceAnchorId === sourceAnchor.id
        )
        return !isHaveBranchEdge
      },
    }
    
    rules.push(onlyUniqueSource)
    return rules
  }
}
```

---

## 🎨 节点组件开发

### 通用节点组件模板

```vue
<template>
  <section class="viewport">
    <!-- 普通节点：显示类型和名称 -->
    <div v-if="!isSpecialType" 
         :class="['custom-node', `custom-node-${data.type}`, { 'error-node': data.properties?.isError }]">
      <div class="node-header">
        <span class="node-type">{{ getNodeTypeLabel(data.type) }}</span>
      </div>
      <div class="node-content">
        <span class="node-name">{{ data.properties.name || 'Unnamed' }}</span>
        <span v-if="data.properties.description" class="node-desc">
          {{ data.properties.description }}
        </span>
      </div>
      <!-- 错误状态指示 -->
      <div v-if="data.properties.hasError" class="error-indicator">⚠️</div>
    </div>

    <!-- 特殊节点：如分支、合并等 -->
    <div v-else :class="`${data.type} custom-node-${data.type}`">
      <div :class="`center-line center-line-${data.type}`"></div>
      <div v-if="needsParallelIndicator(data.type)" class="parallel-indicator"></div>
    </div>
  </section>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { EventType } from '@logicflow/core'

interface Props {
  node: any
  graph: any
}

const props = defineProps<Props>()
const data = ref({ ...props.graph.getNodeModelById(props.node.id) })

// 定义特殊节点类型（需要特殊渲染的）
const specialTypes = ['branch', 'branchEnd', 'parallel', 'parallelEnd', 'gateway']
const isSpecialType = computed(() => specialTypes.includes(data.value.type))

// 获取节点类型的显示文本（可从配置或i18n获取）
const getNodeTypeLabel = (type: string) => {
  const labelMap = {
    start: '开始',
    end: '结束',
    task: '任务',
    decision: '决策'
    // 根据项目需求扩展...
  }
  return labelMap[type] || type
}

// 判断是否需要并行指示器
const needsParallelIndicator = (type: string) => {
  return type.includes('parallel')
}

// 监听节点属性变化
onMounted(() => {
  const eventHandler = (eventData: any) => {
    if (eventData.id === props.node.id) {
      data.value.properties = eventData?.properties
    }
  }
  
  props.graph.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, eventHandler)
  
  return () => {
    props.graph.eventCenter.off(EventType.NODE_PROPERTIES_CHANGE, eventHandler)
  }
})
</script>

<style scoped>
.custom-node {
  display: flex;
  flex-direction: column;
  background: #fff;
  border: 2px solid #ddd;
  border-radius: 4px;
  padding: 8px;
  transition: border-color 0.3s;
}

.node-header {
  font-size: 12px;
  color: #666;
  margin-bottom: 4px;
}

.node-content {
  font-size: 14px;
  color: #333;
}

/* 错误状态：红色边框 + 脉冲动画 */
.error-node {
  border-color: #f56c6c !important;
  animation: error-pulse 1s ease-in-out infinite;
}

@keyframes error-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.4); }
  50% { box-shadow: 0 0 0 6px rgba(245, 108, 108, 0); }
}

.error-indicator {
  position: absolute;
  top: -8px;
  right: -8px;
  color: #f56c6c;
}
</style>
```

---

## 🔧 流程图主组件集成

### 初始化流程

```typescript
import LogicFlow from '@logicflow/core'
import { Control, Menu, DndPanel, SelectionSelect } from '@logicflow/extension'
import { Dagre } from '@logicflow/layout'
import { register, getTeleport } from '@logicflow/vue-node-registry'

// 注册插件
LogicFlow.use(Control)         // 控制面板（放大/缩小/还原）
LogicFlow.use(Menu)            // 右键菜单
LogicFlow.use(DndPanel)        // 拖拽面板
LogicFlow.use(SelectionSelect) // 框选
LogicFlow.use(Dagre)           // 自动布局

let lf: any = null

const renderLf = async () => {
  lf = new LogicFlow({
    container: document.querySelector('.vita-flow') as HTMLElement,
    grid: true,                  // 显示网格
    plugins: [Dagre],
    keyboard: { enabled: true }, // 启用键盘快捷键
    isSilentMode: props.read,    // 只读模式
  })

  // 注册自定义节点
  initFlow(props.render, lf, $t, props.read, menuCallback.value, menuItem.value, addItem.value)
}
```

### 事件监听

```typescript
// 定义事件处理器配置
const setupEventListeners = (lf: any, options: {
  onNodeClick?: (node: any) => void
  onNodeDoubleClick?: (node: any) => void
  onEdgeChange?: () => void
  onConnectionNotAllowed?: (data: any) => void
  shouldAutoValidate?: boolean
}) => {
  let clickTimer: NodeJS.Timeout | null = null
  
  // 连线规则校验失败
  lf.on('connection:not-allowed', (data: any) => {
    if (options.onConnectionNotAllowed) {
      options.onConnectionNotAllowed(data)
    } else {
      console.warn('连接不被允许:', data?.msg)
    }
  })
  
  // 单击节点
  lf.on('node:click', (e: any) => {
    if (clickTimer) clearTimeout(clickTimer)
    clickTimer = setTimeout(() => {
      clickTimer = null
      console.log('单击节点', e?.data)
      options.onNodeClick?.(e.data)
    }, 200)
  })
  
  // 双击节点（打开配置对话框）
  lf.on('node:dbclick', (e: any) => {
    if (clickTimer) {
      clearTimeout(clickTimer)
      clickTimer = null
    }
    
    console.log('双击节点', e?.data)
    options.onNodeDoubleClick?.(e.data)
  })
  
  // 连线变化 - 可选的自动校验
  if (options.shouldAutoValidate) {
    lf.on('edge:add', () => {
      setTimeout(() => options.onEdgeChange?.(), 100)
    })
    
    lf.on('edge:delete', () => {
      setTimeout(() => options.onEdgeChange?.(), 100)
    })
  }
}

// 使用示例
setupEventListeners(lf, {
  onNodeClick: (node) => {
    // 处理节点点击
    selectNode(node.id)
  },
  onNodeDoubleClick: (node) => {
    // 打开编辑器
    openNodeEditor(node)
  },
  onEdgeChange: () => {
    // 重新校验流程图
    validateGraph()
  },
  onConnectionNotAllowed: (data) => {
    showMessage({ type: 'warning', message: data?.msg })
  },
  shouldAutoValidate: true
})
```

### 节点拖拽监听

```typescript
lf.on('node:dnd-add', (data: any) => {
  const rawData = lf.getGraphRawData()
  const node = lf.graphModel.getNodeModelById(data?.data?.id)
  
  // 示例 1: 限制特定节点的数量
  if (needUnique(data.data.type)) {
    const existingNodes = rawData.nodes.filter(n => n.type === data.data.type)
    if (existingNodes.length > 1) {
      lf.deleteNode(data.data.id)
      showMessage({ type: 'warning', message: `只能有一个${data.data.type}节点` })
      return
    }
  }
  
  // 示例 2: 自动命名新节点
  if (needAutoName(data.data.type)) {
    const sameTypeNodes = rawData.nodes.filter(n => n.type === data.data.type)
    const index = sameTypeNodes.length
    node.setProperty('name', `${data.data.type}_${String(index).padStart(3, '0')}`)
  }
  
  // 示例 3: 初始化节点属性
  if (needInitProperties(data.data.type)) {
    node.setProperty('status', 'pending')
    node.setProperty('createdAt', Date.now())
  }
})
```

---

## ✅ 流程图校验系统

> **核心特性**：校验失败时，不符合规则的节点和连线会自动变红高亮，直观显示错误位置

### 校验触发时机

1. 点击"校验"按钮 → 执行校验 → 高亮错误
2. 点击"提交"按钮 → 先校验 → 失败则高亮错误并阻止提交
3. 添加/删除连线时 → 自动重新校验 → 更新高亮状态

### 前端结构校验

```typescript
export interface ValidationResult {
  valid: boolean
  errors: ValidationError[]
  errorEdges: string[]   // 错误的边 ID
  errorNodes: string[]   // 错误的节点 ID
}

export interface ValidationError {
  type: 'isolated_node' | 'isolated_path' | 'circular_path' | 'unconnected_anchor' | 'no_start' | 'no_end' | 'unreachable'
  message: string
  nodeIds?: string[]
  edgeIds?: string[]
}

export class FlowValidator {
  private nodes: any[]
  private edges: any[]
  private adjacencyList: Map<string, string[]>          // 邻接表
  private reverseAdjacencyList: Map<string, string[]>   // 反向邻接表
  private startNode: any | null = null
  private endNode: any | null = null

  constructor(graphData: GraphData) {
    this.nodes = graphData.nodes || []
    this.edges = graphData.edges || []
    this.buildAdjacencyLists()
    this.findStartEndNodes()
  }

  public validate(): ValidationResult {
    const errors: ValidationError[] = []
    const errorEdges: string[] = []
    const errorNodes: string[] = []

    // 1. 检查是否存在 start 和 end 节点
    if (!this.startNode) {
      errors.push({ type: 'no_start', message: '缺少开始节点' })
    }
    if (!this.endNode) {
      errors.push({ type: 'no_end', message: '缺少结束节点' })
    }

    if (!this.startNode || !this.endNode) {
      return { valid: false, errors, errorEdges, errorNodes }
    }

    // 2. 检查从 start 可达的节点（BFS）
    const reachableFromStart = this.getReachableNodes(this.startNode.id, this.adjacencyList)

    // 3. 检查可以到达 end 的节点（反向 BFS）
    const canReachEnd = this.getReachableNodes(this.endNode.id, this.reverseAdjacencyList)

    // 4. 计算主路径节点（既能从 start 到达，又能到达 end）
    const mainPathNodes = new Set(
      [...reachableFromStart].filter(nodeId => canReachEnd.has(nodeId))
    )

    // 5. 检查孤立节点（不在主路径上）
    const isolatedNodes = this.nodes.filter(node => {
      if (node.type === 'start' || node.type === 'end') return false
      return !mainPathNodes.has(node.id)
    })

    if (isolatedNodes.length > 0) {
      const isolatedNodeIds = isolatedNodes.map(n => n.id)
      errors.push({
        type: 'isolated_node',
        message: `发现 ${isolatedNodes.length} 个孤立节点（不在 start 到 end 的有效路径上）`,
        nodeIds: isolatedNodeIds,
      })
      errorNodes.push(...isolatedNodeIds)

      // 找到连接到孤立节点的边
      const isolatedEdges = this.edges.filter(edge => 
        isolatedNodeIds.includes(edge.sourceNodeId) || 
        isolatedNodeIds.includes(edge.targetNodeId)
      )
      errorEdges.push(...isolatedEdges.map(e => e.id))
    }

    // 6. 检查回路（DFS）
    const cycles = this.detectCycles(mainPathNodes)
    if (cycles.length > 0) {
      cycles.forEach(cycle => {
        errors.push({
          type: 'circular_path',
          message: `检测到回路：${cycle.join(' → ')}`,
          nodeIds: cycle,
        })
        errorNodes.push(...cycle)
      })
    }

    // 7. 检查 end 节点是否可达
    if (!reachableFromStart.has(this.endNode.id)) {
      errors.push({
        type: 'unreachable',
        message: '无法从开始节点到达结束节点',
        nodeIds: [this.endNode.id],
      })
      errorNodes.push(this.endNode.id)
    }

    return {
      valid: errors.length === 0,
      errors,
      errorEdges: [...new Set(errorEdges)],
      errorNodes: [...new Set(errorNodes)],
    }
  }

  // BFS 获取可达节点
  private getReachableNodes(startNodeId: string, adjacency: Map<string, string[]>): Set<string> {
    const visited = new Set<string>()
    const queue: string[] = [startNodeId]
    visited.add(startNodeId)

    while (queue.length > 0) {
      const current = queue.shift()!
      const neighbors = adjacency.get(current) || []

      neighbors.forEach(neighbor => {
        if (!visited.has(neighbor)) {
          visited.add(neighbor)
          queue.push(neighbor)
        }
      })
    }

    return visited
  }

  // DFS 检测回路
  private detectCycles(validNodes: Set<string>): string[][] {
    const visited = new Set<string>()
    const recursionStack = new Set<string>()
    const cycles: string[][] = []

    const dfs = (nodeId: string): boolean => {
      visited.add(nodeId)
      recursionStack.add(nodeId)

      const neighbors = (this.adjacencyList.get(nodeId) || [])
        .filter(n => validNodes.has(n))

      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          if (dfs(neighbor)) return true
        } else if (recursionStack.has(neighbor)) {
          // 发现回路
          const cycle: string[] = []
          let current = nodeId
          cycle.push(neighbor)
          while (current !== neighbor) {
            cycle.push(current)
            current = this.getParent(current, neighbor)
          }
          cycles.push(cycle.reverse())
          return true
        }
      }

      recursionStack.delete(nodeId)
      return false
    }

    validNodes.forEach(nodeId => {
      if (!visited.has(nodeId)) {
        dfs(nodeId)
      }
    })

    return cycles
  }
}
```

### 调用校验与错误高亮

```typescript
const validateGraph = async () => {
  try {
    if (!lf) return false

    const rawData = lf.getGraphRawData()
    const { nodes, edges } = rawData

    // 1. 前端结构校验
    const frontendValidation = validateFlow(rawData)
    
    // 核心：高亮错误的边和节点
    highlightErrors(frontendValidation)

    if (!frontendValidation.valid) {
      const errorMessages = frontendValidation.errors.map(err => err.message).join('\n')
      showMessage({
        type: 'error',
        message: `流程图校验失败：\n${errorMessages}`,
        duration: 5000
      })
      return false
    }

    // 2. (可选) 调用后端业务校验
    const backendResult = await callBackendValidation(nodes, edges)
    if (!backendResult.success) {
      showMessage({
        type: 'error',
        message: backendResult.message || '后端校验失败'
      })
      return false
    }

    // 3. 校验通过，清除高亮
    clearErrorHighlight()
    showMessage({ type: 'success', message: '校验通过' })
    return true
  } catch (err) {
    console.error('Validation error:', err)
    showMessage({ type: 'error', message: '校验失败' })
    return false
  }
}

// 核心功能：高亮错误节点和连线
const highlightErrors = (validation: ValidationResult) => {
  if (!lf) return

  // 清除之前的高亮
  clearErrorHighlight()

  // 高亮错误的边（变红色，加粗）
  validation.errorEdges.forEach(edgeId => {
    const edgeModel = lf.graphModel.getEdgeModelById(edgeId)
    if (edgeModel) {
      edgeModel.setProperties({ ...edgeModel.properties, isError: true })
      edgeModel.setAttributes({ 
        stroke: '#f56c6c',    // 红色
        strokeWidth: 2         // 加粗至 2px
      })
    }
  })

  // 高亮错误的节点（红色边框）
  validation.errorNodes.forEach(nodeId => {
    const nodeModel = lf.graphModel.getNodeModelById(nodeId)
    if (nodeModel) {
      nodeModel.setProperties({ ...nodeModel.properties, isError: true })
      // 注意：节点红色边框通过 CSS 样式实现
    }
  })
}

// 清除错误高亮
const clearErrorHighlight = () => {
  if (!lf) return
  
  const rawData = lf.getGraphRawData()
  
  // 清除所有边的错误状态
  rawData.edges.forEach((edge: any) => {
    const edgeModel = lf.graphModel.getEdgeModelById(edge.id)
    if (edgeModel && edgeModel.properties?.isError) {
      edgeModel.setProperties({ ...edgeModel.properties, isError: false })
      edgeModel.setAttributes({ 
        stroke: '#000000',   // 恢复默认颜色
        strokeWidth: 1        // 恢复默认宽度
      })
    }
  })
  
  // 清除所有节点的错误状态
  rawData.nodes.forEach((node: any) => {
    const nodeModel = lf.graphModel.getNodeModelById(node.id)
    if (nodeModel && nodeModel.properties?.isError) {
      nodeModel.setProperties({ ...nodeModel.properties, isError: false })
    }
  })
}
```

---

## 🎨 右键菜单与工具栏

### 右键菜单配置

```typescript
const setMenu = (lf: any, $t: any, menuCallback: any) => {
  const hidePublic = ['start', 'end']
  const publicMenu = [
    { text: $t('删除'), callback: (node: any) => lf.deleteNode(node.id) },
    { text: $t('复制'), callback: (node: any) => lf.graphModel.cloneNode(node.id) }
  ]

  const menus = [
    { 
      type: 'start', 
      menu: [
        { text: $t('编辑文本'), callback: (node: any) => lf.graphModel.setElementStateById(node.id, 2) }
      ]
    },
    { 
      type: 'branch', 
      menu: [
        { text: $t('添加分支锚点'), callback: (node: any) => menuCallback.addBranch(node) },
        { text: $t('减少分支锚点'), callback: (node: any) => menuCallback.removeBranch(node) }
      ] 
    },
  ]

  menus.forEach(item => {
    if (!hidePublic.includes(item.type)) {
      publicMenu.forEach(i => item.menu.push(i))
    }
    lf.setMenuByType(item)
  })
}
```

### 动态锚点管理

```typescript
// logicFunc.ts
export default {
  // 添加分支锚点
  addBranch: (node: any, lf: any) => {
    const newBranch = {
      anchorId: `_bottom_${(node.properties.branches || []).length + 1}`,
      index: (node.properties.branches || []).length,
    }
    const newBranches = (node.properties.branches || []).concat(newBranch)
    const nodeModel = lf.graphModel.getNodeModelById(node.id)
    const edges = lf.graphModel.getNodeEdges(node.id)
    
    nodeModel.setProperty('branches', newBranches)
    
    // 更新连线起点位置
    setTimeout(() => {
      edges.forEach(edge => {
        nodeModel?.anchors?.forEach(fil => {
          if (fil.id === edge.sourceAnchorId) {
            edge.updateStartPoint({ x: fil?.x, y: fil?.y })
          }
        })
      })
    }, 10)
  },

  // 移除分支锚点
  removeBranch: (node: any, lf: any, $t: any) => {
    const newBranches = (node.properties.branches || [])
    
    if (newBranches.length && newBranches.length > 2) {
      const popEdge = newBranches.pop()
      const nodeModel = lf.graphModel.getNodeModelById(node.id)
      
      nodeModel.setProperty('branches', newBranches)
      
      const edges = lf.graphModel.getNodeEdges(node.id)
      setTimeout(() => {
        edges.forEach(edge => {
          nodeModel?.anchors?.forEach(fil => {
            if (fil.id === edge.sourceAnchorId) {
              edge.updateStartPoint({ x: fil?.x, y: fil?.y })
            }
            if (`${node.id}${popEdge.anchorId}` === edge.sourceAnchorId) {
              lf.graphModel.deleteEdgeById(edge.id)
            }
          })
        })
      }, 10)
    } else {
      ElMessage.warning($t('没有可以删除的分支锚点'))
    }
  },
}
```

---

## 📐 自动布局

```typescript
const autoSort = (type = 'LR') => {
  if (!lf) return
  
  const newRender = { ...lf.getGraphRawData() }
  
  // 先对边排序
  lf.render({ 
    nodes: newRender?.nodes, 
    edges: logicFunc.sortPolylinesByAnchorSuffix(newRender?.edges) 
  })

  // 使用 Dagre 自动布局
  setTimeout(() => {
    lf.extension.dagre.layout({
      rankdir: type,          // 'TB' 垂直，'LR' 水平
      ranker: 'longest-path', // 布局算法
      align: undefined,       // 对齐方式
      nodesep: 60,            // 节点间距
      ranksep: 70,            // 层级间距
      isDefaultAnchor: false, // 不使用默认锚点
    })
  }, 100)
}
```

---

## 💾 数据提交

```typescript
const getData = async () => {
  if (!lf) return

  // 先执行校验
  const isValid = await checkData()
  if (!isValid) {
    ElMessage.warning($t('请先修复流程图中的错误'))
    return
  }

  // 获取画布数据
  const rawData = lf.getGraphRawData()
  emit('flowSubmit', { rawData: rawData })
}
```

---

## ⚠️ 常见问题与解决方案

### 1. 节点属性不更新

```typescript
// ❌ 错误：直接修改 properties
node.properties.name = 'new name'

// ✅ 正确：使用 setProperty
const nodeModel = lf.graphModel.getNodeModelById(nodeId)
nodeModel.setProperty('name', 'new name')
```

### 2. 锚点位置错乱

```typescript
// ✅ 添加/删除锚点后，必须更新连线起点
setTimeout(() => {
  edges.forEach(edge => {
    nodeModel?.anchors?.forEach(anchor => {
      if (anchor.id === edge.sourceAnchorId) {
        edge.updateStartPoint({ x: anchor.x, y: anchor.y })
      }
    })
  })
}, 10)
```

### 3. 只读模式下右键菜单仍显示

```typescript
// ✅ 初始化时设置只读模式
lf = new LogicFlow({
  container: document.querySelector('.flow-container') as HTMLElement,
  grid: true,
  isSilentMode: isReadOnly,  // 只读模式
})

// ✅ 只读模式下不设置菜单和拖拽面板
if (!isReadOnly) {
  setupContextMenu(lf, nodeTypes, callbacks)
  lf.extension.dndPanel.setPatternItems(patternItems)
}
```

---

## 📚 参考资源

- **LogicFlow 官方文档**: http://logic-flow.cn/
- **LogicFlow GitHub**: https://github.com/didi/LogicFlow
- **LogicFlow API 参考**: http://logic-flow.cn/api/
- **Vue Node Registry**: https://github.com/Logic-Flow/logicflow-node-registry-vue3
- **Dagre 布局算法**: https://github.com/dagrejs/dagre

---

**核心原则**：
1. 📐 **架构清晰**: Model-View-Component 分离
2. 🔗 **规则完备**: 连接规则明确，校验逻辑严密
3. 🎨 **视觉反馈**: 错误状态红色高亮，成功状态清晰提示
4. 🔧 **可扩展性**: 易于添加新节点类型和校验规则
5. 📝 **类型安全**: 完整的 TypeScript 类型定义

---

**维护者**: MTA工作室  
**创建日期**: 2025-12-16  
**最后更新**: 2025-12-16
