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" @click="handleAddAction">添加操作</div> <div class="menu-item" @click="handleAddDecision">添加节点</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 } 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) // 添加操作节点 const handleAddAction = () => { addActionNode(props.node) emit('update') } // 添加决策节点 const handleAddDecision = () => { addDecisionNode(props.node) emit('update') } // 删除当前节点 const handleDelete = () => { if (props.parentNode && typeof props.index === 'number') { props.parentNode.childList.splice(props.index, 1) emit('update') } } // 用于获取 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; } </style>
浙公网安备 33010602011771号