Bläddra i källkod

fix: 条件节点数据模型修复

- 条件表达式从节点属性迁移到连线属性
- 设计器右侧面板改为展示/编辑出连线列表
- 支持为每条分支配置:目标节点、默认分支、分支名称、条件表达式
- 连线上显示分支名称/条件,画布更直观
- 保存校验:条件节点必须有且只有一个默认分支
- convertToBackendFormat / convertToFrontendFormat 增加 edge.condition 与 edge.properties 互转
- 加载旧流程时自动将节点级 condition 迁移到连线
ye-zhaojia 1 vecka sedan
förälder
incheckning
cfd3910304
1 ändrade filer med 202 tillägg och 31 borttagningar
  1. 202 31
      src/views/flow/designer/index.vue

+ 202 - 31
src/views/flow/designer/index.vue

@@ -88,26 +88,30 @@
           </el-form-item>
         </template>
         <template v-if="selectedNode.type === 'condition-node'">
-          <el-form-item>
-            <template #label>
-              条件表达式
-              <el-tooltip placement="top" effect="dark" raw-content>
-                <template #content>
-                  <div style="line-height: 1.8; max-width: 320px;">
-                    <div style="margin-bottom: 6px; font-weight: bold;">格式:字段名 运算符 值</div>
-                    <div>运算符:<code>==</code>&nbsp;<code>!=</code>&nbsp;<code>&gt;</code>&nbsp;<code>&lt;</code>&nbsp;<code>&gt;=</code>&nbsp;<code>&lt;=</code></div>
-                    <div style="margin-top: 4px;">示例:</div>
-                    <div><code>quantity &gt;= 10</code>&nbsp;&nbsp;数量 ≥ 10</div>
-                    <div><code>tradeTerms == "FOB"</code>&nbsp;&nbsp;贸易条款为FOB</div>
-                    <div><code>destCountry == "USA"</code>&nbsp;&nbsp;目的国为USA</div>
-                    <div style="margin-top: 4px; color: #e6a23c;">注意:条件不满足时走默认分支(无条件的连线)</div>
-                  </div>
-                </template>
-                <el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
-              </el-tooltip>
-            </template>
-            <el-input v-model="nodeProps.condition" type="textarea" placeholder="如: quantity >= 10" />
-          </el-form-item>
+          <div class="panel-title">分支配置</div>
+          <el-table :data="outgoingEdges" size="small" border class="branch-table">
+            <el-table-column label="目标节点" min-width="100">
+              <template #default="{ row }">
+                {{ getEdgeTargetName(row) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="默认分支" width="80" align="center">
+              <template #default="{ row }">
+                <el-switch v-model="row.properties.isDefault" @change="onDefaultChange(row)" />
+              </template>
+            </el-table-column>
+            <el-table-column label="分支名称" min-width="100">
+              <template #default="{ row }">
+                <el-input v-model="row.properties.branchName" size="small" placeholder="如:大额" @blur="onBranchBlur(row)" />
+              </template>
+            </el-table-column>
+            <el-table-column label="条件表达式" min-width="160">
+              <template #default="{ row }">
+                <el-input v-model="row.properties.condition" size="small" placeholder="如:amount >= 1000" :disabled="row.properties.isDefault" @blur="onBranchBlur(row)" />
+              </template>
+            </el-table-column>
+          </el-table>
+          <div class="form-tip">默认分支无需填写条件;有且只能有一个默认分支。</div>
         </template>
         <template v-if="selectedNode.type === 'cc-node'">
           <el-form-item label="抄送人">
@@ -285,7 +289,6 @@ const nodeProps = reactive<Record<string, any>>({
   approveMode: 'or',
   timeoutHours: undefined as number | undefined,
   timeoutAction: 'remind',
-  condition: '',
   ccUsers: ''
 })
 
@@ -298,6 +301,79 @@ const nodeName = computed({
   }
 })
 
+// 条件节点出连线配置
+const outgoingEdges = ref<any[]>([])
+
+function loadOutgoingEdges(nodeId?: string) {
+  if (!lf) {
+    outgoingEdges.value = []
+    return
+  }
+  const id = nodeId || selectedNode.value?.id
+  if (!id) {
+    outgoingEdges.value = []
+    return
+  }
+  const graphData = lf.getGraphData()
+  const edges = (graphData.edges || []).filter((e: any) => e.sourceNodeId === id)
+  outgoingEdges.value = edges.map((e: any) => ({
+    ...e,
+    properties: {
+      condition: '',
+      isDefault: false,
+      branchName: '',
+      ...(e.properties || {})
+    }
+  }))
+}
+
+function getEdgeTargetName(edge: any): string {
+  if (!lf || !edge?.targetNodeId) return '—'
+  try {
+    const node = lf.getNodeData(edge.targetNodeId)
+    return node?.text?.value || node?.text || '未命名节点'
+  } catch {
+    return '—'
+  }
+}
+
+function getEdgeDisplayText(edge: any): string {
+  const props = edge.properties || {}
+  if (props.branchName) return props.branchName
+  if (props.isDefault) return '默认'
+  if (props.condition) return props.condition
+  return ''
+}
+
+function syncEdgeToLf(edge: any) {
+  if (!lf || !edge?.id) return
+  const props = edge.properties || {}
+  const displayText = getEdgeDisplayText(edge)
+  lf.setEdgeData(edge.id, {
+    properties: { ...props },
+    text: displayText ? { value: displayText } : undefined
+  })
+}
+
+function onDefaultChange(edge: any) {
+  if (edge.properties.isDefault) {
+    edge.properties.condition = ''
+    // 同一条件节点下只能有一个默认分支
+    const nodeId = edge.sourceNodeId
+    outgoingEdges.value.forEach((e: any) => {
+      if (e.id !== edge.id && e.sourceNodeId === nodeId) {
+        e.properties.isDefault = false
+        syncEdgeToLf(e)
+      }
+    })
+  }
+  syncEdgeToLf(edge)
+}
+
+function onBranchBlur(edge: any) {
+  syncEdgeToLf(edge)
+}
+
 const nodeTypes = [
   { type: 'start-node', label: '开始', icon: VideoPlay, color: '#67C23A' },
   { type: 'approval-node', label: '审批节点', icon: User, color: '#409EFF' },
@@ -308,7 +384,6 @@ const nodeTypes = [
 
 function getDefaultProps(type: string) {
   if (type === 'approval-node') return { assigneeType: 'ROLE', assigneeValue: '', approveMode: 'or', timeoutHours: undefined, timeoutAction: 'remind' }
-  if (type === 'condition-node') return { condition: '' }
   if (type === 'cc-node') return { ccUsers: '' }
   return {}
 }
@@ -325,8 +400,6 @@ function saveCurrentNodeProps() {
       props.approveMode = nodeProps.approveMode ?? 'or'
       props.timeoutHours = nodeProps.timeoutHours
       props.timeoutAction = nodeProps.timeoutAction ?? 'remind'
-    } else if (type === 'condition-node') {
-      props.condition = nodeProps.condition ?? ''
     } else if (type === 'cc-node') {
       props.ccUsers = nodeProps.ccUsers ?? ''
     }
@@ -356,8 +429,6 @@ function loadNodeProps(data: any) {
     nodeProps.approveMode = approveMode
     nodeProps.timeoutHours = cloned.timeoutHours ?? undefined
     nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind'
-  } else if (data.type === 'condition-node') {
-    nodeProps.condition = cloned.condition ?? ''
   } else if (data.type === 'cc-node') {
     nodeProps.ccUsers = cloned.ccUsers ?? ''
   }
@@ -476,11 +547,25 @@ function initLogicFlow() {
     saveCurrentNodeProps()
     selectedNode.value = data
     loadNodeProps(data)
+    loadOutgoingEdges(data.id)
   })
 
   lf.on('blank:click', () => {
     saveCurrentNodeProps()
     selectedNode.value = null
+    outgoingEdges.value = []
+  })
+
+  // 当条件节点的出连线发生变化时,刷新右侧分支配置面板
+  lf.on('edge:add', ({ data }: any) => {
+    if (selectedNode.value?.id && data?.sourceNodeId === selectedNode.value.id) {
+      loadOutgoingEdges(selectedNode.value.id)
+    }
+  })
+  lf.on('edge:delete', ({ data }: any) => {
+    if (selectedNode.value?.id && data?.sourceNodeId === selectedNode.value.id) {
+      loadOutgoingEdges(selectedNode.value.id)
+    }
   })
 
   // 编辑模式:加载已有流程图
@@ -553,13 +638,79 @@ function convertToBackendFormat(graphData: any) {
       return n
     })
   }
