TaskNode.vue
<template> <div class="task-container"> <div class="task-controls"> <button @click="handleFinish">完成</button> <span class="node-count">节点数: {{ totalNodes }}/300</span> <el-button type="primary" @click="handleFinish"> 完成 </el-button> </div> <HomeView ref="homeView" :show-errors="showErrors" @node-updated="updateNodeCount" /> </div> </template> <script setup> import { ref, computed } from 'vue' import { ElMessage } from 'element-plus' import HomeView from './HomeView.vue' const homeView = ref(null) const totalNodes = ref(0) const showErrors = ref(false) const MAX_NODES = 300 const isLimitReached = computed(() => totalNodes.value >= MAX_NODES) // 更新节点计数 const updateNodeCount = (count) => { totalNodes.value = count } // 完成校验 const handleFinish = async () => { try { const isValid = await homeView.value.validateAll() if (isValid) { console.log('校验通过,可以提交') } } catch (error) { console.log('校验未通过:', error.message) } } </script> <style scoped> .task-container { padding: 20px; } .task-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .node-count { font-weight: bold; color: var(--el-color-primary); } </style>
HomeView.vue
<template> <div class="flowchart-container"> <FlowNode ref="flowNodeRef" :node="flowData[0]" :show-errors="showErrors" @update="$emit('node-updated', getTotalNodes(flowData[0]))" /> </div> </template> <script setup> import { ref, defineEmits } from 'vue' import { flowData } from './rule-node.js' import FlowNode from './FlowNode.vue' const emit = defineEmits(['node-updated']) const flowNodeRef = ref(null) const props = defineProps({ showErrors: Boolean, }) const showAllErrors = ref(false) // 节点总数 const totalNodes = ref(0) // 计算总节点数的方法 // 更新节点总数 const updateTotalNodes = () => { totalNodes.value = getTotalNodes(flowData.value[0]) } // 初始化计算 updateTotalNodes() // 提供给子组件 provide('totalNodes', totalNodes) provide('updateTotalNodes', updateTotalNodes) provide('getTotalNodes', getTotalNodes) // 计算总节点数 const getTotalNodes = (node) => { if (!node) return 0 let count = 1 if (node.childList) { node.childList.forEach((child) => { count += getTotalNodes(child) }) } return count } // HomeView.vue const validateAll = async () => { const result = flowNodeRef.value?.validateAll() if (!result || !result.allValid) { const errorCount = result?.invalidNodes?.reduce( (acc, node) => acc + node.invalidFields.length, 0 ) || 0 throw new Error(`发现 ${errorCount} 个必填项未填写`) } return true } defineExpose({ validateAll }) </script> <style scoped> .flowchart-container { border: 1px solid #eee; border-radius: 8px; padding: 20px; 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>
PatternNode.vue
<template> <div class="pattern-node-box"> <div class="pattern-node-container"> <!-- 左值 --> <div class="form-item"> <el-select style="width: 240px" v-model="node.pattern.leftValue" placeholder="请选择左值" > <el-option v-for="item in options" :key="item" :value="item" /> </el-select> <div v-if="(localShowError || showAllErrors) && !node.pattern.leftValue" class="error-message" > 必填 </div> </div> <!-- 右值来源 --> <div class="form-item"> <el-select v-model="node.pattern.rightSource" placeholder="请选择右值来源" style="width: 240px" > <el-option v-for="item in rightSourceOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> <div v-if="(localShowError || showAllErrors) && !node.pattern.rightSource" class="error-message" > 必填 </div> </div> <!-- 右值 --> <div class="form-item"> <el-select v-if="node.pattern.rightSource == 'Main'" v-model="node.pattern.rightValue" placeholder="请选择右值" style="width: 240px" > <el-option v-for="item in options" :key="item" :value="item" /> </el-select> <el-input v-if="node.pattern.rightSource == 'Custom'" v-model="node.pattern.rightValue" placeholder="请输入右值" style="width: 240px" /> <div v-if="(localShowError || showAllErrors) && !node.pattern.rightValue" class="error-message" > 必填 </div> </div> </div> <!-- 图标 --> <el-popover placement="right-start" :width="80"> <template #reference> <el-icon class="set-menu-icon"> <Tools /> </el-icon> </template> <div class="menu-item" @click="handleDelete">删除</div> </el-popover> </div> </template> <script setup> import { ref, computed, defineExpose } from 'vue' const props = defineProps({ parentNode: { type: Object, default: null, }, node: { type: Object, required: true, }, showAllErrors: { type: Boolean, default: false, }, }) const emit = defineEmits(['validate']) const localShowError = ref(false) const isValid = computed(() => { return ( props.node.pattern.leftValue && props.node.pattern.rightSource && props.node.pattern.rightValue ) }) const rightSourceOptions = ref([ { label: '数据列', value: 'Main', }, { label: '手动输入', value: 'Custom', }, ]) const options = ref(['sgdsd', 'dgdg', 'eefdf', 'sfsf']) const handleChange = () => { emit('validate', isValid.value) } const getInvalidFields = () => { const fields = [] if (!props.node.pattern.leftValue) fields.push('左值未选择') if (!props.node.pattern.rightSource) fields.push('未选择右值来源') if (props.node.pattern.rightSource && !props.node.pattern.rightValue) { fields.push( props.node.pattern.rightSource === 'Main' ? '右值未选择' : '右值未输入' ) } return fields } // PatternNode.vue const validate = () => { localShowError.value = true // 确保显示错误状态 // 根据当前显示状态动态校验 let isValid = !!props.node.pattern.leftValue && !!props.node.pattern.rightSource if ( props.node.pattern.rightSource === 'Main' || props.node.pattern.rightSource === 'Custom' ) { isValid = isValid && !!props.node.pattern.rightValue } return isValid } // 暴露验证方法 defineExpose({ validate, getInvalidFields, }) // 删除当前节点 const handleDelete = () => { const index = props.parentNode.childList.findIndex( (item) => item === props.node ) if (index !== -1) { props.parentNode.childList.splice(index, 1) emit('update') } else { console.warn('未找到要删除的节点') } } </script> <style scoped lang="less"> .pattern-node-box { display: inline-flex; align-items: center; .set-menu-icon { margin-left: 8px; } } .pattern-node-container { padding: 10px 15px; border-radius: 20px; min-width: 120px; text-align: center; background-color: #2196f3; 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; } } .pattern-node-container { display: flex; gap: 10px; padding: 10px; background-color: #f5f7fa; border-radius: 4px; } .form-item { position: relative; } .error-message { color: #f56c6c; font-size: 12px; position: absolute; bottom: -18px; left: 6px; } </style>
浙公网安备 33010602011771号