663 lines
17 KiB
Vue
663 lines
17 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
|
||
:key="node.id"
|
||
: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,
|
||
onMounted,
|
||
getCurrentInstance,
|
||
watch,
|
||
ref,
|
||
} from "vue";
|
||
import { $deepCopy, getRes } 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: getRes(loginUser.avatar),
|
||
sn: loginUser.sn,
|
||
};
|
||
|
||
const orgPicker = ref();
|
||
const data = reactive({
|
||
selectUserNodes: new Set(),
|
||
loading: true,
|
||
selectedNode: {},
|
||
reverse: false,
|
||
userCatch: {},
|
||
oldFormData: {},
|
||
models: null,
|
||
processTasks: [],
|
||
conditionFormItem: new Set(),
|
||
branchNodeMap: new Map(),
|
||
loadingReqs: [],
|
||
calls: [],
|
||
});
|
||
|
||
onBeforeMount(() => processRenderDebounce());
|
||
|
||
//流程渲染去抖动
|
||
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
|
||
await loadProcess(props.process, data.processTasks);
|
||
if (data.loadingReqs.length > 0) {
|
||
Promise.all(data.loadingReqs)
|
||
.then(() => {
|
||
setTimeout(() => {
|
||
data.processTasks.push({
|
||
title: "结束",
|
||
name: "END",
|
||
icon: "checkbox-filled",
|
||
enableEdit: false,
|
||
});
|
||
data.loading = false;
|
||
console.log("渲染完成11233");
|
||
}, 3500);
|
||
emits("render-ok");
|
||
})
|
||
.catch(() => (data.loading = false));
|
||
} else {
|
||
emits("render-ok");
|
||
setTimeout(() => {
|
||
data.processTasks.push({
|
||
title: "结束",
|
||
name: "END",
|
||
icon: "checkbox-filled",
|
||
enableEdit: false,
|
||
});
|
||
data.loading = false;
|
||
console.log("渲染完成11233");
|
||
}, 3500);
|
||
}
|
||
// console.log('渲染完成', data.loading)
|
||
}
|
||
|
||
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;
|
||
const activeProcessTasksList = data.processTasks.filter(
|
||
(item) => item.active
|
||
);
|
||
const selectUserNodes = new Set(
|
||
Array.from(data.selectUserNodes).sort((a, b) => {
|
||
return (
|
||
Number(a.replace(/[^0-9]/g, "")) - Number(b.replace(/[^0-9]/g, ""))
|
||
);
|
||
})
|
||
);
|
||
console.log(isOk, "我的OK");
|
||
for (let nodeId of selectUserNodes) {
|
||
if ((_value.value[nodeId] || []).length === 0) {
|
||
const branchNodeMap = new Map(
|
||
Array.from(data.branchNodeMap)
|
||
.reverse()
|
||
.map((i) => {
|
||
return {
|
||
...i,
|
||
};
|
||
})
|
||
);
|
||
//遍历所有的分支,从底部向上搜索进行自动切换分支渲染路线
|
||
let brNode = branchNodeMap.get(nodeId);
|
||
const option = brNode.node.options.find((i) => i.id === brNode.id);
|
||
const find = activeProcessTasksList.map((i) => {
|
||
const find = i.options.find((e) => e.id === i.active);
|
||
return find ? find : {};
|
||
})[0];
|
||
while (brNode && brNode.id && !option.skip && !find.skip) {
|
||
brNode.node.active = brNode.id;
|
||
brNode = data.branchNodeMap.get(brNode.id);
|
||
//没设置审批人员
|
||
isOk = false;
|
||
break;
|
||
}
|
||
|
||
nextTick(() => {
|
||
if (instance.refs[nodeId]) {
|
||
instance.refs[nodeId][0].errorShark();
|
||
}
|
||
});
|
||
// break
|
||
}
|
||
}
|
||
console.log(isOk, "我的OK");
|
||
if (call) {
|
||
call(isOk);
|
||
}
|
||
}
|
||
|
||
watch(
|
||
props.formData,
|
||
async () => {
|
||
console.log("数据变化");
|
||
//监听表单数据变化,对流程重渲染
|
||
processRenderDebounce();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.w-process {
|
||
position: relative;
|
||
|
||
.w-process-line {
|
||
width: 4rpx;
|
||
top: 20px;
|
||
height: calc(100% - 80rpx);
|
||
background-color: #9e9e9e;
|
||
position: absolute;
|
||
left: 26rpx;
|
||
}
|
||
}
|
||
|
||
.w-process-render {
|
||
.w-node-render {
|
||
padding: 32rpx 0;
|
||
}
|
||
}
|
||
</style>
|