-  // edges 保持原样,properties 中的 condition 会在后端解析
+  // 将 LogicFlow edge.properties 转换为后端的 edge.condition 结构
+  if (data.edges) {
+    data.edges = data.edges.map((edge: any) => {
+      const e = { ...edge }
+      const props = e.properties || {}
+      e.condition = {
+        condition: props.condition || '',
+        isDefault: props.isDefault ?? !props.condition,
+        branchName: props.branchName || ''
+      }
+      // 清理 LogicFlow 运行时不需要的属性,避免后端序列化冗余
+      if (e.text !== undefined && (!e.text || !e.text.value)) {
+        delete e.text
+      }
+      delete e.properties
+      return e
+    })
+  }
   return data
 }
 
 // 将后端的 graphData 转换为 LogicFlow 格式
 function convertToFrontendFormat(graphData: any) {
   const data = JSON.parse(JSON.stringify(graphData))
+
+  // 兼容旧格式:条件表达式写在节点 properties.condition 上,需要迁移到连线
+  if (data.nodes && data.edges) {
+    data.nodes.forEach((node: any) => {
+      if ((node.type === 'condition' || node.type === 'condition-node') && node.properties?.condition) {
+        const nodeCondition = String(node.properties.condition)
+        const outEdges = data.edges.filter((e: any) => e.sourceNodeId === node.id)
+        if (outEdges.length > 0) {
+          // 第一条连线作为条件分支,其余作为默认分支
+          outEdges.forEach((e: any, idx: number) => {
+            if (!e.condition) {
+              e.condition = {}
+            }
+            if (idx === 0) {
+              e.condition.condition = nodeCondition
+              e.condition.isDefault = false
+            } else {
+              e.condition.condition = ''
+              e.condition.isDefault = true
+            }
+          })
+        }
+        delete node.properties.condition
+      }
+    })
+  }
+
+  if (data.edges) {
+    data.edges = data.edges.map((edge: any) => {
+      const e = { ...edge }
+      // 将后端的 edge.condition 转换为 LogicFlow 的 edge.properties
+      const conditionMap = e.condition || {}
+      e.properties = {
+        condition: conditionMap.condition || '',
+        isDefault: conditionMap.isDefault ?? !conditionMap.condition,
+        branchName: conditionMap.branchName || ''
+      }
+      // 设置/清理连线文本
+      const displayText = e.properties.branchName || (!e.properties.isDefault && e.properties.condition) || (e.properties.isDefault ? '默认' : '')
+      if (displayText) {
+        e.text = { value: displayText }
+      } else {
+        delete e.text
+      }
+      delete e.condition
+      return e
+    })
+  }
+
   if (data.nodes) {
     data.nodes = data.nodes.map((node: any) => {
       const n = { ...node }
@@ -714,7 +865,7 @@ function validateFlow(graphData: any): string | null {
       return `节点 "${node.text || node.name}" 是孤立节点,请删除或连接`
     }
   }
-  // 检查条件分支是否有默认分支
+  // 检查条件分支:有且只能有一个默认分支,非默认分支必须填写条件
   const sourceMap: Record<string, any[]> = {}
   for (const edge of edges) {
     if (!sourceMap[edge.sourceNodeId]) sourceMap[edge.sourceNodeId] = []
@@ -724,9 +875,22 @@ function validateFlow(graphData: any): string | null {
     if (node.type === 'condition-node' || node.type === 'condition') {
       const outEdges = sourceMap[node.id] || []
       if (outEdges.length > 1) {
-        const hasDefault = outEdges.some((e: any) => !e.properties?.condition && !e.condition)
-        if (!hasDefault) {
-          return `条件节点 "${node.text || node.name}" 需要至少一条无条件的默认分支`
+        const defaultEdges = outEdges.filter((e: any) => {
+          const props = e.properties || {}
+          return props.isDefault || (!props.condition && !e.condition)
+        })
+        if (defaultEdges.length === 0) {
+          return `条件节点 "${node.text || node.name}" 需要至少一条默认分支`
+        }
+        if (defaultEdges.length > 1) {
+          return `条件节点 "${node.text || node.name}" 只能有一条默认分支`
+        }
+        const invalid = outEdges.some((e: any) => {
+          const props = e.properties || {}
+          return !props.isDefault && !props.condition && !e.condition
+        })
+        if (invalid) {
+          return `条件节点 "${node.text || node.name}" 的非默认分支必须填写条件表达式`
         }
       }
     }
@@ -975,4 +1139,11 @@ onUnmounted(() => {
   font-weight: bold;
   color: #303133;
 }
+.branch-table {
+  margin-top: 8px;
+}
+.branch-table :deep(.el-input__wrapper) {
+  padding-left: 4px;
+  padding-right: 4px;
+}
 </style>