Files
mosr-web/src/views/workflow/process/ProcessTree.vue

765 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import RootNode from './nodes/RootNode.vue'
import END from './nodes/ProcessEndNode.vue'
import APPROVAL from './nodes/ApprovalNode.vue'
import CcNode from './nodes/CcNode.vue'
import ConcurrentNode from './nodes/ConcurrentNode.vue'
import ConditionNode from './nodes/ConditionNode.vue'
import EmptyNode from './nodes/EmptyNode.vue'
import TriggerNode from './nodes/TriggerNode.vue'
import MergeNode from './nodes/MergeNode.vue'
import DelayNode from './nodes/DelayNode.vue'
import AddBranchNode from './nodes/AddBranchNode.vue'
import {debounce} from 'lodash'
import {defineExpose, h, render, ref} from 'vue'
import DefaultProps from "./DefaultNodeProps"
import {ElMessage, ElMessageBox} from 'element-plus'
import {useProcessStore} from '@/stores/processStore.js'
const processStore = useProcessStore()
const emit = defineEmits()
const props = defineProps({
mode: {
type: String,
default: 'design'
},
idName: {
type: String,
default: 'processTree'
}
})
const valid = ref(true)
let vNode = {}
const init = () => {
// console.log("sdsdsdsdsdsdsd",processStore.getProcess())
processStore.init()
initMapping(processStore.getProcess())
// 定义类名(可忽略)
let processTrees = getDomTree(h, "admin")
vNode = []
const dom = document.getElementById(props.idName);
vNode = h('div', {class: {'_root': true}}, processTrees);
render(vNode, dom)
}
// 初始化map集合,以便数据整理
const initMapping = (node) => {
node?.forEach(nodeItem => {
processStore.nodeMap.set(nodeItem.id, nodeItem)
processStore.parentMap.set(nodeItem.parentId, nodeItem)
})
}
const initHeaderBgc = (node) => {
if (node.props && props.mode === 'preview') {
let headerBgc = '#ff943e'
if (processStore.runningList.value?.includes(node.id)) {
headerBgc = '#1e90ff'
} else if (processStore.endList.value?.includes(node.id)) {
headerBgc = '#20b2aa'
} else if (processStore.noTakeList.value?.includes(node.id)) {
headerBgc = '#909399'
} else if (processStore.refuseList.value?.includes(node.id)) {
headerBgc = '#f56c6c'
} else if (processStore.passList.value?.includes(node.id)) {
headerBgc = '#ff943e'
}
node.props.headerBgc = headerBgc
}
}
// 获取demo的树形结构
const getDomTree = (h, id) => {
let node = processStore.parentMap.get(id)
if (!(node && node.id)) {
return []
}
initHeaderBgc(node)
if (isPrimaryNode(node)) {
//普通业务节点
let childDoms = getDomTree(h, node.id)
decodeAppendDom(h, node, childDoms)
return [h('div', {'class': {'primary-node': true}}, childDoms)];
} else if (isBranchNode(node)) {
let index = 0;
//遍历分支节点,包含并行及条件节点
let branchItems = node.branchs.map(branchNode => {
//处理每个分支内子节点
toMapping(branchNode)
let childDoms = getDomTree(h, branchNode.id)
decodeAppendDom(h, branchNode, childDoms, {level: index + 1, size: node.branchs.length})
//插入4条横线遮挡掉条件节点左右半边线条
insertCoverLine(h, index, childDoms, node.branchs)
//遍历子分支尾部分支
index++;
return h('div', {'class': {'branch-node-item': true}}, childDoms);
})
//插入添加分支/条件的按钮
branchItems.unshift(h('div', {'class': {'add-branch-btn': true}}, [
h(AddBranchNode, {
mode: props.mode,
size: 'small', round: true,
value: `添加${isConditionNodes(node) ? '条件' : '分支'}`,
onAddBranch: () => addBranchNode(node),
}, [])
]));
let bchDom = [h('div', {'class': {'branch-node': true}}, branchItems)]
//继续遍历分支后的节点
let afterChildDoms = getDomTree(h, node.id)
return [h('div', {}, [bchDom, afterChildDoms])]
} else if (isMergeNode(node)) {
//空节点,存在于分支尾部
let childDoms = getDomTree(h, node.id)
decodeAppendDom(h, node, childDoms)
return [h('div', {'class': {'empty-node': true}}, childDoms)];
} else if (isEmptyNode(node)) {
//空节点,存在于分支尾部
let childDoms = getDomTree(h, node.id)
decodeAppendDom(h, node, childDoms)
return [h('div', {'class': {'empty-node': true}}, childDoms)];
} else if (isEndNode(node)) {
let childDoms = getDomTree(h, node.id)
decodeAppendEndDom(h, node, childDoms)
return [h('div', {class: 'process-end'}, childDoms)];
}
return []
}
const decodeAppendEndDom = (h, node, dom, props = {}) => {
props.config = node
dom.unshift(h(END, {
id: node.id,
key: node.id,
}, []))
}
//解码渲染的时候插入dom到同级
const decodeAppendDom = (h, node, dom, nodeProps = {}) => {
nodeProps.config = node
dom.unshift(h(getNodeType(node), {
mode: props.mode,
...nodeProps,
id: node.id,
key: node.id,
//定义事件,插入节点,删除节点,选中节点,复制/移动
'onInsertNode': type => insertNode(type, node),
'onDelNode': () => delNode(node),
'onSelected': () => selectNode(node),
'onCopy': () => copyBranch(node),
'onLeftMove': () => branchMove(node, -1),
'onRightMove': () => branchMove(node, 1)
}, []))
}
const getNodeType = (node) => {
switch (node.type) {
case "ROOT":
return RootNode;
case "APPROVAL":
return APPROVAL;
case "CC":
return CcNode;
case "CONDITION":
return ConditionNode;
case "CONCURRENT":
return ConcurrentNode;
case "DELAY":
return DelayNode;
case "MERGE":
return MergeNode;
case "EMPTY":
return EmptyNode;
case "TRIGGER":
return TriggerNode;
}
}
//id映射到map用来向上遍历
const toMapping = (node) => {
if (node && node.id) {
let newNode = {
...node
}
newNode.children = []
processStore.nodeMap.set(newNode.id, newNode)
}
}
// 新增线条
const insertCoverLine = (h, index, doms, branchs) => {
if (index === 0) {
//最左侧分支
doms.unshift(h('div', {'class': {'line-top-left': true}}, []))
doms.unshift(h('div', {'class': {'line-bot-left': true}}, []))
}
if (index === branchs.length - 1) {
//最右侧分支
doms.unshift(h('div', {'class': {'line-top-right': true}}, []))
doms.unshift(h('div', {'class': {'line-bot-right': true}}, []))
}
}
const copyBranch = (node) => {
let parentNode = processStore.nodeMap.get(node.parentId)
let branchNode = deepCopy(node)
branchNode.name = branchNode.name + '-copy'
forEachNode(parentNode, branchNode, (parent, node) => {
node.id = getRandomId()
node.parentId = parent.id
})
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 0, branchNode)
init()
}
//移动分支节点
const branchMove = (node, offset) => {
let parentNode = processStore.nodeMap.get(node.parentId)
let index = parentNode.branchs.indexOf(node)
let branch = parentNode.branchs[index + offset]
parentNode.branchs[index + offset] = parentNode.branchs[index]
parentNode.branchs[index] = branch
init()
}
//判断是否为主要业务节点
const isPrimaryNode = (node) => {
return node &&
(node.type === 'ROOT' || node.type === 'APPROVAL'
|| node.type === 'CC' || node.type === 'DELAY'
|| node.type === 'TRIGGER');
}
//是否为分支节点
const isBranchNode = (node) => {
return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS');
}
//是否为空节点
const isEmptyNode = (node) => {
return node && (node.type === 'EMPTY')
}
const isEndNode = (node) => {
return node && (node.type === 'END')
}
//是否为空节点
const isMergeNode = (node) => {
return node && (node.type === 'MERGE')
}
//是分支节点
const isConditionNodes = (node) => {
return node.type === 'CONDITIONS';
}
const isConditionNode = (node) => {
return node.type === 'CONDITION';
}
//是分支节点
const isBranchSubNode = (node) => {
return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT');
}
//时候并行节点
const isConcurrentNodes = (node) => {
return node.type === 'CONCURRENTS'
}
//时候并行节点
const isConcurrentNode = (node) => {
return node.type === 'CONCURRENT'
}
//新增一个节点id
const getRandomId = () => {
let d = new Date().getTime()
// x 是 0-9 或 a-f 范围内的一个32位十六进制数
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
return 'node_' + id
}
//选中一个节点
const selectNode = (node) => {
processStore.setSelectedNode(node)
if (!isConcurrentNode(node)) {
emit('selectedNode', node)
}
}
//处理节点插入逻辑
const insertNode = debounce((type, parentNode) => {
//插入新节点
let id = getRandomId();
updateParentId(id, parentNode.id)
let children = {
id: id,
parentId: parentNode.id,
type: type,
}
switch (type) {
case 'APPROVAL':
insertApprovalNode(children);
break;
case 'CC':
insertCcNode(children);
break;
case 'DELAY':
insertDelayNode(children);
break;
// case 'TRIGGER':
// insertTriggerNode(children);
// break;
case 'CONDITIONS':
insertConditionsNode(children);
break;
case 'CONCURRENTS':
insertConcurrentsNode(children);
break;
default:
break;
}
init()
}, 100)
/**
* 更新父id
* @param newId
* @param oldId
*/
const updateParentId = (newId, oldId) => {
processStore.getProcess().map(node => {
if (node.parentId === oldId) {
node.parentId = newId
}
})
}
/**
* 审批人
* @param parentNode
*/
const deepCopy = (obj) => {
return JSON.parse(JSON.stringify(obj))
}
const insertApprovalNode = (parentNode) => {
let node = {
...parentNode,
name: "审批人",
props: deepCopy(DefaultProps.APPROVAL_PROPS)
}
processStore.addProcess(node)
}
/**
* 抄送人
* @param node
*/
const insertCcNode = (node) => {
let newNode = {
...node,
name: "抄送人",
props: deepCopy(DefaultProps.CC_PROPS)
}
processStore.addProcess(newNode)
}
/**
* 延时处理
* @param node
*/
const insertDelayNode = (node) => {
let newNode = {
...node,
name: "延时处理",
props: deepCopy(DefaultProps.DELAY_PROPS)
}
processStore.addProcess(newNode)
}
/**
* 触发器
* @param node
*/
const insertTriggerNode = (node) => {
let newNode = {
...node,
name: "触发器",
props: deepCopy(DefaultProps.TRIGGER_PROPS)
}
processStore.addProcess(newNode)
}
/**
* 新增条件分支F
* @param node
*/
const insertConditionsNode = (node) => {
let newNode = {
...node,
name: "条件分支",
branchs: [
{
id: getRandomId(),
parentId: node.id,
type: "CONDITION",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "条件1",
children: {}
}, {
id: getRandomId(),
parentId: node.id,
type: "CONDITION",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "条件2",
children: {}
}
]
}
processStore.addProcess(newNode)
let emptyNode = {
id: getRandomId(),
parentId: node.id,
type: "EMPTY"
}
updateParentId(emptyNode.id, newNode.id)
processStore.addProcess(emptyNode)
}
/**
* 新增同步运行节点
* @param node
*/
const insertConcurrentsNode = (node) => {
let newNode = {
...node,
name: "并行分支",
branchs: [
{
id: getRandomId(),
parentId: node.id,
type: "CONCURRENT",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "分支1",
children: {}
}, {
id: getRandomId(),
parentId: node.id,
type: "CONCURRENT",
props: deepCopy(DefaultProps.CONDITION_PROPS),
name: "分支2",
children: {}
}
]
}
processStore.addProcess(newNode)
let emptyNode = {
id: getRandomId(),
parentId: node.id,
type: "MERGE"
}
updateParentId(emptyNode.id, newNode.id)
processStore.addProcess(emptyNode)
}
const addBranchNode = (node) => {
if (node.branchs.length < 8) {
node.branchs.push({
id: getRandomId(),
parentId: node.id,
name: (isConditionNodes(node) ? '条件' : '分支') + (node.branchs.length + 1),
props: isConditionNodes(node) ? deepCopy(DefaultProps.CONDITION_PROPS) : {},
type: isConditionNodes(node) ? "CONDITION" : "CONCURRENT",
children: {}
})
init()
} else {
ElMessage.warning("最多只能添加 8 项😥")
}
}
//删除当前节点
const delNode = debounce((node) => {
//获取该节点的父节点
let parentNode = processStore.nodeMap.get(node.parentId)
if (parentNode) {
if (isBranchNode(parentNode)) {
delBranchNode(parentNode, node)
} else {
delNodeInDomChange(node.id, parentNode.id)
}
init()
} else {
ElMessage.warning("出现错误,找不到上级节点😥")
}
}, 100)
/**
* 从dom中删除
* @param delId
* @param parentId
*/
const delNodeInDomChange = (delId, parentId) => {
updateParentId(parentId, delId)
let delNode = processStore.nodeMap.get(delId)
processStore.delProcess(delNode)
init()
}
//删除分支
const delBranchNode = (parentNode, node) => {
let sunNode = processStore.parentMap.get(node.id)
//判断当前节点下有没有字节点,有则需要提示
if (sunNode) {
ElMessageBox.confirm('当前分支下有子节点,是否继续?', '提示', {
confirmButtonText: '确 定',
cancelButtonText: '取 消',
type: 'warning'
}).then(() => {
//确认后进行子节点的操作
delBranchSunNode(sunNode.id)
doDelBranchNode(parentNode, node)
})
} else {
// 没有直接开始删除
doDelBranchNode(parentNode, node)
}
}
//删除分支节点
const doDelBranchNode = (parentNode, node) => {
//判断当前分时是否为2
if (parentNode.branchs.length === 2) {
let nodeList = [...parentNode.branchs]
nodeList.splice(nodeList.indexOf(node), 1)
//查看另外一个分支上是否有节点
let sunNode = processStore.parentMap.get(nodeList[0].id)
//有则需要放到主分支上
if (sunNode) {
//更改分支上第一个节点的父id
updateParentId(parentNode.parentId, sunNode.parentId)
//找到最后一个节点
let lastNode = getLastBranchNode(sunNode.id)
let emptyNode = processStore.parentMap.get(parentNode.id)
//更新空节点下的第一个节点的id为当前分支最后一个节点
updateParentId(lastNode.id, emptyNode.id)
//删除分支的主节点
delNodeInDom(parentNode)
//删除分支的空节点
delNodeInDom(emptyNode)
} else {
//没有则直接删除
delEntireBranch(parentNode)
}
} else {
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
}
}
//获取最后一个节点
const getLastBranchNode = (id) => {
let node = processStore.parentMap.get(id)
if (node) {
return getLastBranchNode(node.id)
} else {
return processStore.nodeMap.get(id)
}
}
const delEntireBranch = (node) => {
//删除分支节点和空节点
let emptyNode = processStore.parentMap.get(node.id)
delNodeInDomChange(node.id, node.parentId)
delNodeInDomChange(emptyNode.id, emptyNode.parentId)
}
const delNodeInDom = (delNode) => {
processStore.delProcess(delNode)
init()
}
//删除分支的子节点
const delBranchSunNode = (id) => {
let node = processStore.parentMap.get(id)
delNodeInDomChange(id)
if (node) {
delBranchSunNode(node.id)
}
}
//给定一个起始节点,遍历内部所有节点
const forEachNode = (parent, node, callback) => {
if (isBranchNode(node)) {
callback(parent, node)
forEachNode(node, node.children, callback)
node.branchs.map(branchNode => {
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 validateProcess = () => {
valid.value = true
let err = []
validate(err, processStore.getProcess())
return err
}
const validateNode = (err, node, nodeMap) => {
let component = nodeMap.get(node.id)
if (component !== undefined) {
valid.value = component.exposed.validate(err)
}
}
const validateNodeList = ['APPROVAL', 'CC', 'CONDITION', 'DELAY', 'TRIGGER']
const analysisNode = (vNode, nodeMap) => {
let children = vNode.children
if (!children || children.length === 0) {
return
}
for (let child of children) {
let node = processStore.nodeMap.get(child.key)
if (node !== undefined && node && validateNodeList.includes(node.type)) {
nodeMap.set(node.id, child.component)
} else {
analysisNode(child, nodeMap)
}
}
}
//校验所有节点设置
const validate = (err, nodeList) => {
const nodeMap = new Map()
analysisNode(vNode, nodeMap)
nodeList.map(node => {
if (isPrimaryNode(node)) {
//校验条件节点
validateNode(err, node, nodeMap)
} else if (isBranchNode(node)) {
node.branchs.map(branchNode => {
//校验条件节点
validateNode(err, branchNode, nodeMap)
})
}
})
}
defineExpose({
init,
validateProcess
})
</script>
<style lang="scss">
._root {
margin: 0 auto;
}
.process-end {
width: 77px;
margin: 0 auto 20px auto;
border-radius: 15px;
text-align: center;
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;
position: relative;
//border-top: 2px solid #cccccc;
//border-bottom: 2px solid #cccccc;
}
.branch-node-item {
position: relative;
display: flex;
//background: #f5f6f6;
flex-direction: column;
align-items: center;
border-top: 2px solid #000000;
border-bottom: 2px solid #000000;
&:before {
content: "";
position: absolute;
top: 0;
left: calc(50% - 1px);
margin: auto;
width: 2px;
height: 100%;
background-color: #000000;
}
.line-top-left, .line-top-right, .line-bot-left, .line-bot-right {
position: absolute;
width: 50%;
height: 4px;
background-color: #fff;
}
.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;
.el-button {
border-color: #000000;
}
.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>