fix(bpm):修正 BPM 流程实例审批弹窗网关分支重算的并发与提交问题

- 提交时不再用节点表单值覆盖 data.variables;与预览阶段使用同一份合并变量
- onChange 加 useDebounceFn(300ms) + 请求序号去重,handleAudit 提交前 await 最新一轮重算
- 切换任务时重置请求序号与 pending 重算
- 改用 form-create 官方 formData() 取节点表单当前值
- 节点表单初始化等 fApi 就绪后再计算下一节点(until + 1s 兜底)

同步至 web-antd / web-ele 两端
pull/341/head
YunaiV 2026-05-03 16:35:03 +08:00
parent a3d8e4bfc1
commit c641542c71
2 changed files with 124 additions and 16 deletions

View File

@ -24,6 +24,7 @@ import { useUserStore } from '@vben/stores';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import FormCreate from '@form-create/ant-design-vue'; import FormCreate from '@form-create/ant-design-vue';
import { until, useDebounceFn } from '@vueuse/core';
import { import {
Alert, Alert,
Button, Button,
@ -113,6 +114,8 @@ const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
[], [],
); // ); //
const nextAssigneesTimelineRef = ref(); // 线 const nextAssigneesTimelineRef = ref(); // 线
let nextApprovalRequestId = 0; // onChange
let pendingNextNodesTask: null | Promise<unknown> = null; // onChange await
const approveReasonForm: any = reactive({ const approveReasonForm: any = reactive({
reason: '', reason: '',
signPicUrl: '', signPicUrl: '',
@ -256,7 +259,6 @@ async function openPopover(type: string) {
message.warning('表单校验不通过,请先完善表单!!'); message.warning('表单校验不通过,请先完善表单!!');
return; return;
} }
await initNextAssigneesFormField();
} }
if (type === 'return') { if (type === 'return') {
// 退 // 退
@ -269,6 +271,20 @@ async function openPopover(type: string) {
Object.keys(popOverVisible.value).forEach((item) => { Object.keys(popOverVisible.value).forEach((item) => {
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type; if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
}); });
if (type === 'approve') {
// form-create fApi
// approveFormFApi
if (runningTask.value?.formId > 0) {
// 1s until
await until(
() => typeof approveFormFApi.value?.validate === 'function',
)
.toBeTruthy({ timeout: 1000 })
.catch(() => {});
}
//
await initNextAssigneesFormField();
}
} }
/** 关闭气泡卡 */ /** 关闭气泡卡 */
@ -286,6 +302,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */ /** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
async function initNextAssigneesFormField() { async function initNextAssigneesFormField() {
//
const requestId = ++nextApprovalRequestId;
// , // ,
const variables = getUpdatedProcessInstanceVariables(); const variables = getUpdatedProcessInstanceVariables();
const data = await getNextApprovalNodes({ const data = await getNextApprovalNodes({
@ -293,6 +311,12 @@ async function initNextAssigneesFormField() {
taskId: runningTask.value.id, taskId: runningTask.value.id,
processVariablesStr: JSON.stringify(variables), processVariablesStr: JSON.stringify(variables),
}); });
//
if (requestId !== nextApprovalRequestId) {
return;
}
//
nextAssigneesActivityNode.value = [];
if (data && data.length > 0) { if (data && data.length > 0) {
const customApproveUsersData: Record<string, any[]> = {}; // Timeline const customApproveUsersData: Record<string, any[]> = {}; // Timeline
data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => { data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
@ -327,6 +351,12 @@ async function initNextAssigneesFormField() {
} }
} }
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
const debouncedInitNextAssigneesFormField = useDebounceFn(
initNextAssigneesFormField,
300,
);
/** 选择下一个节点的审批人 */ /** 选择下一个节点的审批人 */
function selectNextAssigneesConfirm(id: string, userList: any[]) { function selectNextAssigneesConfirm(id: string, userList: any[]) {
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id); approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
@ -362,6 +392,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
} }
if (pass) { if (pass) {
// onChange + +
if (pendingNextNodesTask) {
await pendingNextNodesTask;
}
const nextAssigneesValid = validateNextAssignees(); const nextAssigneesValid = validateNextAssignees();
if (!nextAssigneesValid) return; if (!nextAssigneesValid) return;
const variables = getUpdatedProcessInstanceVariables(); const variables = getUpdatedProcessInstanceVariables();
@ -376,12 +410,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
if (runningTask.value.signEnable) { if (runningTask.value.signEnable) {
data.signPicUrl = approveReasonForm.signPicUrl; data.signPicUrl = approveReasonForm.signPicUrl;
} }
// approveForm + data // getUpdatedProcessInstanceVariables data.variables
// TODO
const formCreateApi = approveFormFApi.value; const formCreateApi = approveFormFApi.value;
if (Object.keys(formCreateApi)?.length > 0) { if (Object.keys(formCreateApi)?.length > 0) {
await formCreateApi.validate(); await formCreateApi.validate();
data.variables = approveForm.value.value;
} }
await approveTask(data); await approveTask(data);
popOverVisible.value.approve = false; popOverVisible.value.approve = false;
@ -648,18 +680,32 @@ function loadTodoTask(task: any) {
approveForm.value = {}; approveForm.value = {};
runningTask.value = task; runningTask.value = task;
approveFormFApi.value = {}; approveFormFApi.value = {};
// pending /Promise
nextApprovalRequestId += 1;
pendingNextNodesTask = null;
reasonRequire.value = task?.reasonRequire ?? false; reasonRequire.value = task?.reasonRequire ?? false;
nodeTypeName.value = nodeTypeName.value =
task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批'; task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批';
// approve // approve
if (task && task.formId && task.formConf) { if (task && task.formId && task.formConf) {
const tempApproveForm = {}; const tempApproveForm: { option?: any; rule?: any; value?: any } = {};
setConfAndFields2( setConfAndFields2(
tempApproveForm, tempApproveForm,
task.formConf, task.formConf,
task.formFields, task.formFields,
task.formVariables, task.formVariables,
); );
// onChange
tempApproveForm.option.onChange = () => {
//
if (!popOverVisible.value.approve) {
return;
}
// useDebounceFn Promise reject catch 'cancelled'
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(
() => {},
);
};
approveForm.value = tempApproveForm; approveForm.value = tempApproveForm;
} else { } else {
approveForm.value = {}; // approveForm.value = {}; //
@ -684,9 +730,17 @@ async function validateNormalForm() {
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */ /** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
function getUpdatedProcessInstanceVariables() { function getUpdatedProcessInstanceVariables() {
const variables: any = {}; const variables: any = {};
props.writableFields.forEach((field: string) => { //
variables[field] = props.normalFormApi.getValue(field); if (props.writableFields?.length && props.normalFormApi) {
}); props.writableFields.forEach((field: string) => {
variables[field] = props.normalFormApi.getValue(field);
});
}
// form-create formData()
const nodeFormData = approveFormFApi.value?.formData?.();
if (nodeFormData) {
Object.assign(variables, nodeFormData);
}
return variables; return variables;
} }

View File

@ -23,6 +23,7 @@ import { useUserStore } from '@vben/stores';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import FormCreate from '@form-create/element-ui'; import FormCreate from '@form-create/element-ui';
import { until, useDebounceFn } from '@vueuse/core';
import { import {
ElAlert, ElAlert,
ElButton, ElButton,
@ -111,6 +112,8 @@ const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
[], [],
); // ); //
const nextAssigneesTimelineRef = ref(); // 线 const nextAssigneesTimelineRef = ref(); // 线
let nextApprovalRequestId = 0; // onChange
let pendingNextNodesTask: null | Promise<unknown> = null; // onChange await
const approveReasonForm: any = reactive({ const approveReasonForm: any = reactive({
reason: '', reason: '',
signPicUrl: '', signPicUrl: '',
@ -264,7 +267,6 @@ async function openPopover(type: string) {
ElMessage.warning('表单校验不通过,请先完善表单!!'); ElMessage.warning('表单校验不通过,请先完善表单!!');
return; return;
} }
await initNextAssigneesFormField();
} }
if (type === 'return') { if (type === 'return') {
// 退 // 退
@ -277,6 +279,20 @@ async function openPopover(type: string) {
Object.keys(popOverVisible.value).forEach((item) => { Object.keys(popOverVisible.value).forEach((item) => {
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type; if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
}); });
if (type === 'approve') {
// form-create fApi
// approveFormFApi
if (runningTask.value?.formId > 0) {
// 1s until
await until(
() => typeof approveFormFApi.value?.validate === 'function',
)
.toBeTruthy({ timeout: 1000 })
.catch(() => {});
}
//
await initNextAssigneesFormField();
}
} }
/** 关闭气泡卡 */ /** 关闭气泡卡 */
@ -294,6 +310,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */ /** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
async function initNextAssigneesFormField() { async function initNextAssigneesFormField() {
//
const requestId = ++nextApprovalRequestId;
// , // ,
const variables = getUpdatedProcessInstanceVariables(); const variables = getUpdatedProcessInstanceVariables();
const data = await getNextApprovalNodes({ const data = await getNextApprovalNodes({
@ -301,6 +319,12 @@ async function initNextAssigneesFormField() {
taskId: runningTask.value.id, taskId: runningTask.value.id,
processVariablesStr: JSON.stringify(variables), processVariablesStr: JSON.stringify(variables),
}); });
//
if (requestId !== nextApprovalRequestId) {
return;
}
//
nextAssigneesActivityNode.value = [];
if (data && data.length > 0) { if (data && data.length > 0) {
const customApproveUsersData: Record<string, any[]> = {}; // Timeline const customApproveUsersData: Record<string, any[]> = {}; // Timeline
data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => { data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
@ -335,6 +359,12 @@ async function initNextAssigneesFormField() {
} }
} }
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
const debouncedInitNextAssigneesFormField = useDebounceFn(
initNextAssigneesFormField,
300,
);
/** 选择下一个节点的审批人 */ /** 选择下一个节点的审批人 */
function selectNextAssigneesConfirm(id: string, userList: any[]) { function selectNextAssigneesConfirm(id: string, userList: any[]) {
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id); approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
@ -370,6 +400,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
} }
if (pass) { if (pass) {
// onChange + +
if (pendingNextNodesTask) {
await pendingNextNodesTask;
}
const nextAssigneesValid = validateNextAssignees(); const nextAssigneesValid = validateNextAssignees();
if (!nextAssigneesValid) return; if (!nextAssigneesValid) return;
const variables = getUpdatedProcessInstanceVariables(); const variables = getUpdatedProcessInstanceVariables();
@ -384,12 +418,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
if (runningTask.value.signEnable) { if (runningTask.value.signEnable) {
data.signPicUrl = approveReasonForm.signPicUrl; data.signPicUrl = approveReasonForm.signPicUrl;
} }
// approveForm + data // getUpdatedProcessInstanceVariables data.variables
// TODO
const formCreateApi = approveFormFApi.value; const formCreateApi = approveFormFApi.value;
if (Object.keys(formCreateApi)?.length > 0) { if (Object.keys(formCreateApi)?.length > 0) {
await formCreateApi.validate(); await formCreateApi.validate();
data.variables = approveForm.value.value;
} }
await approveTask(data); await approveTask(data);
popOverVisible.value.approve = false; popOverVisible.value.approve = false;
@ -656,18 +688,32 @@ function loadTodoTask(task: any) {
approveForm.value = {}; approveForm.value = {};
runningTask.value = task; runningTask.value = task;
approveFormFApi.value = {}; approveFormFApi.value = {};
// pending /Promise
nextApprovalRequestId += 1;
pendingNextNodesTask = null;
reasonRequire.value = task?.reasonRequire ?? false; reasonRequire.value = task?.reasonRequire ?? false;
nodeTypeName.value = nodeTypeName.value =
task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批'; task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批';
// approve // approve
if (task && task.formId && task.formConf) { if (task && task.formId && task.formConf) {
const tempApproveForm = {}; const tempApproveForm: { option?: any; rule?: any; value?: any } = {};
setConfAndFields2( setConfAndFields2(
tempApproveForm, tempApproveForm,
task.formConf, task.formConf,
task.formFields, task.formFields,
task.formVariables, task.formVariables,
); );
// onChange
tempApproveForm.option.onChange = () => {
//
if (!popOverVisible.value.approve) {
return;
}
// useDebounceFn Promise reject catch 'cancelled'
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(
() => {},
);
};
approveForm.value = tempApproveForm; approveForm.value = tempApproveForm;
} else { } else {
approveForm.value = {}; // approveForm.value = {}; //
@ -692,9 +738,17 @@ async function validateNormalForm() {
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */ /** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
function getUpdatedProcessInstanceVariables() { function getUpdatedProcessInstanceVariables() {
const variables: any = {}; const variables: any = {};
props.writableFields.forEach((field: string) => { //
variables[field] = props.normalFormApi.getValue(field); if (props.writableFields?.length && props.normalFormApi) {
}); props.writableFields.forEach((field: string) => {
variables[field] = props.normalFormApi.getValue(field);
});
}
// form-create formData()
const nodeFormData = approveFormFApi.value?.formData?.();
if (nodeFormData) {
Object.assign(variables, nodeFormData);
}
return variables; return variables;
} }