562 lines
14 KiB
Vue
562 lines
14 KiB
Vue
<template>
|
||
<view class="w-process" v-if="!data.loading && data.processTasks.length > 0">
|
||
<view class="w-process-line"></view>
|
||
<view class="w-process-render" v-for="node in data.processTasks" :key="node.id">
|
||
<process-node-render :ref="node.id" class="w-node-render" :task="node" @addOrg="addOrg" @delOrg="delOrg" />
|
||
</view>
|
||
<org-picker ref="orgPicker" :type="data.selectedNode.type || 'user'"
|
||
:multiple="data.selectedNode.multiple || false" :selected="data.selectedNode.users" @ok="doAddOrg" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
nextTick,
|
||
reactive,
|
||
computed,
|
||
onBeforeMount,
|
||
getCurrentInstance,
|
||
watch,
|
||
ref
|
||
} from 'vue';
|
||
import {
|
||
$deepCopy
|
||
} from '@/utils/tool.js'
|
||
import ProcessNodeRender from "./ProcessNodeRender.vue";
|
||
import {
|
||
forEachNode
|
||
} from '@/utils/ProcessUtil.js'
|
||
import processApi from '@/api/process'
|
||
import OrgPicker from "@/components/OrgPicker.vue";
|
||
import {
|
||
getGroupModels
|
||
} from "@/api/model";
|
||
import {
|
||
debounce
|
||
} from '@/utils/tool.js'
|
||
|
||
const instance = getCurrentInstance()
|
||
|
||
|
||
//小程序端不支持jsx,真是艹了,这块要重写😂
|
||
const props = defineProps({
|
||
processDefId: String,
|
||
process: {
|
||
type: Object,
|
||
default: () => {
|
||
return {}
|
||
}
|
||
},
|
||
formData: {
|
||
type: Object,
|
||
default: () => {
|
||
return {}
|
||
}
|
||
},
|
||
modelValue: {
|
||
type: Object,
|
||
default: () => {
|
||
return {}
|
||
}
|
||
},
|
||
deptId: {
|
||
type: String,
|
||
default: null
|
||
},
|
||
})
|
||
|
||
const _value = computed({
|
||
get() {
|
||
return props.modelValue
|
||
},
|
||
set(val) {
|
||
emits('update:modelValue', val)
|
||
}
|
||
})
|
||
|
||
defineExpose({
|
||
validate
|
||
})
|
||
const emits = defineEmits(['update:modelValue', 'render-ok'])
|
||
|
||
const loginUser = JSON.parse(uni.getStorageSync('loginUser'))
|
||
loginUser.value = {
|
||
id: loginUser.userId,
|
||
name: loginUser.realName,
|
||
avatar: loginUser.avatar,
|
||
sn: loginUser.sn
|
||
}
|
||
|
||
const orgPicker = ref()
|
||
const data = reactive({
|
||
selectUserNodes: new Set(),
|
||
loading: false,
|
||
selectedNode: {},
|
||
reverse: false,
|
||
userCatch: {},
|
||
oldFormData: {},
|
||
models: null,
|
||
processTasks: [],
|
||
conditionFormItem: new Set(),
|
||
branchNodeMap: new Map(),
|
||
loadingReqs: [],
|
||
calls: []
|
||
})
|
||
|
||
onBeforeMount(() => loadProcessRender())
|
||
|
||
//流程渲染去抖动
|
||
const processRenderDebounce = debounce(() => loadProcessRender(), 500)
|
||
|
||
async function loadProcessRender() {
|
||
console.log('渲染流程')
|
||
data.loading = true
|
||
data.processTasks.length = 0
|
||
data.selectUserNodes.clear()
|
||
data.loadingReqs.length = 0
|
||
//TODO 从这里可以使用去抖动函数 this.$debounce
|
||
loadProcess(props.process, data.processTasks)
|
||
data.processTasks.push({
|
||
title: '结束',
|
||
name: 'END',
|
||
icon: 'checkbox-filled',
|
||
enableEdit: false
|
||
})
|
||
if (data.loadingReqs.length > 0) {
|
||
Promise.all(data.loadingReqs).then(() => {
|
||
data.loading = false
|
||
emits('render-ok')
|
||
}).catch(() => data.loading = false)
|
||
} else {
|
||
emits('render-ok')
|
||
data.loading = false
|
||
}
|
||
}
|
||
|
||
function loadProcess(processNode, processTasks, bnode, bid) {
|
||
forEachNode(processNode, node => {
|
||
if (bnode) { //如果是分支内子节点
|
||
data.branchNodeMap.set(node.id, {
|
||
node: bnode,
|
||
id: bid
|
||
})
|
||
}
|
||
switch (node.type) {
|
||
case 'ROOT':
|
||
processTasks.push({
|
||
id: node.id,
|
||
title: node.name,
|
||
name: '发起人',
|
||
desc: `${loginUser.value.name} 将发起本流程`,
|
||
icon: 'contact-filled',
|
||
enableEdit: false,
|
||
users: [loginUser.value]
|
||
});
|
||
break;
|
||
case 'APPROVAL':
|
||
processTasks.push(getApprovalNode(node))
|
||
break;
|
||
case 'TASK':
|
||
processTasks.push(getApprovalNode(node, false))
|
||
break;
|
||
case 'SUBPROC':
|
||
processTasks.push(getSubProcNode(node))
|
||
break;
|
||
case 'CC':
|
||
processTasks.push(getCcNode(node))
|
||
break;
|
||
case 'CONDITIONS': //条件节点选一项
|
||
processTasks.push(getConditionNode(node, bnode, bid))
|
||
loadProcess(node.children, processTasks)
|
||
return true
|
||
case 'INCLUSIVES': //包容分支会执行所有符合条件的分支
|
||
processTasks.push(getInclusiveNode(node, bnode, bid))
|
||
loadProcess(node.children, processTasks)
|
||
return true
|
||
case 'CONCURRENTS': //并行分支无条件执行所有分支
|
||
processTasks.push(getConcurrentNode(node, bnode, bid))
|
||
loadProcess(node.children, processTasks)
|
||
return true
|
||
}
|
||
})
|
||
}
|
||
|
||
function getSubProcNode(node) {
|
||
let user = {}
|
||
//提取发起人
|
||
switch (node.props.staterUser.type) {
|
||
case "ROOT":
|
||
user = loginUser.value;
|
||
break;
|
||
case "FORM":
|
||
const fd = props.formData[node.props.staterUser.value]
|
||
user = Array.isArray(fd) && fd.length > 0 ? fd[0] : {
|
||
name: '请选人'
|
||
};
|
||
break;
|
||
case "SELECT":
|
||
user = node.props.staterUser.value || {};
|
||
break;
|
||
}
|
||
const procNode = {
|
||
id: node.id,
|
||
title: `${node.name} [由${user.id ? user.name : '?'}发起]`,
|
||
name: '子流程',
|
||
desc: '',
|
||
icon: 'more-filled',
|
||
enableEdit: false,
|
||
users: [user]
|
||
}
|
||
getSubModel(() => {
|
||
procNode.desc = `调用子流程 [${data.models[node.props.subProcCode]}]`
|
||
})
|
||
return procNode
|
||
}
|
||
|
||
function getApprovalNode(node, isApproval = true) {
|
||
let result = {
|
||
id: node.id,
|
||
title: node.name,
|
||
name: isApproval ? '审批人' : '办理人',
|
||
icon: isApproval ? 'person-filled' : 'calendar-filled',
|
||
enableEdit: false,
|
||
multiple: false,
|
||
mode: node.props.mode,
|
||
users: [],
|
||
desc: ''
|
||
}
|
||
let loadCatch = true
|
||
switch (node.props.assignedType) {
|
||
case 'ASSIGN_USER':
|
||
result.users = $deepCopy(node.props.assignedUser)
|
||
result.desc = isApproval ? '指定审批人' : '指定办理人'
|
||
break
|
||
case 'ASSIGN_LEADER':
|
||
data.loadingReqs.push(
|
||
processApi.getLeaderByDepts((node.props.assignedDept || []).map(d => d.id)).then(res => {
|
||
result.users = res.data
|
||
}))
|
||
result.desc = '指定部门的领导'
|
||
break
|
||
case 'SELF':
|
||
result.users = [loginUser.value]
|
||
result.desc = `发起人自己${isApproval?'审批':'办理'}`
|
||
break
|
||
case 'SELF_SELECT':
|
||
result.enableEdit = true
|
||
data.selectUserNodes.add(node.id)
|
||
result.multiple = node.props.selfSelect.multiple || false
|
||
result.desc = isApproval ? '自选审批人' : '自选办理人'
|
||
break
|
||
case 'LEADER_TOP':
|
||
result.desc = `连续多级主管${isApproval?'审批':'办理'}`
|
||
const leaderTop = node.props.leaderTop
|
||
data.loadingReqs.push(
|
||
processApi.getUserLeaders(
|
||
'TOP' === leaderTop.endCondition ? 0 : leaderTop.endLevel,
|
||
props.deptId, leaderTop.skipEmpty).then(res => {
|
||
result.users = res.data
|
||
}))
|
||
break
|
||
case 'LEADER':
|
||
result.desc = node.props.leader.level === 1 ?
|
||
`直接主管${isApproval?'审批':'办理'}` :
|
||
`第${node.props.leader.level}级主管${isApproval?'审批':'办理'}`
|
||
data.loadingReqs.push(
|
||
processApi.getUserLeader(
|
||
node.props.leader.level,
|
||
props.deptId,
|
||
node.props.leader.skipEmpty).then(res => {
|
||
result.users = res.data ? [res.data] : []
|
||
}))
|
||
break
|
||
case 'ROLE':
|
||
result.desc = `由角色[${(node.props.role || []).map(r => r.name)}]${isApproval?'审批':'办理'}`
|
||
data.loadingReqs.push(
|
||
processApi.getUsersByRoles({
|
||
projectSn: loginUser.value.sn,
|
||
roleIds: (node.props.role || []).map(r => r.id)
|
||
}).then(res => {
|
||
result.users = res.data
|
||
}))
|
||
break
|
||
case 'FORM_USER':
|
||
loadCatch = false
|
||
result.desc = `由表单字段内人员${isApproval?'审批':'办理'}`
|
||
data.conditionFormItem.add(node.props.formUser)
|
||
result.users = props.formData[node.props.formUser] || []
|
||
break
|
||
case 'FORM_DEPT':
|
||
loadCatch = false
|
||
result.desc = `由表单部门内主管${isApproval?'审批':'办理'}`
|
||
data.conditionFormItem.add(node.props.formDept)
|
||
data.loadingReqs.push(
|
||
processApi.getLeaderByDepts((props.formData[node.props.formDept] || []).map(d => d.id)).then(
|
||
res => {
|
||
result.users = res.data
|
||
}))
|
||
break
|
||
case 'REFUSE':
|
||
result.desc = `流程此处将被自动驳回`
|
||
break
|
||
}
|
||
if (data.userCatch[node.id] && data.userCatch[node.id].length > 0) {
|
||
result.users = data.userCatch[node.id]
|
||
}
|
||
if (loadCatch) {
|
||
data.userCatch[node.id] = result.users
|
||
}
|
||
return result
|
||
}
|
||
|
||
function getCcNode(node) {
|
||
let result = {
|
||
id: node.id,
|
||
title: node.name,
|
||
icon: 'paperplane-filled',
|
||
name: '抄送人',
|
||
enableEdit: node.props.shouldAdd,
|
||
type: 'org',
|
||
multiple: true,
|
||
desc: node.props.shouldAdd ? '可添加抄送人' : '流程将会抄送到他们',
|
||
users: $deepCopy(node.props.assignedUser)
|
||
}
|
||
if (data.userCatch[node.id] && data.userCatch[node.id].length > 0) {
|
||
result.users = data.userCatch[node.id]
|
||
}
|
||
data.userCatch[node.id] = result.users
|
||
return result
|
||
}
|
||
|
||
function getInclusiveNode(node, pbnode, pbid) {
|
||
let branchTasks = {
|
||
id: node.id,
|
||
title: node.name,
|
||
name: '包容分支',
|
||
icon: 'tune-filled',
|
||
enableEdit: false,
|
||
active: node.branchs[0].id, //激活得分支
|
||
options: [], //分支选项,渲染单选框
|
||
desc: '满足条件的分支均会执行',
|
||
branchs: {} //分支数据,不包含分支节点,key=分支子节点id,value = [后续节点]
|
||
}
|
||
const req = processApi.getTrueConditions({
|
||
processDfId: props.processDefId,
|
||
conditionNodeId: node.id,
|
||
multiple: true,
|
||
context: {
|
||
...props.formData,
|
||
deptId: props.deptId
|
||
}
|
||
}).then(rsp => {
|
||
//拿到满足的条件
|
||
const cds = new Set(rsp.data || [])
|
||
for (let i = 0; i < node.branchs.length; i++) {
|
||
const cdNode = node.branchs[i]
|
||
cdNode.skip = !cds.has(cdNode.id)
|
||
if (!cdNode.skip) {
|
||
branchTasks.active = cdNode.id
|
||
}
|
||
}
|
||
node.branchs.forEach(nd => {
|
||
branchTasks.options.push({
|
||
id: nd.id,
|
||
title: nd.name,
|
||
skip: nd.skip
|
||
})
|
||
branchTasks.branchs[nd.id] = []
|
||
//设置下子级分支的父级分支节点
|
||
data.branchNodeMap.set(nd.id, {
|
||
node: pbnode,
|
||
id: pbid
|
||
})
|
||
loadProcess(nd.children, branchTasks.branchs[nd.id], branchTasks, nd.id)
|
||
})
|
||
}).catch(err => {
|
||
branchTasks.desc = `<span style="color:#CE5266;">条件解析异常,渲染失败😢<span>`
|
||
//
|
||
})
|
||
data.loadingReqs.push(req)
|
||
return branchTasks
|
||
}
|
||
|
||
function getConditionNode(node, pbnode, pbid) {
|
||
let branchTasks = {
|
||
id: node.id,
|
||
title: node.name,
|
||
name: '条件分支',
|
||
icon: 'tune',
|
||
enableEdit: false,
|
||
active: node.branchs[0].id, //激活得分支
|
||
options: [], //分支选项,渲染单选框
|
||
desc: '只执行第一个满足条件的分支',
|
||
branchs: {} //分支数据,不包含分支节点,key=分支子节点id,value = [后续节点]
|
||
}
|
||
const req = processApi.getTrueConditions({
|
||
processDfId: props.processDefId,
|
||
conditionNodeId: node.id,
|
||
multiple: false,
|
||
context: {
|
||
...props.formData,
|
||
deptId: props.deptId
|
||
}
|
||
}).then(rsp => {
|
||
//拿到满足的条件
|
||
const cds = new Set(rsp.data || [])
|
||
for (let i = 0; i < node.branchs.length; i++) {
|
||
const cdNode = node.branchs[i]
|
||
cdNode.skip = !cds.has(cdNode.id)
|
||
if (!cdNode.skip) {
|
||
branchTasks.active = cdNode.id
|
||
}
|
||
}
|
||
node.branchs.forEach(nd => {
|
||
branchTasks.options.push({
|
||
id: nd.id,
|
||
title: nd.name,
|
||
skip: nd.skip
|
||
})
|
||
branchTasks.branchs[nd.id] = []
|
||
//设置下子级分支的父级分支节点
|
||
data.branchNodeMap.set(nd.id, {
|
||
node: pbnode,
|
||
id: pbid
|
||
})
|
||
loadProcess(nd.children, branchTasks.branchs[nd.id], branchTasks, nd.id)
|
||
})
|
||
}).catch(err => {
|
||
branchTasks.desc = `<span style="color:#CE5266;">条件解析异常,渲染失败😢<span>`
|
||
//this.$err(err, "解析条件失败:")
|
||
})
|
||
data.loadingReqs.push(req)
|
||
return branchTasks
|
||
}
|
||
|
||
function getConcurrentNode(node, pbnode, pbid) {
|
||
let concurrentTasks = {
|
||
id: node.id,
|
||
title: node.name,
|
||
name: '并行分支',
|
||
icon: 'settings-filled',
|
||
enableEdit: false,
|
||
active: node.branchs[0].id, //激活得分支
|
||
options: [], //分支选项,渲染单选框
|
||
desc: '所有分支都将同时执行',
|
||
branchs: {} //分支数据,不包含分支节点,key=分支子节点id,value = [后续节点]
|
||
}
|
||
node.branchs.forEach(nd => {
|
||
concurrentTasks.options.push({
|
||
id: nd.id,
|
||
title: nd.name,
|
||
skip: false
|
||
})
|
||
concurrentTasks.branchs[nd.id] = []
|
||
//设置下子级分支的父级分支节点
|
||
data.branchNodeMap.set(nd.id, {
|
||
node: pbnode,
|
||
id: pbid
|
||
})
|
||
loadProcess(nd.children, concurrentTasks.branchs[nd.id], concurrentTasks, nd.id)
|
||
})
|
||
return concurrentTasks
|
||
}
|
||
|
||
function selected(users) {
|
||
_value.value[data.selectedNode.id] = []
|
||
users.forEach(u => {
|
||
if (data.selectedNode.users.findIndex(v => v.id === u.id) === -1) {
|
||
u.enableEdit = true
|
||
data.selectedNode.users.push(u)
|
||
_value.value[data.selectedNode.id] = data.selectedNode.users
|
||
}
|
||
})
|
||
}
|
||
|
||
function getSubModel(call) {
|
||
if (data.models) {
|
||
call()
|
||
} else {
|
||
data.calls.push(call)
|
||
if (data.calls.length === 1) {
|
||
getGroupModels({}, true).then(rsp => {
|
||
data.models = {}
|
||
rsp.data.forEach(group => {
|
||
group.items.forEach(v => data.models[v.procCode] = v.procName)
|
||
})
|
||
data.calls.forEach(callFun => callFun())
|
||
data.calls.length = 0
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
function addOrg(node) {
|
||
console.log(node,"node");
|
||
data.selectedNode = node
|
||
orgPicker.value.show()
|
||
}
|
||
|
||
function doAddOrg(orgs) {
|
||
selected(orgs)
|
||
}
|
||
|
||
function delOrg(i, node) {
|
||
node.users.splice(i, 1)
|
||
}
|
||
|
||
//执行校验流程步骤设置
|
||
function validate(call) {
|
||
//遍历自选审批人节点
|
||
let isOk = true
|
||
console.log(isOk,'我的OK')
|
||
for (let nodeId of data.selectUserNodes) {
|
||
if ((_value.value[nodeId] || []).length === 0) {
|
||
//没设置审批人员
|
||
isOk = false
|
||
//遍历所有的分支,从底部向上搜索进行自动切换分支渲染路线
|
||
let brNode = data.branchNodeMap.get(nodeId)
|
||
while (brNode && brNode.id) {
|
||
brNode.node.active = brNode.id
|
||
brNode = data.branchNodeMap.get(brNode.id)
|
||
}
|
||
|
||
nextTick(() => {
|
||
if (instance.refs[nodeId]) {
|
||
instance.refs[nodeId][0].errorShark()
|
||
}
|
||
})
|
||
break
|
||
}
|
||
}
|
||
console.log(isOk,'我的OK')
|
||
if (call) {
|
||
call(isOk)
|
||
}
|
||
}
|
||
|
||
watch(props.formData, async () => {
|
||
//监听表单数据变化,对流程重渲染
|
||
processRenderDebounce()
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.w-process {
|
||
position: relative;
|
||
|
||
.w-process-line {
|
||
width: 2px;
|
||
top: 20px;
|
||
height: calc(100% - 40px);
|
||
background-color: #9e9e9e;
|
||
position: absolute;
|
||
left: 13px;
|
||
}
|
||
}
|
||
|
||
.w-process-render {
|
||
.w-node-render {
|
||
padding: 32rpx 0;
|
||
}
|
||
}
|
||
</style> |