流程图有两个类型节点,type:a有子节点,功能:添加type:a节点,添加type:b节点,删除节点。type:b没有子节点,功能:删除
1.FlowNode.vue---type:a节点
<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="selectValue"
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
:node="node"
:parentNode="parentNode"
@update="handleUpdate"
/>
<div v-if="hasExpression" class="expression-connector"></div>
</div>
<div v-if="hasExpression" class="expression-container">
<div
:style="{ height: 65 * (node.expression.length - 1) + 'px' }"
class="vertical-line"
></div>
<div class="expression-nodes">
<FlowNode
v-for="(child, index) in node.expression"
:key="index"
:node="child"
:is-last="index === node.expression.length - 1"
:is-nested="true"
:parentNode="node"
@update="handleUpdate"
/>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRuleNode, selectOptions, selectValue } 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,
},
})
const emit = defineEmits(['update'])
const { addActionNode, addDecisionNode } = useRuleNode()
const isTypeA = computed(() => props.node.type === 'A')
const hasExpression = computed(() => props.node.expression?.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.expression.splice(props.index, 1)
emit('update')
}
}
</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;
}
}
/* 主节点右侧的水平连接线 */
.expression-connector {
width: 20px;
height: 2px;
background: #999;
margin-left: -1px;
z-index: 1;
margin-top: 2px;
}
.expression-container {
display: flex;
}
/* 左侧垂直连接线 */
.vertical-line {
width: 2px;
background: #999;
margin-right: 18px;
margin-top: 22px;
}
.expression-nodes {
display: flex;
flex-direction: column;
flex-grow: 1;
}
/* 子节点左侧的水平连接线 */
.expression-nodes > .node-wrapper > .node-container::before {
content: '';
position: absolute;
left: -20px;
top: 50%;
width: 20px;
height: 2px;
background: #999;
z-index: 1;
}
/* 最后一个子节点的垂直连接线缩短 */
.expression-nodes
> .node-wrapper:last-child
> .expression-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>
2.HomeView.vue---流程图入口
<template>
<div class="flowchart-app">
<div>流程图展示</div>
<div
class="flowchart-container"
v-for="(flow, index) in flowData"
:key="'flow' + index"
>
<FlowNode
:node="flow"
:index="index"
@update="handleUpdate"
/>
</div>
</div>
</template>
<script setup>
import { flowData } from './rule-node.js'
import FlowNode from './FlowNode.vue'
const handleUpdate = () => {
// 触发数据更新
flowData.value = [...flowData.value]
}
</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>
3.PatternNode.vue---type:b节点
<template>
<div class="pattern-node-box">
<!-- 节点 -->
<div class="pattern-node-container">
<span class="node-label">决策节点</span>
<span v-if="node.content?.value" class="node-value"
>: {{ node.content.value }}</span
>
</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>
const props = defineProps({
parentNode: {
type: Object,
default: null,
},
node: {
type: Object,
required: true,
},
})
const emit = defineEmits(['update'])
// 删除当前节点
const handleDelete = () => {
const index = props.parentNode.expression.findIndex(
(item) => item === props.node
)
if (index !== -1) {
props.parentNode.expression.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;
}
}
</style>
4.rule-node.js---数据、方法文件
import { ref } from 'vue'
// 流程图数据
export const flowData = ref([
{
type: 'A',
expression: [
{
type: 'B',
content: { value: 'b1' },
},
{
type: 'B',
content: { value: 'b2' },
},
{
type: 'A',
expression: [
{
type: 'B',
content: { value: 'b3' },
},
{
type: 'B',
content: { value: 'b4' },
},
],
},
],
},
{
type: 'A',
expression: [
{
type: 'B',
content: { value: 'b1' },
},
{
type: 'B',
content: { value: 'b2' },
},
{
type: 'A',
expression: [
{
type: 'B',
content: { value: 'b3' },
},
{
type: 'A',
expression: [
{
type: 'B',
content: { value: 'b3' },
},
{
type: 'B',
content: { value: 'b4' },
},
],
},
],
},
],
},
])
// 选择器Option数据
export const selectOptions = ref([
{
value: 'Option1',
label: 'Option1',
},
{
value: 'Option2',
label: 'Option2',
},
])
// 选择器绑定值
export const selectValue = ref('Option2')
// 节点操作方法
export const useRuleNode = () => {
/**
* 添加操作节点
* @param {Object} parentNode - 父节点对象
* @param {Number} [index] - 插入位置,默认为末尾
*/
const addActionNode = (parentNode, index) => {
if (!parentNode.expression) {
parentNode.expression = []
}
const newNode = {
type: 'A',
expression: [],
}
if (typeof index === 'number') {
parentNode.expression.splice(index, 0, newNode)
} else {
parentNode.expression.push(newNode)
}
return newNode
}
/**
* 添加决策节点
* @param {Object} parentNode - 父节点对象
* @param {Number} [index] - 插入位置,默认为末尾
*/
const addDecisionNode = (parentNode, index) => {
if (!parentNode.expression) {
parentNode.expression = []
}
const newNode = {
type: 'B',
content: { value: `决策${parentNode.expression.length + 1}` },
}
if (typeof index === 'number') {
parentNode.expression.splice(index, 0, newNode)
} else {
parentNode.expression.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,
}
}
文件结构

流程图结构:

浙公网安备 33010602011771号