HomeView.vue
<template> <div class="flowchart-app"> <div>流程图展示{{ nodeCountStatus }}</div> <el-button type="primary" @click="handleFinish">完成</el-button> <div class="flowchart-container"> <FlowNode ref="flowNodeRef" :node="flowData[0]" @update="handleUpdate" /> </div> </div> </template> <script setup> import { ref, provide, computed } from 'vue' import { flowData } from './rule-node.js' import FlowNode from './FlowNode.vue' const flowNodeRef = ref(null) const showAllErrors = ref(false) // 节点总数 const totalNodes = ref(0) // 计算总节点数的方法 const getTotalNodes = (node) => { if (!node) return 0 let count = 1 // 当前节点 if (node.childList) { node.childList.forEach((child) => { count += getTotalNodes(child) }) } return count } // 更新节点总数 const updateTotalNodes = () => { totalNodes.value = getTotalNodes(flowData.value[0]) } // 初始化计算 updateTotalNodes() // 提供给子组件 provide('totalNodes', totalNodes) provide('updateTotalNodes', updateTotalNodes) provide('getTotalNodes', getTotalNodes) // 节点数量状态显示 const nodeCountStatus = computed(() => { return `当前节点数: ${totalNodes.value}/300` }) const handleUpdate = () => { updateTotalNodes() flowData.value = [...flowData.value] } const handleFinish = async () => { showAllErrors.value = true const validationResult = flowNodeRef.value?.validateAll() if (validationResult?.allValid) { console.log('提交数据:', flowData.value) } else { const errorCount = validationResult.invalidNodes.reduce( (acc, node) => acc + node.invalidFields.length, 0 ) console.log(`有${errorCount}处需要修正,请完善所有必填项和节点规则`) } } </script> <style> .flowchart-app { font-family: Arial, sans-serif; padding: 20px; max-width: 1000px; margin: 0 auto; } .flowchart-container { margin-top: 20px; padding: 20px; border: 1px solid #eee; border-radius: 8px; background: #f9f9f9; } </style>
FlowNode.vue
<template> <div class="node-wrapper"> <div class="node-container"> <!-- 操作步骤节点 --> <div v-if="isTypeA" class="action-node-box"> <div class="action-node-container"> <el-icon><Share /></el-icon> <el-select v-model="node.type" placeholder="Select" size="small" style="width: 60px" > <el-option v-for="item in selectOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </div> <!-- 图标 --> <el-popover placement="right-start" :width="100"> <template #reference> <el-icon class="set-menu-icon"> <Tools /> </el-icon> </template> <div class="menu-item" :class="{ 'is-disabled': isNodesLimitReached }" @click="handleAddAction" :title="isNodesLimitReached ? '所有节点数量不能超过300' : ''" > 添加操作 </div> <div class="menu-item" :class="{ 'is-disabled': isNodesLimitReached }" @click="handleAddDecision" :title="isNodesLimitReached ? '所有节点数量不能超过300' : ''" > 添加节点 </div> <div class="menu-item" v-if="isNested" @click="handleDelete()"> 删除 </div> </el-popover> </div> <!-- 决策节点 --> <PatternNode v-else ref="patternNodeRef" :node="node" :parent-node="parentNode" :show-all-errors="showAllErrors" @update="$emit('update')" /> <!-- 逻辑节点错误提示 --> <div v-if="showLogicNodeError" class="logic-node-error"> <el-icon color="#F56C6C"><Warning /></el-icon> <span>至少要添加2个子节点</span> </div> <div v-if="hasChildList" class="childList-connector"></div> </div> <div v-if="hasChildList" class="childList-container"> <div :style="{ height: 65 * (node.childList.length - 1) + 'px' }" class="vertical-line" ></div> <div class="childList-nodes"> <FlowNode v-for="(child, index) in node.childList" :key="index" :ref="(el) => (childNodeRefs[index] = el)" :node="child" :parent-node="node" :show-all-errors="showAllErrors" @update="$emit('update')" /> </div> </div> </div> </template> <script setup> import { computed, ref, inject } from 'vue' import { useRuleNode, selectOptions } from './rule-node.js' import PatternNode from './PatternNode.vue' const props = defineProps({ isNested: { // 新增prop,表示是否是嵌套节点 type: Boolean, default: false, }, node: { type: Object, required: true, }, isLast: { type: Boolean, default: false, }, parentNode: { type: Object, default: null, }, index: { type: Number, default: -1, }, showAllErrors: { type: Boolean, default: false, }, }) const emit = defineEmits(['update']) const { addActionNode, addDecisionNode } = useRuleNode() const isTypeA = computed(() => ['AND', 'OR'].includes(props.node.type)) const hasChildList = computed(() => props.node.childList?.length > 0) const showLogicNodeError = computed(() => { return ( isTypeA.value && (props.showAllErrors || localShowError.value) && (!props.node.childList || props.node.childList.length < 2) ) }) const localShowError = ref(false) // 注入来自App.vue的方法和状态 const totalNodes = inject('totalNodes') const updateTotalNodes = inject('updateTotalNodes') const getTotalNodes = inject('getTotalNodes') // 计算是否达到节点限制 const isNodesLimitReached = computed(() => { return totalNodes.value >= 300 }) // 添加操作节点 const handleAddAction = () => { if (isNodesLimitReached.value) { console.log('节点数量已达上限(300),无法继续添加') return } addActionNode(props.node) emit('update') updateTotalNodes() } // 添加决策节点 const handleAddDecision = () => { if (isNodesLimitReached.value) { console.log('节点数量已达上限(300),无法继续添加') return } addDecisionNode(props.node) emit('update') updateTotalNodes() } // 删除当前节点 const handleDelete = () => { if (props.parentNode && typeof props.index === 'number') { props.parentNode.childList.splice(props.index, 1) emit('update') updateTotalNodes() } } // 用于获取 PatternNode 的引用 const patternNodeRef = ref(null) // 用于存储子 FlowNode 的引用 const childNodeRefs = ref([]) // 暴露验证方法 defineExpose({ validateAll: () => { localShowError.value = true let allValid = true const invalidNodes = [] // 验证当前节点 if (isTypeA.value) { // 逻辑节点必须至少有2个子节点 const childCountValid = props.node.childList?.length >= 2 if (!childCountValid) { allValid = false invalidNodes.push({ node: props.node, invalidFields: ['子节点数量不足'], }) } // 验证所有子节点 childNodeRefs.value.forEach((childRef) => { const result = childRef?.validateAll() if (!result.allValid) { allValid = false invalidNodes.push(...result.invalidNodes) } }) } else { // 模式节点验证 const isValid = patternNodeRef.value?.validate() ?? false if (!isValid) { allValid = false invalidNodes.push({ node: props.node, invalidFields: patternNodeRef.value?.getInvalidFields() || [], }) } } return { allValid, invalidNodes } }, }) </script> <style scoped lang="less"> .node-wrapper { display: flex; align-items: flex-start; margin-bottom: 20px; &:last-child { margin-bottom: 0px; } } .node-container { display: flex; align-items: center; position: relative; } .action-node-container { padding: 10px 15px; border-radius: 20px; min-width: 120px; text-align: center; background-color: #4caf50; color: white; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); position: relative; z-index: 2; display: inline-flex; align-items: center; .node-label { font-weight: 500; } .node-value { margin-left: 4px; opacity: 0.9; } } /* 主节点右侧的水平连接线 */ .childList-connector { width: 20px; height: 2px; background: #999; margin-left: -1px; z-index: 1; margin-top: 2px; } .childList-container { display: flex; } /* 左侧垂直连接线 */ .vertical-line { width: 2px; background: #999; margin-right: 18px; margin-top: 22px; } .childList-nodes { display: flex; flex-direction: column; flex-grow: 1; } /* 添加间距样式确保错误消息有足够空间 */ .node-wrapper { margin-bottom: 24px; } .childList-nodes { margin-top: 8px; } /* 子节点左侧的水平连接线 */ .childList-nodes > .node-wrapper > .node-container::before { content: ''; position: absolute; left: -20px; top: 50%; width: 20px; height: 2px; background: #999; z-index: 1; } /* 最后一个子节点的垂直连接线缩短 */ .childList-nodes > .node-wrapper:last-child > .childList-container > .vertical-line { min-height: 22px; } .action-node-box { position: relative; .set-menu-icon { position: absolute; margin-left: 8px; top: 16px; z-index: 9; } } .logic-node-error { color: #f56c6c; font-size: 12px; margin-top: 4px; display: flex; align-items: center; gap: 4px; animation: shake 0.5s; } .is-disabled { color: #eee; cursor: disabled; } </style>
浙公网安备 33010602011771号