|
@@ -88,26 +88,30 @@
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</template>
|
|
</template>
|
|
|
<template v-if="selectedNode.type === 'condition-node'">
|
|
<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> <code>!=</code> <code>></code> <code><</code> <code>>=</code> <code><=</code></div>
|
|
|
|
|
- <div style="margin-top: 4px;">示例:</div>
|
|
|
|
|
- <div><code>quantity >= 10</code> 数量 ≥ 10</div>
|
|
|
|
|
- <div><code>tradeTerms == "FOB"</code> 贸易条款为FOB</div>
|
|
|
|
|
- <div><code>destCountry == "USA"</code> 目的国为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>
|
|
|
<template v-if="selectedNode.type === 'cc-node'">
|
|
<template v-if="selectedNode.type === 'cc-node'">
|
|
|
<el-form-item label="抄送人">
|
|
<el-form-item label="抄送人">
|
|
@@ -285,7 +289,6 @@ const nodeProps = reactive<Record<string, any>>({
|
|
|
approveMode: 'or',
|
|
approveMode: 'or',
|
|
|
timeoutHours: undefined as number | undefined,
|
|
timeoutHours: undefined as number | undefined,
|
|
|
timeoutAction: 'remind',
|
|
timeoutAction: 'remind',
|
|
|
- condition: '',
|
|
|
|
|
ccUsers: ''
|
|
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 = [
|
|
const nodeTypes = [
|
|
|
{ type: 'start-node', label: '开始', icon: VideoPlay, color: '#67C23A' },
|
|
{ type: 'start-node', label: '开始', icon: VideoPlay, color: '#67C23A' },
|
|
|
{ type: 'approval-node', label: '审批节点', icon: User, color: '#409EFF' },
|
|
{ type: 'approval-node', label: '审批节点', icon: User, color: '#409EFF' },
|
|
@@ -308,7 +384,6 @@ const nodeTypes = [
|
|
|
|
|
|
|
|
function getDefaultProps(type: string) {
|
|
function getDefaultProps(type: string) {
|
|
|
if (type === 'approval-node') return { assigneeType: 'ROLE', assigneeValue: '', approveMode: 'or', timeoutHours: undefined, timeoutAction: 'remind' }
|
|
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: '' }
|
|
if (type === 'cc-node') return { ccUsers: '' }
|
|
|
return {}
|
|
return {}
|
|
|
}
|
|
}
|
|
@@ -325,8 +400,6 @@ function saveCurrentNodeProps() {
|
|
|
props.approveMode = nodeProps.approveMode ?? 'or'
|
|
props.approveMode = nodeProps.approveMode ?? 'or'
|
|
|
props.timeoutHours = nodeProps.timeoutHours
|
|
props.timeoutHours = nodeProps.timeoutHours
|
|
|
props.timeoutAction = nodeProps.timeoutAction ?? 'remind'
|
|
props.timeoutAction = nodeProps.timeoutAction ?? 'remind'
|
|
|
- } else if (type === 'condition-node') {
|
|
|
|
|
- props.condition = nodeProps.condition ?? ''
|
|
|
|
|
} else if (type === 'cc-node') {
|
|
} else if (type === 'cc-node') {
|
|
|
props.ccUsers = nodeProps.ccUsers ?? ''
|
|
props.ccUsers = nodeProps.ccUsers ?? ''
|
|
|
}
|
|
}
|
|
@@ -356,8 +429,6 @@ function loadNodeProps(data: any) {
|
|
|
nodeProps.approveMode = approveMode
|
|
nodeProps.approveMode = approveMode
|
|
|
nodeProps.timeoutHours = cloned.timeoutHours ?? undefined
|
|
nodeProps.timeoutHours = cloned.timeoutHours ?? undefined
|
|
|
nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind'
|
|
nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind'
|
|
|
- } else if (data.type === 'condition-node') {
|
|
|
|
|
- nodeProps.condition = cloned.condition ?? ''
|
|
|
|
|
} else if (data.type === 'cc-node') {
|
|
} else if (data.type === 'cc-node') {
|
|
|
nodeProps.ccUsers = cloned.ccUsers ?? ''
|
|
nodeProps.ccUsers = cloned.ccUsers ?? ''
|
|
|
}
|
|
}
|
|
@@ -476,11 +547,25 @@ function initLogicFlow() {
|
|
|
saveCurrentNodeProps()
|
|
saveCurrentNodeProps()
|
|
|
selectedNode.value = data
|
|
selectedNode.value = data
|
|
|
loadNodeProps(data)
|
|
loadNodeProps(data)
|
|
|
|
|
+ loadOutgoingEdges(data.id)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
lf.on('blank:click', () => {
|
|
lf.on('blank:click', () => {
|
|
|
saveCurrentNodeProps()
|
|
saveCurrentNodeProps()
|
|
|
selectedNode.value = null
|
|
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
|
|
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
|
|
return data
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 将后端的 graphData 转换为 LogicFlow 格式
|
|
// 将后端的 graphData 转换为 LogicFlow 格式
|
|
|
function convertToFrontendFormat(graphData: any) {
|
|
function convertToFrontendFormat(graphData: any) {
|
|
|
const data = JSON.parse(JSON.stringify(graphData))
|
|
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) {
|
|
if (data.nodes) {
|
|
|
data.nodes = data.nodes.map((node: any) => {
|
|
data.nodes = data.nodes.map((node: any) => {
|
|
|
const n = { ...node }
|
|
const n = { ...node }
|
|
@@ -714,7 +865,7 @@ function validateFlow(graphData: any): string | null {
|
|
|
return `节点 "${node.text || node.name}" 是孤立节点,请删除或连接`
|
|
return `节点 "${node.text || node.name}" 是孤立节点,请删除或连接`
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- // 检查条件分支是否有默认分支
|
|
|
|
|
|
|
+ // 检查条件分支:有且只能有一个默认分支,非默认分支必须填写条件
|
|
|
const sourceMap: Record<string, any[]> = {}
|
|
const sourceMap: Record<string, any[]> = {}
|
|
|
for (const edge of edges) {
|
|
for (const edge of edges) {
|
|
|
if (!sourceMap[edge.sourceNodeId]) sourceMap[edge.sourceNodeId] = []
|
|
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') {
|
|
if (node.type === 'condition-node' || node.type === 'condition') {
|
|
|
const outEdges = sourceMap[node.id] || []
|
|
const outEdges = sourceMap[node.id] || []
|
|
|
if (outEdges.length > 1) {
|
|
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;
|
|
font-weight: bold;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
}
|
|
}
|
|
|
|
|
+.branch-table {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+.branch-table :deep(.el-input__wrapper) {
|
|
|
|
|
+ padding-left: 4px;
|
|
|
|
|
+ padding-right: 4px;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|