import { ref, onMounted, onUnmounted, nextTick, reactive, computed, type Ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import LogicFlow, { RectNode, RectNodeModel, CircleNode, CircleNodeModel, PolygonNode, PolygonNodeModel, } from '@logicflow/core' import { Menu, Snapshot } from '@logicflow/extension' import '@logicflow/core/dist/style/index.css' import '@logicflow/extension/lib/style/index.css' import { ElMessage } from 'element-plus' import type { FormRules } from 'element-plus' import { listRole } from '@/api/system/role' import { addDefinition, updateDefinition, getDefinition } from '@/api/flow/definition' import type { Role } from '@/types/system' import type { FlowDefinition, FormField, FormFieldOption } from '@/types/flow' /* eslint-disable @typescript-eslint/no-explicit-any */ export function useFlowDesigner(lfContainer: Ref) { const route = useRoute() const router = useRouter() let lf: LogicFlow | null = null const lfRef = ref(null) const mode = computed(() => (route.query.mode as string) || '') const flowId = computed(() => (route.query.id as string) || '') const flowCode = computed(() => (route.query.code as string) || '') const flowName = computed(() => (route.query.name as string) || '') const flowCategory = computed(() => (route.query.category as string) || '') const flowDescription = computed(() => (route.query.description as string) || '') const flowInfoVisible = computed(() => mode.value === 'create' || mode.value === 'edit') const selectedNode = ref(null) const roleList = ref([]) const nodeProps = reactive>({ assigneeType: 'ROLE', assigneeValue: '', nodeKey: '', approveMode: 'or', timeoutHours: undefined as number | undefined, timeoutAction: 'remind', approveTimeField: '', ccUsers: '', subNodesEnabled: false, subNodeMode: 'parallel', subNodes: [] as any[], }) // 条件节点出连线配置 const outgoingEdges = ref([]) 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) } function generateSubNodeId(): string { return 'sub-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8) } function getDefaultProps(type: string) { if (type === 'approval-node') return { assigneeType: 'ROLE', assigneeValue: '', nodeKey: '', approveMode: 'or', timeoutHours: undefined, timeoutAction: 'remind', approveTimeField: '', subNodesEnabled: false, subNodeMode: 'parallel', subNodes: [], } if (type === 'cc-node') return { ccUsers: '' } return {} } function saveCurrentNodeProps() { if (selectedNode.value && lf) { const type = selectedNode.value.type const defaultProps = getDefaultProps(type) // 只保存当前节点类型相关的属性,避免不同类型节点属性互相污染 const props: Record = JSON.parse(JSON.stringify(defaultProps)) if (type === 'approval-node') { props.assigneeType = nodeProps.assigneeType ?? 'ROLE' props.assigneeValue = nodeProps.assigneeValue ?? '' props.nodeKey = nodeProps.nodeKey ?? '' props.approveMode = nodeProps.approveMode ?? 'or' props.timeoutHours = nodeProps.timeoutHours props.timeoutAction = nodeProps.timeoutAction ?? 'remind' props.approveTimeField = nodeProps.approveTimeField ?? '' props.subNodesEnabled = nodeProps.subNodesEnabled ?? false props.subNodeMode = nodeProps.subNodeMode ?? 'parallel' props.subNodes = Array.isArray(nodeProps.subNodes) ? nodeProps.subNodes : [] } else if (type === 'cc-node') { props.ccUsers = nodeProps.ccUsers ?? '' } lf.setProperties(selectedNode.value.id, props) // 同步子节点数量角标到节点文本 if (type === 'approval-node') { syncNodeTextWithSubNodeBadge(selectedNode.value.id) } // 通知画布刷新子节点 overlay lf.emit('properties:changed', { nodeId: selectedNode.value.id }) } } function getNodeRawText(nodeId: string): string { if (!lf) return '' try { const node = lf.getNodeData(nodeId) const text = node?.text?.value || node?.text || '' return String(text).replace(/ \(\+\d+\)$/, '') } catch { return '' } } function syncNodeTextWithSubNodeBadge(nodeId: string) { if (!lf) return const rawText = getNodeRawText(nodeId) if (!rawText) return // 不再把子节点数量拼进节点文本,改为画布浮层角标 lf.updateText(nodeId, rawText) } function loadNodeProps(data: any) { const props = data.properties || getDefaultProps(data.type) const cloned = JSON.parse(JSON.stringify(props)) // 先重置为默认值,避免上一个节点的属性残留 Object.assign(nodeProps, getDefaultProps(data.type)) // 兼容旧格式:approver + approveType -> 新格式 if (data.type === 'approval-node') { const supportedTypes = ['ROLE', 'SELF', 'EXTERNAL'] let assigneeType = cloned.assigneeType ?? 'ROLE' let assigneeValue = cloned.assigneeValue ?? '' const approveMode = cloned.approveMode ?? cloned.approveType ?? 'or' if (!cloned.assigneeType && !cloned.assigneeValue && cloned.approver) { assigneeType = 'ROLE' assigneeValue = cloned.approver } // 当前设计器支持 ROLE / SELF / EXTERNAL,其它旧类型降级为 ROLE 并清空值 if (!supportedTypes.includes(assigneeType)) { assigneeType = 'ROLE' assigneeValue = '' } // SELF / EXTERNAL 不需要审批角色值 if (assigneeType === 'SELF' || assigneeType === 'EXTERNAL') { assigneeValue = '' } nodeProps.assigneeType = assigneeType nodeProps.assigneeValue = assigneeValue nodeProps.nodeKey = cloned.nodeKey ?? '' nodeProps.approveMode = approveMode nodeProps.timeoutHours = cloned.timeoutHours ?? undefined nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind' nodeProps.approveTimeField = cloned.approveTimeField ?? '' nodeProps.subNodesEnabled = cloned.subNodesEnabled === true nodeProps.subNodeMode = cloned.subNodeMode ?? 'parallel' nodeProps.subNodes = Array.isArray(cloned.subNodes) ? cloned.subNodes.map((s: any) => ({ id: s.id || generateSubNodeId(), name: s.name || '', assigneeType: s.assigneeType ?? 'ROLE', assigneeValue: s.assigneeValue ?? '', approveMode: s.approveMode ?? 'or', timeoutHours: s.timeoutHours ?? undefined, timeoutAction: s.timeoutAction ?? 'remind', })) : [] } else if (data.type === 'cc-node') { nodeProps.ccUsers = cloned.ccUsers ?? '' } } function registerNodes() { if (!lf) return class StartNodeModel extends CircleNodeModel { setAttributes() { this.r = 30 if (!this.text.value) this.text.value = '开始' } getNodeStyle() { const style = super.getNodeStyle() style.fill = '#67C23A' style.stroke = '#67C23A' return style } getTextStyle() { const style = super.getTextStyle() style.color = '#fff' return style } } lf.register({ type: 'start-node', view: CircleNode, model: StartNodeModel }) class ApprovalNodeModel extends RectNodeModel { setAttributes() { this.width = 160 this.height = 60 this.radius = 8 if (!this.text.value) this.text.value = '审批节点' } getNodeStyle() { const style = super.getNodeStyle() style.fill = '#fff' style.stroke = '#409EFF' style.strokeWidth = 1 return style } } lf.register({ type: 'approval-node', view: RectNode, model: ApprovalNodeModel }) class CcNodeModel extends RectNodeModel { setAttributes() { this.width = 160 this.height = 60 this.radius = 8 if (!this.text.value) this.text.value = '抄送节点' } getNodeStyle() { const style = super.getNodeStyle() style.fill = '#fff' style.stroke = '#E6A23C' style.strokeWidth = 1 return style } } lf.register({ type: 'cc-node', view: RectNode, model: CcNodeModel }) class ConditionNodeModel extends PolygonNodeModel { setAttributes() { this.points = [ [60, 0], [120, 40], [60, 80], [0, 40], ] if (!this.text.value) this.text.value = '条件' } getNodeStyle() { const style = super.getNodeStyle() style.fill = '#fff' style.stroke = '#909399' return style } } lf.register({ type: 'condition-node', view: PolygonNode, model: ConditionNodeModel }) class EndNodeModel extends CircleNodeModel { setAttributes() { this.r = 30 if (!this.text.value) this.text.value = '结束' } getNodeStyle() { const style = super.getNodeStyle() style.fill = '#F56C6C' style.stroke = '#F56C6C' return style } getTextStyle() { const style = super.getTextStyle() style.color = '#fff' return style } } lf.register({ type: 'end-node', view: CircleNode, model: EndNodeModel }) } function defaultNodes() { return [ { id: 'start-1', type: 'start-node', x: 200, y: 100, text: '开始' }, { id: 'end-1', type: 'end-node', x: 200, y: 500, text: '结束' }, ] } function initLogicFlow() { if (!lfContainer.value) return lf = new LogicFlow({ container: lfContainer.value, grid: true, plugins: [Menu, Snapshot], pluginsOptions: { menu: { nodeMenu: [ { text: '删除', callback(node: any) { lf?.deleteNode(node.id) }, }, ], }, } as any, }) lfRef.value = lf registerNodes() lf.on('node:click', ({ data }: any) => { 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) } }) // 编辑模式:加载已有流程图 if (mode.value === 'edit' && flowId.value) { getDefinition(Number(flowId.value)) .then((def: FlowDefinition) => { parseFormSchema(def.formSchema) if (def.flowJson) { try { const parsed = JSON.parse(def.flowJson) const graphData = convertToFrontendFormat(parsed) lf?.render(graphData) nextTick(() => syncAllSubNodeBadges()) } catch { lf?.render({ nodes: defaultNodes(), edges: [] }) } } else { lf?.render({ nodes: defaultNodes(), edges: [] }) } }) .catch(() => { lf?.render({ nodes: defaultNodes(), edges: [] }) }) } else { lf.render({ nodes: defaultNodes(), edges: [] }) } nextTick(() => syncAllSubNodeBadges()) } function syncAllSubNodeBadges() { if (!lf) return const graphData = lf.getGraphData() for (const node of graphData.nodes || []) { if (node.type !== 'approval-node') continue syncNodeTextWithSubNodeBadge(node.id) } } const typeToBackend: Record = { 'start-node': 'start', 'approval-node': 'approval', 'cc-node': 'cc', 'condition-node': 'condition', 'end-node': 'end', } const typeToFrontend: Record = { start: 'start-node', approval: 'approval-node', cc: 'cc-node', condition: 'condition-node', end: 'end-node', } // 将 LogicFlow 的 graphData 转换为后端格式 function convertToBackendFormat(graphData: any) { const data = JSON.parse(JSON.stringify(graphData)) if (data.nodes) { data.nodes = data.nodes.map((node: any) => { const n = { ...node } if (n.type && typeToBackend[n.type]) { n.type = typeToBackend[n.type] } // text -> name if (n.text !== undefined) { n.name = typeof n.text === 'object' ? n.text.value : n.text // 去掉子节点数量角标,避免保存到后端 n.name = String(n.name).replace(/ \(\+\d+\)$/, '') delete n.text } return n }) } // 将 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) 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 } if (n.type && typeToFrontend[n.type]) { n.type = typeToFrontend[n.type] } // name -> text if (n.name !== undefined) { n.text = String(n.name).replace(/ \(\+\d+\)$/, '') delete n.name } return n }) } return data } function handleDrop(e: DragEvent) { e.preventDefault() const dataStr = e.dataTransfer?.getData('application/json') if (!dataStr || !lf || !lfContainer.value) return const nodeData = JSON.parse(dataStr) const rect = lfContainer.value.getBoundingClientRect() lf.addNode({ type: nodeData.type, x: e.clientX - rect.left, y: e.clientY - rect.top, text: nodeData.label, }) } function updateNodeName() { if (!lf || !selectedNode.value) return const text = selectedNode.value.text const value = typeof text === 'object' && text !== null ? text.value || '' : text || '' // 使用 LogicFlow 公开 API updateText 更新节点文本 lf.updateText(selectedNode.value.id, value) } // 保存流程 const saveDialogVisible = ref(false) const saveForm = reactive({ code: '', name: '', category: '', description: '', }) const saveFormRules: FormRules = { code: [{ required: true, message: '请输入流程编码', trigger: 'blur' }], name: [{ required: true, message: '请输入流程名称', trigger: 'blur' }], } const formFields = ref([]) const fieldTypeOptions = [ { label: '文本', value: 'text' }, { label: '多行文本', value: 'textarea' }, { label: '数字', value: 'number' }, { label: '日期', value: 'date' }, { label: '下拉选择', value: 'select' }, ] function addFormField() { formFields.value.push({ name: '', label: '', type: 'text', required: false }) } function removeFormField(index: number) { formFields.value.splice(index, 1) } function getFormSchema(): string { const validFields = formFields.value.filter((f) => f.name.trim() && f.label.trim()) return validFields.length > 0 ? JSON.stringify(validFields) : '' } function parseFormSchema(schema?: string) { if (!schema) return try { const parsed = JSON.parse(schema) if (Array.isArray(parsed)) { formFields.value = parsed } } catch { formFields.value = [] } } // 下拉选项编辑 const optionDialogVisible = ref(false) const optionEditingIndex = ref(null) const optionEditingField = ref(null) const optionList = ref([]) function openOptionDialog(index: number) { optionEditingIndex.value = index optionEditingField.value = formFields.value[index] const rawOptions = optionEditingField.value.options || [] optionList.value = rawOptions.map((opt: any) => { if (typeof opt === 'string') { return { label: opt, value: opt } } return { label: String(opt.label ?? opt.value), value: opt.value } }) optionDialogVisible.value = true } function addOption() { optionList.value.push({ label: '', value: '' }) } function removeOption(index: number) { optionList.value.splice(index, 1) } function saveOptions() { if (optionEditingIndex.value === null || !optionEditingField.value) return const validOptions = optionList.value.filter( (opt) => String(opt.label).trim() !== '' && String(opt.value).trim() !== '' ) formFields.value[optionEditingIndex.value].options = validOptions formFields.value[optionEditingIndex.value].multiple = optionEditingField.value.multiple optionDialogVisible.value = false } function validateFlow(graphData: any): string | null { const nodes = graphData.nodes || [] const edges = graphData.edges || [] if (nodes.length === 0) return '流程图不能为空' const startNodes = nodes.filter((n: any) => n.type === 'start-node' || n.type === 'start') const endNodes = nodes.filter((n: any) => n.type === 'end-node' || n.type === 'end') if (startNodes.length === 0) return '流程图缺少开始节点' if (endNodes.length === 0) return '流程图缺少结束节点' if (startNodes.length > 1) return '流程图只能有一个开始节点' // 检查审批节点是否配置了审批对象 const externalNodeKeys = new Set() for (const node of nodes) { if (node.type === 'approval-node' || node.type === 'approval') { const props = node.properties || {} const assigneeType = props.assigneeType ?? 'ROLE' const assigneeValue = props.assigneeValue || props.approver if (assigneeType === 'ROLE' && !assigneeValue && props.subNodesEnabled !== true) { return `审批节点 ${node.text || node.name} 未选择审批角色` } if (assigneeType === 'EXTERNAL') { if (props.subNodesEnabled === true) { return `审批节点 "${node.text || node.name}" 使用外部指定审批人时不支持子节点` } const nodeKey = String(props.nodeKey || '').trim() if (!nodeKey) { return `审批节点 "${node.text || node.name}" 未填写节点编码(nodeKey)` } if (externalNodeKeys.has(nodeKey)) { return `节点编码 "${nodeKey}" 重复,请保证每个外部指定节点编码唯一` } externalNodeKeys.add(nodeKey) } if (props.subNodesEnabled === true) { const subNodes = props.subNodes || [] if (subNodes.length === 0) { return `审批节点 "${node.text || node.name}" 已启用子节点,请至少配置一个子节点` } for (let i = 0; i < subNodes.length; i++) { const sub = subNodes[i] const subAssigneeType = sub.assigneeType ?? 'ROLE' const subAssigneeValue = sub.assigneeValue if (subAssigneeType === 'ROLE' && !subAssigneeValue) { return `审批节点 ${node.text || node.name} 的子节点 ${i + 1} 未选择审批角色` } } } } } // 检查孤立节点 const connectedNodeIds = new Set() for (const edge of edges) { connectedNodeIds.add(edge.sourceNodeId) connectedNodeIds.add(edge.targetNodeId) } for (const node of nodes) { if ( !connectedNodeIds.has(node.id) && node.type !== 'start-node' && node.type !== 'start' && node.type !== 'end-node' && node.type !== 'end' ) { return `节点 "${node.text || node.name}" 是孤立节点,请删除或连接` } } // 检查条件分支:有且只能有一个默认分支,非默认分支必须填写条件 const sourceMap: Record = {} for (const edge of edges) { if (!sourceMap[edge.sourceNodeId]) sourceMap[edge.sourceNodeId] = [] sourceMap[edge.sourceNodeId].push(edge) } for (const node of nodes) { if (node.type === 'condition-node' || node.type === 'condition') { const outEdges = sourceMap[node.id] || [] if (outEdges.length > 1) { 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}" 的非默认分支必须填写条件表达式` } } } } return null } async function handleSave() { if (!lf) return saveCurrentNodeProps() // 先刷新属性面板中可能未失焦的节点名称修改,再读取画布数据 updateNodeName() const rawGraphData = lf.getGraphData() const error = validateFlow(rawGraphData) if (error) { ElMessage.warning(error) return } // 统一弹出保存弹窗,方便配置流程表单字段 if (mode.value === 'create') { saveForm.code = flowCode.value saveForm.name = flowName.value saveForm.category = flowCategory.value saveForm.description = flowDescription.value formFields.value = [] } else if (mode.value === 'edit' && flowId.value) { saveForm.code = flowCode.value saveForm.name = flowName.value saveForm.category = flowCategory.value saveForm.description = flowDescription.value // formFields 在编辑模式加载流程时已通过 parseFormSchema 填充 } else { saveForm.code = '' saveForm.name = '' saveForm.category = '' saveForm.description = '' formFields.value = [] } saveDialogVisible.value = true } async function submitSave() { if (!lf) return saveCurrentNodeProps() // 先刷新属性面板中可能未失焦的节点名称修改,再读取画布数据 updateNodeName() const rawGraphData = lf.getGraphData() const error = validateFlow(rawGraphData) if (error) { ElMessage.warning(error) return } const graphData = convertToBackendFormat(rawGraphData) if (mode.value === 'create') { const data: Partial = { code: saveForm.code, name: saveForm.name, category: saveForm.category, description: saveForm.description, formSchema: getFormSchema(), flowJson: JSON.stringify(graphData), status: 0, } await addDefinition(data) ElMessage.success('流程新增成功') saveDialogVisible.value = false router.push('/flow/definition') } else if (mode.value === 'edit' && flowId.value) { const data: Partial = { id: Number(flowId.value), code: saveForm.code, name: saveForm.name, category: saveForm.category, description: saveForm.description, formSchema: getFormSchema(), flowJson: JSON.stringify(graphData), } const newId = await updateDefinition(data) if (newId !== Number(flowId.value)) { ElMessage.success('流程已创建为新版本,请重新发布') } else { ElMessage.success('流程修改成功') } saveDialogVisible.value = false router.push('/flow/definition') } else { const data: Partial = { code: saveForm.code, name: saveForm.name, category: saveForm.category, description: saveForm.description, formSchema: getFormSchema(), flowJson: JSON.stringify(graphData), status: 0, } await addDefinition(data) ElMessage.success('流程保存成功') saveDialogVisible.value = false router.push('/flow/definition') } } function deleteSelectedNode() { if (!lf || !selectedNode.value) return lf.deleteNode(selectedNode.value.id) selectedNode.value = null outgoingEdges.value = [] } function selectNodeById(nodeId: string) { if (!lf) return try { const nodeData = lf.getNodeData(nodeId) if (!nodeData) return lf.selectElementById(nodeId) saveCurrentNodeProps() selectedNode.value = nodeData loadNodeProps(nodeData) loadOutgoingEdges(nodeData.id) } catch { // ignore } } function handleClear() { if (!lf) return lf.clearData() selectedNode.value = null } function goBack() { router.push('/flow/definition') } async function loadRoles() { try { const res = await listRole({ pageNum: 1, pageSize: 100 }) roleList.value = res.list } catch { /* ignore */ } } onMounted(() => { // 防止快速进入/离开设计器时重复创建 LogicFlow 实例(v1.x 无 destroy 方法) if (lf) { if (typeof lf.destroy === 'function') { lf.destroy() } lf = null } nextTick(initLogicFlow) loadRoles() }) onUnmounted(() => { if (lf) { if (typeof lf.destroy === 'function') { lf.destroy() } lf = null } selectedNode.value = null }) return { // flow info mode, flowId, flowCode, flowName, flowCategory, flowDescription, flowInfoVisible, // logic flow / selection selectedNode, outgoingEdges, roleList, nodeProps, lfRef, // property panel helpers loadOutgoingEdges, getEdgeTargetName, onDefaultChange, onBranchBlur, saveCurrentNodeProps, loadNodeProps, updateNodeName, generateSubNodeId, deleteSelectedNode, selectNodeById, // save dialog saveDialogVisible, saveForm, saveFormRules, formFields, fieldTypeOptions, addFormField, removeFormField, getFormSchema, parseFormSchema, // option dialog optionDialogVisible, optionEditingIndex, optionEditingField, optionList, openOptionDialog, addOption, removeOption, saveOptions, // toolbar actions handleSave, submitSave, handleClear, goBack, // drag/drop handleDrop, } }