<script setup lang="ts">
import { computed, defineAsyncComponent, h, nextTick, onMounted, ref, render, watch } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import { useWorkflowStoreHook } from '@/stores/modules/workflow'
import { cloneDeep } from '@/utils/general'
import DefaultProps from './DefaultNodeProps'
defineOptions({
name: 'process-tree',
})
// 初始化核心依赖
const emit = defineEmits(['selectedNode'])
// 响应式数据定义
const valid = ref(true)
const _rootContainer = ref(null) // 组件根元素引用
const nodeRefs = ref({}) // 存储所有节点的 ref(替代原 this.$refs)
// 计算属性
const nodeMap = computed(() => useWorkflowStoreHook().nodeMap)
const dom = computed(() => useWorkflowStoreHook().design.process)
// 节点类型与组件映射
const componentMap = {
approval: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/ApprovalNode'),),
cc: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/CcNode')),
concurrent: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/ConcurrentNode'),),
condition: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/ConditionNode'),),
delay: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/DelayNode')),
empty: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/EmptyNode')),
inclusive: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/InclusiveNode'),),
node: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/Node')),
root: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/RootNode')),
subprocess: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/SubprocessNode'),),
task: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/TaskNode')),
trigger: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/TriggerNode')),
default: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/Node')),
}
// 节点类型判断方法
const isPrimaryNode = (node: any) => {
return (
node && ['APPROVAL', 'CC', 'DELAY', 'ROOT', 'SUBPROCESS', 'TASK', 'TRIGGER'].includes(node.type)
)
}
const isBranchNode = (node: any) => {
return node && ['CONCURRENT', 'CONDITION', 'INCLUSIVE'].includes(node.type)
}
const isEmptyNode = (node: any) => {
return node && node.type === 'EMPTY'
}
const isConditionNode = (node: any) => {
return node?.type === 'CONDITION'
}
const isBranchSubNode = (node: any) => {
return node && ['CONCURRENT', 'CONDITION', 'INCLUSIVE'].includes(node.type)
}
const isInclusiveNode = (node: any) => {
return node?.type === 'INCLUSIVE'
}
const isConcurrentNode = (node: any) => {
return node?.type === 'CONCURRENT'
}
// 核心业务方法实现
/**
* 生成随机节点 ID
*/
const getRandomId = () => {
return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random() * 9000 + 1000)}`
}
/**
* 节点 ID 映射到 nodeMap
*/
const toMapping = (node: any) => {
if (node && node.id) {
nodeMap.value.set(node.id, node)
}
}
/**
* 插入分支遮挡线条
*/
const insertCoverLine = (index: number, doms: any[], branchs: any[]) => {
if (index === 0) {
// 最左侧分支
doms.unshift(h('div', { class: 'line-top-left' }, []))
doms.unshift(h('div', { class: 'line-bot-left' }, []))
} else if (index === branchs.length - 1) {
// 最右侧分支
doms.unshift(h('div', { class: 'line-top-right' }, []))
doms.unshift(h('div', { class: 'line-bot-right' }, []))
}
}
/**
* 解码并插入节点 DOM
*/
const decodeAppendDom = (node: any, domList: any[], props = {}) => {
props.config = node
const compKey = node.type.toLowerCase()
const TargetComp = componentMap[compKey as keyof typeof componentMap]
if (!TargetComp) {
console.warn(`未找到匹配的节点组件:${compKey}`)
return
}
// 插入节点组件到 DOM 列表
domList.unshift(
h(TargetComp, {
props: props,
key: node.id,
ref: (el: any) => {
if (el) nodeRefs.value[node.id] = el
},
onInsertNode: (type: string) => insertNode(type, node),
onDelNode: () => delNode(node),
onSelected: () => selectNode(node),
onCopy: () => copyBranch(node),
onLeftMove: () => branchMove(node, -1),
onRightMove: () => branchMove(node, 1),
}),
)
}
/**
* 递归生成流程树 DOM
*/
const buildDomTree = (node: any): any[] => {
if (!node) return []
toMapping(node)
// 1. 普通业务节点
if (isPrimaryNode(node)) {
const childDoms = buildDomTree(node.children)
decodeAppendDom(node, childDoms)
return [h('div', { class: 'primary-node' }, childDoms)]
}
// 2. 分支节点
if (isBranchNode(node)) {
let index = 0
const branchItems = node.branchs.map((branchNode: any) => {
toMapping(branchNode)
const childDoms = buildDomTree(branchNode.children)
decodeAppendDom(branchNode, childDoms, {
level: index + 1,
size: node.branchs.length,
})
insertCoverLine(index, childDoms, node.branchs)
index++
return h('div', { class: 'branch-node-item' }, childDoms)
})
// 插入「添加分支/条件」按钮
branchItems.unshift(
h('div', { class: 'add-branch-btn' }, [
h(
ElButton,
{
class: 'add-branch-btn-el',
size: 'small',
round: true,
onClick: () => addBranchNode(node),
},
() => `添加${isConditionNode(node) ? '条件' : '分支'}`,
),
]),
)
const branchDom = [h('div', { class: 'branch-node' }, branchItems)]
const afterChildDoms = buildDomTree(node.children)
return [h('div', {}, [branchDom, afterChildDoms])]
}
// 3. 空节点
if (isEmptyNode(node)) {
const childDoms = buildDomTree(node.children)
decodeAppendDom(node, childDoms)
return [h('div', { class: 'empty-node' }, childDoms)]
}
// 4. 末端节点
return []
}
/**
* 复制分支节点
*/
const copyBranch = (node: any) => {
const parentNode = nodeMap.value.get(node.parentId)
if (!parentNode) return
const newBranchNode = cloneDeep(node)
newBranchNode.name = `${newBranchNode.name}-copy`
forEachNode(parentNode, newBranchNode, (parent: any, currentNode: any) => {
currentNode.id = getRandomId()
currentNode.parentId = parent.id
})
const insertIndex = parentNode.branchs.indexOf(node)
parentNode.branchs.splice(insertIndex, 0, newBranchNode)
nextTick(() => {
renderProcessTree()
})
}
/**
* 分支节点左右移动
*/
const branchMove = (node: any, offset: number) => {
const parentNode = nodeMap.value.get(node.parentId)
if (!parentNode) return
const currentIndex = parentNode.branchs.indexOf(node)
const targetIndex = currentIndex + offset
if (targetIndex < 0 || targetIndex >= parentNode.branchs.length) {
ElMessage.info('已达边界,无法继续移动')
return
}
[parentNode.branchs[currentIndex], parentNode.branchs[targetIndex]] = [
parentNode.branchs[targetIndex],
parentNode.branchs[currentIndex],
]
nextTick(() => {
renderProcessTree()
})
}
/**
* 选中节点
*/
const selectNode = (node: any) => {
useWorkflowStoreHook().selectedNode = node
emit('selectedNode', node)
}
/**
* 插入新节点
*/
const insertNode = (type: string, parentNode: any) => {
if (_rootContainer.value) {
_rootContainer.value.click()
}
const afterNode = parentNode.children
parentNode.children = {
id: getRandomId(),
parentId: parentNode.id,
props: {},
type: type,
}
switch (type) {
case 'APPROVAL':
case 'SUBPROCESS':
insertApprovalNode(parentNode)
break
case 'CC':
insertCcNode(parentNode)
break
case 'CONCURRENT':
insertConcurrentNode(parentNode)
break
case 'CONDITION':
insertConditionNode(parentNode)
break
case 'DELAY':
insertDelayNode(parentNode)
break
case 'INCLUSIVE':
insertInclusiveNode(parentNode)
break
case 'TASK':
insertTaskNode(parentNode)
break
case 'TRIGGER':
insertTriggerNode(parentNode)
break
default:
break
}
if (isBranchNode({ type: type })) {
if (afterNode && afterNode.id) {
afterNode.parentId = parentNode.children.children.id
}
parentNode.children.children.children = afterNode
} else {
if (afterNode && afterNode.id) {
afterNode.parentId = parentNode.children.id
}
parentNode.children.children = afterNode
}
nextTick(() => {
renderProcessTree()
})
}
/**
* 节点插入的具体实现
*/
const insertApprovalNode = (parentNode: any) => {
parentNode.children.name = parentNode.children.type === 'APPROVAL' ? '审批人' : '子流程'
parentNode.children.props = cloneDeep(DefaultProps.APPROVAL_PROPS)
}
const insertCcNode = (parentNode: any) => {
parentNode.children.name = '抄送人'
parentNode.children.props = cloneDeep(DefaultProps.CC_PROPS)
}
const insertConcurrentNode = (parentNode: any) => {
parentNode.children.name = '并行分支'
parentNode.children.children = {
id: getRandomId(),
parentId: parentNode.children.id,
type: 'EMPTY',
}
parentNode.children.branchs = [
{
id: getRandomId(),
name: '分支1',
parentId: parentNode.children.id,
type: 'CONCURRENT',
props: {},
children: {},
},
{
id: getRandomId(),
name: '分支2',
parentId: parentNode.children.id,
type: 'CONCURRENT',
props: {},
children: {},
},
]
}
const insertConditionNode = (parentNode: any) => {
parentNode.children.name = '条件分支'
parentNode.children.children = {
id: getRandomId(),
parentId: parentNode.children.id,
type: 'EMPTY',
}
parentNode.children.branchs = [
{
id: getRandomId(),
parentId: parentNode.children.id,
type: 'CONDITION',
props: cloneDeep(DefaultProps.CONDITION_PROPS),
name: '条件1',
children: {},
},
{
id: getRandomId(),
parentId: parentNode.children.id,
type: 'CONDITION',
props: cloneDeep(DefaultProps.CONDITION_PROPS),
name: '条件2',
children: {},
},
]
}
const insertDelayNode = (parentNode: any) => {
parentNode.children.name = '延时处理'
parentNode.children.props = cloneDeep(DefaultProps.DELAY_PROPS)
}
const insertInclusiveNode = (parentNode: any) => {
parentNode.children.name = '包容分支'
parentNode.children.children = {
id: getRandomId(),
parentId: parentNode.children.id,
type: 'EMPTY',
}
parentNode.children.branchs = [
{
id: getRandomId(),
parentId: parentNode.children.id,
type: 'INCLUSIVE',
props: cloneDeep(DefaultProps.INCLUSIVE_PROPS),
name: '包容条件1',
children: {},
},
{
id: getRandomId(),
parentId: parentNode.children.id,
type: 'INCLUSIVE',
props: cloneDeep(DefaultProps.INCLUSIVE_PROPS),
name: '包容条件2',
children: {},
},
]
}
const insertTaskNode = (parentNode: any) => {
parentNode.children.name = '办理人'
parentNode.children.props = cloneDeep(DefaultProps.TASK_PROPS)
}
const insertTriggerNode = (parentNode: any) => {
parentNode.children.name = '触发器'
parentNode.children.props = cloneDeep(DefaultProps.TRIGGER_PROPS)
}
/**
* 获取分支末端节点
*/
const getBranchEndNode = (conditionNode: any): any => {
if (!conditionNode.children || !conditionNode.children.id) {
return conditionNode
}
return getBranchEndNode(conditionNode.children)
}
/**
* 添加分支节点
*/
const addBranchNode = (node: any) => {
if (node.branchs.length >= 8) {
ElMessage.warning('最多只能添加 8 项😥')
return
}
const newBranch = {
id: getRandomId(),
parentId: node.id,
name: `${isConditionNode(node) ? '条件' : isInclusiveNode(node) ? '包容条件' : '分支'}${node.branchs.length + 1}`,
props: isConditionNode(node)
? cloneDeep(DefaultProps.CONDITION_PROPS)
: isInclusiveNode(node)
? cloneDeep(DefaultProps.INCLUSIVE_PROPS)
: {},
type: isConditionNode(node) ? 'CONDITION' : isInclusiveNode(node) ? 'INCLUSIVE' : 'CONCURRENT',
children: {},
}
node.branchs.push(newBranch)
nextTick(() => {
renderProcessTree()
})
}
/**
* 删除节点
*/
const delNode = (node: any) => {
console.log('删除节点', node)
const parentNode = nodeMap.value.get(node.parentId)
if (!parentNode) {
ElMessage.warning('出现错误,找不到上级节点😥')
return
}
if (isBranchNode(parentNode)) {
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
if (parentNode.branchs.length < 2) {
const grandParentNode = nodeMap.value.get(parentNode.parentId)
const remainingBranch = parentNode.branchs[0]
if (remainingBranch?.children?.id) {
grandParentNode.children = remainingBranch.children
grandParentNode.children.parentId = grandParentNode.id
const endNode = getBranchEndNode(remainingBranch)
endNode.children = parentNode.children.children
if (endNode.children && endNode.children.id) {
endNode.children.parentId = endNode.id
}
} else {
grandParentNode.children = parentNode.children.children
if (grandParentNode.children && grandParentNode.children.id) {
grandParentNode.children.parentId = grandParentNode.id
}
}
}
} else {
if (node.children && node.children.id) {
node.children.parentId = parentNode.id
}
parentNode.children = node.children
}
nextTick(() => {
renderProcessTree()
})
}
/**
* 流程校验入口
*/
const validateProcess = () => {
valid.value = true
const err: any[] = []
validate(err, dom.value)
return err
}
/**
* 递归校验所有节点
*/
const validate = (err: any[], node: any) => {
if (isPrimaryNode(node)) {
validateNode(err, node)
validate(err, node.children)
} else if (isBranchNode(node)) {
node.branchs.forEach((branchNode: any) => {
validateNode(err, branchNode)
validate(err, branchNode.children)
})
validate(err, node.children)
} else if (isEmptyNode(node)) {
validate(err, node.children)
}
}
/**
* 单个节点校验
*/
const validateNode = (err: any[], node: any) => {
const nodeComp = nodeRefs.value[node.id]
if (nodeComp && typeof nodeComp.validate === 'function') {
valid.value = nodeComp.validate(err)
}
}
/**
* 更新指定节点 DOM
*/
const nodeDomUpdate = (node: any) => {
const nodeComp = nodeRefs.value[node.id]
if (nodeComp && typeof nodeComp.$forceUpdate === 'function') {
nodeComp.$forceUpdate()
}
}
/**
* 遍历节点树
*/
const forEachNode = (parent: any, node: any, callback: (parent: any, node: any) => void) => {
if (isBranchNode(node)) {
callback(parent, node)
forEachNode(node, node.children, callback)
node.branchs.forEach((branchNode: any) => {
callback(node, branchNode)
forEachNode(branchNode, branchNode.children, callback)
})
} else if (isPrimaryNode(node) || isEmptyNode(node) || isBranchSubNode(node)) {
callback(parent, node)
forEachNode(node, node.children, callback)
}
}
/**
* 渲染流程树入口
*/
const renderProcessTree = () => {
if (!_rootContainer.value) return
console.log('渲染流程树')
nodeMap.value.clear()
// 确保有初始节点数据
if (!dom.value || Object.keys(dom.value).length === 0) {
console.warn('没有流程数据可渲染')
// 初始化一个默认的根节点
useWorkflowStoreHook().design.process = {
id: getRandomId(),
type: 'ROOT',
name: '开始节点',
children: {},
}
return
}
const processTrees = buildDomTree(dom.value)
// 添加流程结束标记
processTrees.push(
h('div', { style: { 'text-align': 'center' } }, [
h('div', {
class: 'process-end',
innerHTML: '流程结束',
}),
]),
)
const rootDom = h('div', { class: '_root', ref: '_root' }, processTrees)
// 清空容器并渲染新内容
_rootContainer.value.innerHTML = ''
render(rootDom, _rootContainer.value)
}
// 监听流程数据变化
watch(
dom,
(newVal) => {
if (newVal && Object.keys(newVal).length > 0) {
nextTick(() => {
renderProcessTree()
})
}
},
{ deep: true },
)
// 组件挂载时初始化渲染
onMounted(() => {
// 确保容器已挂载
nextTick(() => {
if (dom.value && Object.keys(dom.value).length > 0) {
renderProcessTree()
} else {
// 如果没有初始数据,创建一个默认的开始节点
useWorkflowStoreHook().design.process = {
id: getRandomId(),
type: 'ROOT',
name: '开始节点',
children: {},
}
}
})
})
// 暴露组件方法
defineExpose({
validateProcess,
nodeDomUpdate,
forEachNode,
renderProcessTree,
})
</script>
<template>
<!-- Vue3 中渲染函数生成的内容挂载到该容器 -->
<div ref="_rootContainer" class="process-tree-container"></div>
</template>
<style lang="less" scoped>
.process-tree-container {
min-height: 400px;
position: relative;
}
.loading-placeholder {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: #999;
}
/* 样式完全保留原逻辑,无需修改 */
._root {
margin: 0 auto;
}
.process-end {
width: 60px;
margin: 0 auto;
margin-bottom: 20px;
border-radius: 15px;
padding: 5px 10px;
font-size: small;
color: #747474;
background-color: #f2f2f2;
box-shadow: 0 0 10px 0 #bcbcbc;
}
.primary-node {
display: flex;
align-items: center;
flex-direction: column;
}
.branch-node {
display: flex;
justify-content: center;
}
.branch-node-item {
position: relative;
display: flex;
background: #f5f6f6;
flex-direction: column;
align-items: center;
border-top: 2px solid #cccccc;
border-bottom: 2px solid #cccccc;
&:before {
content: '';
position: absolute;
top: 0;
left: calc(50% - 1px);
margin: auto;
width: 2px;
height: 100%;
background-color: #cacaca;
}
.line-top-left,
.line-top-right,
.line-bot-left,
.line-bot-right {
position: absolute;
width: 50%;
height: 4px;
background-color: #f5f6f6;
}
.line-top-left {
top: -2px;
left: -1px;
}
.line-top-right {
top: -2px;
right: -1px;
}
.line-bot-left {
bottom: -2px;
left: -1px;
}
.line-bot-right {
bottom: -2px;
right: -1px;
}
}
.add-branch-btn {
position: absolute;
width: 80px;
.add-branch-btn-el {
z-index: 999;
position: absolute;
top: -15px;
}
}
.empty-node {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
</style>
最新发布