HomeView.vue
<template> <div class="flowchart-app"> <div>流程图展示</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 } from 'vue' import { flowData } from './rule-node.js' import FlowNode from './FlowNode.vue' const flowNodeRef = ref(null) const showAllErrors = ref(false) 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" @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="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(() => props.node.type !== 'PATTERN') const hasChildList = computed(() => props.node.childList?.length > 0) // 添加操作节点 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: () => { let allValid = true const invalidNodes = [] // 验证当前节点 if (props.node.type === 'PATTERN') { const isValid = patternNodeRef.value?.validate() ?? false if (!isValid) { allValid = false invalidNodes.push({ node: props.node, invalidFields: patternNodeRef.value?.getInvalidFields() || [] }) } } // 递归验证子节点 childNodeRefs.value.forEach(childRef => { const result = childRef?.validateAll() if (!result.allValid) { allValid = false invalidNodes.push(...result.invalidNodes) } }) 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; } } </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) } // 暴露验证方法 defineExpose({ validate: () => { localShowError.value = true return isValid.value }, getInvalidFields: () => { const invalidFields = [] if (!props.node.pattern.leftValue) invalidFields.push('左值') if (!props.node.pattern.rightSource) invalidFields.push('右值来源') if (!props.node.pattern.rightValue) invalidFields.push('右值') return invalidFields }, }) // 删除当前节点 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>
rule-node.js
import { ref } from 'vue'
// 流程图数据
export const flowData = ref([
{
type: 'AND',
childList: [
{
type: 'PATTERN',
pattern: {
leftValue: 'obj',
leftSource: 'Main',
rightSource: 'Main',
rightValue: '',
},
},
{
type: 'PATTERN',
pattern: {
leftValue: 'obj',
leftSource: 'Main',
rightSource: 'Main',
rightValue: 'kks',
},
},
{
type: 'AND',
childList: [
{
type: 'PATTERN',
pattern: {
leftValue: 'obj',
leftSource: 'Main',
rightSource: '',
rightValue: '',
},
},
{
type: 'PATTERN',
pattern: {
leftValue: 'obj',
leftSource: 'Main',
rightSource: 'Main',
rightValue: 'kks',
},
},
],
},
],
},
])
// 选择器Option数据
export const selectOptions = ref([
{
value: 'AND',
label: '并且',
},
{
value: 'OR',
label: '或者',
},
])
// 选择器绑定值
export const selectValue = ref('AND')
// 节点操作方法
export const useRuleNode = () => {
/**
* 添加操作节点
* @param {Object} parentNode - 父节点对象
* @param {Number} [index] - 插入位置,默认为末尾
*/
const addActionNode = (parentNode, index) => {
if (!parentNode.childList) {
parentNode.childList = []
}
const newNode = {
type: 'AND',
childList: [],
}
if (typeof index === 'number') {
parentNode.childList.splice(index, 0, newNode)
} else {
parentNode.childList.push(newNode)
}
return newNode
}
/**
* 添加决策节点
* @param {Object} parentNode - 父节点对象
* @param {Number} [index] - 插入位置,默认为末尾
*/
const addDecisionNode = (parentNode, index) => {
if (!parentNode.childList) {
parentNode.childList = []
}
const newNode = {
type: 'PATTERN',
pattern: {
leftValue: '',
leftSource: 'Main',
rightSource: '',
rightValue: '',
},
}
if (typeof index === 'number') {
parentNode.childList.splice(index, 0, newNode)
} else {
parentNode.childList.push(newNode)
}
return newNode
}
/**
* 根据路径查找节点
* @param {Array} path - 节点路径数组
*/
const findNodeByPath = (path) => {
let current = flowData.value
for (const segment of path) {
current = current[segment]
}
return current
}
return {
addActionNode,
addDecisionNode,
findNodeByPath,
}
}
<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="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(() => props.node.type !== 'PATTERN')
const hasChildList = computed(() => props.node.childList?.length > 0)
// 添加操作节点
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: () => {
let allValid = true
const invalidNodes = []
// 验证当前节点
if (props.node.type === 'PATTERN') {
const isValid = patternNodeRef.value?.validate() ?? false
if (!isValid) {
allValid = false
invalidNodes.push({
node: props.node,
invalidFields: patternNodeRef.value?.getInvalidFields() || []
})
}
}
// 递归验证子节点
childNodeRefs.value.forEach(childRef => {
const result = childRef?.validateAll()
if (!result.allValid) {
allValid = false
invalidNodes.push(...result.invalidNodes)
}
})
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;
}
}
</style>
浙公网安备 33010602011771号