2025-08-20 14:39:30 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal :mask-closable="false" :title="dialogTitle" :visible="isOpen" :width="500" centered class="geg" @cancel="onClickCancel">
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<a-button :disabled="loading" @click="onClickCancel">取消</a-button>
|
|
|
|
|
|
<a-button :loading="loading" type="primary" @click="onClickOK">确定</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="dialog-wrap">
|
|
|
|
|
|
<a-form :label-col="{ span: 6 }" :model="formState" autocomplete="off">
|
|
|
|
|
|
<a-form-item v-if="_action === 'agree'" label="下一节点" name="nextNodeName">
|
|
|
|
|
|
<span>{{ getNextNodesName() }}</span>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<template v-for="node in flowNextNodes" :key="node.activityId">
|
|
|
|
|
|
<div v-if="flowNextNodes.length > 1" class="node-info">
|
|
|
|
|
|
<span class="node-label">{{ node.nodeTypeLabel }}</span>
|
2025-10-21 18:04:02 +08:00
|
|
|
|
<span class="node-name">{{ node.activityName }}</span>
|
|
|
|
|
|
<a-switch :checked="node.chooseNode" v-if="!node.hiddenNode" style="margin-left: 10px" @change="agreeNodeChange(node)"></a-switch>
|
2025-08-20 14:39:30 +08:00
|
|
|
|
</div>
|
2025-10-21 18:04:02 +08:00
|
|
|
|
<a-form-item :required="flowNextNodes.length === 1 || node.chooseNode" v-if="(_action === 'agree' || _action == 'disagree') && !isEnd" :label="'审批人'">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-show="node.chooseAssign"
|
|
|
|
|
|
v-model:value="node.assignees"
|
|
|
|
|
|
:options="node.nextAssignees"
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
:placeholder="'审批人'"
|
|
|
|
|
|
max-tag-count="responsive"
|
|
|
|
|
|
:mode="node.isChooseMulti ? 'multiple' : ''"
|
|
|
|
|
|
:filterOption="search"
|
2025-08-20 14:39:30 +08:00
|
|
|
|
></a-select>
|
|
|
|
|
|
<span v-show="!node.chooseAssign">{{ getAssigneeText(node) }}</span>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form-item v-if="_action === 'reject'" label="退回至" name="rejectNode">
|
|
|
|
|
|
<a-select v-model:value="rejectNodeId" :disabled="loading">
|
|
|
|
|
|
<a-select-option v-for="(item, index) in rejectNodeList" :key="index" :value="item.activityId">{{ item.activityName }}</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</a-form-item>
|
2025-10-13 11:53:54 +08:00
|
|
|
|
<template v-for="node in rejectNodeList" :key="node.activityId">
|
2025-10-21 18:04:02 +08:00
|
|
|
|
<a-form-item required v-if="_action === 'reject' && rejectNodeId === node.activityId" label="审批人">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-show="node.chooseAssign"
|
|
|
|
|
|
v-model:value="node.assignees"
|
|
|
|
|
|
:options="node.nextAssignees"
|
|
|
|
|
|
:placeholder="'请选择' + node.activityName + '的审批人'"
|
|
|
|
|
|
max-tag-count="responsive"
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
:mode="node.rejectIsChooseMulti ? 'multiple' : ''"
|
|
|
|
|
|
:filterOption="search"
|
2025-08-20 14:39:30 +08:00
|
|
|
|
></a-select>
|
|
|
|
|
|
<span v-show="!node.chooseAssign">{{ getAssigneeText(node) }}</span>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form-item label="审批意见" name="opinion">
|
|
|
|
|
|
<a-textarea v-model:value="formState.opinion" :disabled="loading" :maxlength="200" :rows="3" placeholder="请输入审批意见,不超过200字" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { reactive, ref } from 'vue';
|
|
|
|
|
|
import { getRejectNodeList } from '/@/api/workflow/task';
|
|
|
|
|
|
import { message } from 'ant-design-vue';
|
|
|
|
|
|
|
|
|
|
|
|
const dialogTitle = ref('审批');
|
|
|
|
|
|
const isOpen = ref(false);
|
|
|
|
|
|
const rejectNodeList = ref([]);
|
|
|
|
|
|
const rejectNodeId = ref('');
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const isEnd = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
let _action = ref('agree');
|
|
|
|
|
|
let _processId = '';
|
|
|
|
|
|
let _taskId = '';
|
|
|
|
|
|
let flowNextNodes = ref([]);
|
|
|
|
|
|
let _callback = null;
|
|
|
|
|
|
let _onCancel = null;
|
|
|
|
|
|
|
|
|
|
|
|
const formState = reactive({
|
|
|
|
|
|
opinion: '',
|
|
|
|
|
|
opinionList: ['同意。', '请领导审批。']
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function getAssigneeText(node) {
|
|
|
|
|
|
// 注意这里用的是下拉框的数据结构 所以字段是value和label
|
|
|
|
|
|
return (node.nextAssignees || [])
|
|
|
|
|
|
.filter((item) => node.assignees.includes(item.value))
|
|
|
|
|
|
.map((item) => item.label)
|
|
|
|
|
|
.join('、');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getNextNodesName() {
|
2025-10-13 11:53:54 +08:00
|
|
|
|
return flowNextNodes.value.length > 1 ? '多个节点,请确认流向节点' : flowNextNodes?.value[0]?.activityName;
|
2025-08-20 14:39:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleDialog({ isClose, action, callback, rejectCancel, processId, taskId, nextNodes } = {}) {
|
|
|
|
|
|
if (isClose) {
|
|
|
|
|
|
isOpen.value = false;
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
isOpen.value = true;
|
|
|
|
|
|
_action.value = action;
|
|
|
|
|
|
_callback = callback;
|
|
|
|
|
|
_onCancel = rejectCancel;
|
|
|
|
|
|
_processId = processId;
|
|
|
|
|
|
_taskId = taskId;
|
|
|
|
|
|
flowNextNodes.value = nextNodes;
|
|
|
|
|
|
formState.opinion = '';
|
|
|
|
|
|
dialogTitle.value = `审批`;
|
|
|
|
|
|
if (nextNodes && nextNodes.length) {
|
|
|
|
|
|
// 下一个节点唯一时(可能有并行节点)
|
|
|
|
|
|
const nNode = nextNodes[0];
|
|
|
|
|
|
//formState.nextNodeName = nNode.activityName;
|
|
|
|
|
|
isEnd.value = nNode.isEnd;
|
2025-10-21 18:04:02 +08:00
|
|
|
|
const typeCount = nextNodes.length <= 1 ? nextNodes.length : new Set(nextNodes.map((nNode) => nNode.nodeType)).size;
|
2025-08-20 14:39:30 +08:00
|
|
|
|
nextNodes.forEach((nNode) => {
|
|
|
|
|
|
if (!nNode.userList?.length) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const selected = [];
|
|
|
|
|
|
nNode.nextAssignees = nNode.userList.map((item) => {
|
|
|
|
|
|
if (item.checked || nNode.userList.length === 1) {
|
|
|
|
|
|
// 只有一个人的时候必须选他
|
|
|
|
|
|
selected.push(item['F_UserId']);
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
value: item['F_UserId'],
|
|
|
|
|
|
label: item['F_RealName'],
|
2025-10-21 18:04:02 +08:00
|
|
|
|
item: item
|
2025-08-20 14:39:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
nNode.assignees = selected;
|
|
|
|
|
|
if (!nNode.chooseAssign) {
|
|
|
|
|
|
// 不需要选审批人的时候 所有备选人都要放到下个节点
|
|
|
|
|
|
nNode.assignees = nNode.userList.map((item) => item['F_UserId']);
|
|
|
|
|
|
}
|
|
|
|
|
|
nNode.chooseAssign = nNode.chooseAssign;
|
|
|
|
|
|
|
|
|
|
|
|
if (nNode.nodeType === 'commitToNode') {
|
|
|
|
|
|
nNode.nodeTypeLabel = '提交到退回节点:';
|
|
|
|
|
|
}
|
2025-10-21 18:04:02 +08:00
|
|
|
|
if (nextNodes.length > 1 && nNode.nodeType === 'common') {
|
2025-08-20 14:39:30 +08:00
|
|
|
|
nNode.nodeTypeLabel = '并行节点:';
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是必选并行节点 或者 只有一个节点,默认选中
|
|
|
|
|
|
if (nNode.isChooseParallel || nextNodes.length == 1) {
|
|
|
|
|
|
nNode.chooseNode = true;
|
|
|
|
|
|
if (typeCount == 1) {
|
|
|
|
|
|
// 如果只有一个节点类型的,默认隐藏
|
|
|
|
|
|
nNode.hiddenNode = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nNode.hiddenNode = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
flowNextNodes.value = nextNodes;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (action === 'reject') {
|
|
|
|
|
|
loadRejectNodeList();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function agreeNodeChange(node) {
|
|
|
|
|
|
if (node.chooseNode) {
|
|
|
|
|
|
// 如果是必选并行节点,取消选择时,警告并返回
|
|
|
|
|
|
if (node.isChooseParallel) {
|
|
|
|
|
|
return message.warning('并行节点必选,不能取消选择');
|
|
|
|
|
|
}
|
|
|
|
|
|
node.chooseNode = false;
|
|
|
|
|
|
// 遍历节点,如果不是common类型都是false了,需要遍历common类型中必填节点是否都是true
|
|
|
|
|
|
const sumNotCommonNode = flowNextNodes.value.filter((nNode) => nNode.nodeType != 'common' && nNode.chooseNode).length;
|
|
|
|
|
|
if (sumNotCommonNode === 0) {
|
|
|
|
|
|
// 如果没有非common类型的节点被选中,则将遍历common类型的节点,必填节点设置为true
|
|
|
|
|
|
flowNextNodes.value.forEach((nNode) => {
|
|
|
|
|
|
if (nNode.nodeType === 'common' && nNode.isChooseParallel) {
|
|
|
|
|
|
nNode.chooseNode = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (!node.chooseNode) {
|
|
|
|
|
|
node.chooseNode = true;
|
|
|
|
|
|
// 遍历节点,取消与自己不同类型的节点的选择
|
|
|
|
|
|
flowNextNodes.value.forEach((nNode) => {
|
|
|
|
|
|
if (nNode.nodeType != node.nodeType) {
|
|
|
|
|
|
nNode.chooseNode = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if (node.nodeType === 'common') {
|
|
|
|
|
|
// 如果是common类型的节点,遍历所有common类型的节点,必填节点设置为true
|
|
|
|
|
|
flowNextNodes.value.forEach((nNode) => {
|
|
|
|
|
|
if (nNode.nodeType === 'common' && nNode.isChooseParallel) {
|
|
|
|
|
|
nNode.chooseNode = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 18:04:02 +08:00
|
|
|
|
function search(inputValue, option) {
|
|
|
|
|
|
return inputValue ? option.item.F_Account.indexOf(inputValue) > -1 || option.item.F_RealName.indexOf(inputValue) > -1 : true;
|
2025-08-20 14:39:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadRejectNodeList() {
|
2025-10-21 18:04:02 +08:00
|
|
|
|
rejectNodeId.value = '';
|
2025-08-20 14:39:30 +08:00
|
|
|
|
let res = await getRejectNodeList(_processId, _taskId);
|
|
|
|
|
|
if (res && Array.isArray(res) && res.length > 0) {
|
|
|
|
|
|
rejectNodeList.value = res;
|
|
|
|
|
|
dialogTitle.value = `退回`;
|
|
|
|
|
|
if (res?.length) {
|
|
|
|
|
|
res.forEach((nNode) => {
|
|
|
|
|
|
if (!nNode.userList?.length) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const selected = [];
|
|
|
|
|
|
nNode.nextAssignees = nNode.userList.map((item) => {
|
|
|
|
|
|
if (item.checked || nNode.userList.length === 1) {
|
|
|
|
|
|
// 只有一个人的时候必须选他
|
|
|
|
|
|
selected.push(item['F_UserId']);
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
value: item['F_UserId'],
|
2025-10-21 18:04:02 +08:00
|
|
|
|
label: item['F_RealName'] + (item.remarks ? '(' + item.remarks + ')' : ''),
|
|
|
|
|
|
item: item
|
2025-08-20 14:39:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
nNode.assignees = selected;
|
|
|
|
|
|
if (!nNode.chooseAssign) {
|
|
|
|
|
|
// 不需要选审批人的时候 所有备选人都要放到下个节点
|
|
|
|
|
|
nNode.assignees = nNode.userList.map((item) => item['F_UserId']);
|
|
|
|
|
|
}
|
|
|
|
|
|
nNode.chooseAssign = nNode.chooseAssign;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onClickOK() {
|
|
|
|
|
|
const nextTaskUser = {};
|
|
|
|
|
|
if (_action.value === 'agree' && !isEnd.value) {
|
|
|
|
|
|
// 如果是同意,并且没有选择节点 报错
|
2025-10-21 18:04:02 +08:00
|
|
|
|
const choose = flowNextNodes.value.find((node) => node.chooseNode);
|
2025-08-20 14:39:30 +08:00
|
|
|
|
if (!choose) {
|
|
|
|
|
|
return message.error('请选择下一节点');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 调整成选中的节点的被审人员
|
2025-10-21 18:04:02 +08:00
|
|
|
|
const isEmpty = flowNextNodes.value.find((node) => !node.assignees?.length && node.chooseNode);
|
2025-08-20 14:39:30 +08:00
|
|
|
|
if (isEmpty) {
|
|
|
|
|
|
return message.error('请选择审批人');
|
|
|
|
|
|
}
|
2025-10-21 18:04:02 +08:00
|
|
|
|
flowNextNodes.value
|
|
|
|
|
|
.filter((node) => node.chooseNode)
|
|
|
|
|
|
.forEach((nNode) => {
|
|
|
|
|
|
nextTaskUser[nNode.activityId] = isEnd.value ? '' : typeof nNode.assignees == 'string' ? nNode.assignees : nNode.assignees.join(',');
|
|
|
|
|
|
});
|
2025-08-20 14:39:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (_action.value === 'reject') {
|
2025-10-21 18:04:02 +08:00
|
|
|
|
const isChoose = rejectNodeList.value.find((node) => node.activityId == rejectNodeId.value && node.assignees?.length);
|
2025-08-20 14:39:30 +08:00
|
|
|
|
if (!isChoose) {
|
|
|
|
|
|
return message.error('请选择审批人');
|
|
|
|
|
|
}
|
|
|
|
|
|
rejectNodeList.value.forEach((nNode) => {
|
2025-10-21 18:04:02 +08:00
|
|
|
|
if (nNode.activityId == rejectNodeId.value) {
|
|
|
|
|
|
nextTaskUser[nNode.activityId] = isEnd.value ? '' : typeof nNode.assignees == 'string' ? nNode.assignees : nNode.assignees.join(',');
|
2025-08-20 14:39:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_callback && typeof _callback === 'function') {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
_callback({
|
|
|
|
|
|
opinion: formState.opinion,
|
|
|
|
|
|
rejectNodeId: rejectNodeId.value,
|
|
|
|
|
|
nextTaskUser,
|
|
|
|
|
|
isEnd
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
isOpen.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onClickCancel() {
|
|
|
|
|
|
if (_onCancel && typeof _onCancel === 'function') {
|
|
|
|
|
|
_onCancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
isOpen.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopLoading() {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
|
toggleDialog,
|
|
|
|
|
|
stopLoading
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
2025-10-21 18:04:02 +08:00
|
|
|
|
.dialog-wrap {
|
|
|
|
|
|
padding: 10px 15px 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-info {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin: 6px 0;
|
|
|
|
|
|
line-height: 2;
|
|
|
|
|
|
padding-left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-label {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
min-width: 25%;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
padding-right: 8px;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-name {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
|
}
|
2025-08-20 14:39:30 +08:00
|
|
|
|
</style>
|