diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue index 829fb71f8..f7e155338 100644 --- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue @@ -24,6 +24,7 @@ import { useUserStore } from '@vben/stores'; import { isEmpty } from '@vben/utils'; import FormCreate from '@form-create/ant-design-vue'; +import { until, useDebounceFn } from '@vueuse/core'; import { Alert, Button, @@ -113,6 +114,8 @@ const nextAssigneesActivityNode = ref( [], ); // 下一个审批节点信息 const nextAssigneesTimelineRef = ref(); // 下一个节点审批人时间线组件的引用 +let nextApprovalRequestId = 0; // 请求序号;onChange 高频触发时,丢弃过期请求结果 +let pendingNextNodesTask: null | Promise = null; // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成 const approveReasonForm: any = reactive({ reason: '', signPicUrl: '', @@ -256,7 +259,6 @@ async function openPopover(type: string) { message.warning('表单校验不通过,请先完善表单!!'); return; } - await initNextAssigneesFormField(); } if (type === 'return') { // 获取退回节点 @@ -269,6 +271,20 @@ async function openPopover(type: string) { Object.keys(popOverVisible.value).forEach((item) => { 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() { + // 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废 + const requestId = ++nextApprovalRequestId; // 获取修改的流程变量, 暂时只支持流程表单 const variables = getUpdatedProcessInstanceVariables(); const data = await getNextApprovalNodes({ @@ -293,6 +311,12 @@ async function initNextAssigneesFormField() { taskId: runningTask.value.id, processVariablesStr: JSON.stringify(variables), }); + // 已有更新的请求发出,丢弃本次过期结果,避免把旧分支节点回写到当前列表 + if (requestId !== nextApprovalRequestId) { + return; + } + // 在最新结果到达时再清空,避免请求期间出现节点信息抖动 + nextAssigneesActivityNode.value = []; if (data && data.length > 0) { const customApproveUsersData: Record = {}; // 用于收集需要设置到 Timeline 组件的自定义审批人数据 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[]) { approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id); @@ -362,6 +392,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { } if (pass) { + // 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交 + if (pendingNextNodesTask) { + await pendingNextNodesTask; + } const nextAssigneesValid = validateNextAssignees(); if (!nextAssigneesValid) return; const variables = getUpdatedProcessInstanceVariables(); @@ -376,12 +410,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { if (runningTask.value.signEnable) { data.signPicUrl = approveReasonForm.signPicUrl; } - // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 - // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突 + // 多表单处理:节点表单需要校验;变量已经在 getUpdatedProcessInstanceVariables 中合并到 data.variables,无需再覆盖 const formCreateApi = approveFormFApi.value; if (Object.keys(formCreateApi)?.length > 0) { await formCreateApi.validate(); - data.variables = approveForm.value.value; } await approveTask(data); popOverVisible.value.approve = false; @@ -648,18 +680,32 @@ function loadTodoTask(task: any) { approveForm.value = {}; runningTask.value = task; approveFormFApi.value = {}; + // 切换任务时重置请求序号与 pending 重算,避免旧任务飞行中的请求/Promise 串到新任务 + nextApprovalRequestId += 1; + pendingNextNodesTask = null; reasonRequire.value = task?.reasonRequire ?? false; nodeTypeName.value = task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批'; // 处理 approve 表单 if (task && task.formId && task.formConf) { - const tempApproveForm = {}; + const tempApproveForm: { option?: any; rule?: any; value?: any } = {}; setConfAndFields2( tempApproveForm, task.formConf, task.formFields, task.formVariables, ); + // 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段 + tempApproveForm.option.onChange = () => { + // 弹窗打开时,才重新计算下一个节点的信息 + if (!popOverVisible.value.approve) { + return; + } + // useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled' + pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch( + () => {}, + ); + }; approveForm.value = tempApproveForm; } else { approveForm.value = {}; // 占位,避免为空 @@ -684,9 +730,17 @@ async function validateNormalForm() { /** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */ function getUpdatedProcessInstanceVariables() { 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; } diff --git a/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue index 28f1fe4a2..acaff4d0d 100644 --- a/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue +++ b/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue @@ -23,6 +23,7 @@ import { useUserStore } from '@vben/stores'; import { isEmpty } from '@vben/utils'; import FormCreate from '@form-create/element-ui'; +import { until, useDebounceFn } from '@vueuse/core'; import { ElAlert, ElButton, @@ -111,6 +112,8 @@ const nextAssigneesActivityNode = ref( [], ); // 下一个审批节点信息 const nextAssigneesTimelineRef = ref(); // 下一个节点审批人时间线组件的引用 +let nextApprovalRequestId = 0; // 请求序号;onChange 高频触发时,丢弃过期请求结果 +let pendingNextNodesTask: null | Promise = null; // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成 const approveReasonForm: any = reactive({ reason: '', signPicUrl: '', @@ -264,7 +267,6 @@ async function openPopover(type: string) { ElMessage.warning('表单校验不通过,请先完善表单!!'); return; } - await initNextAssigneesFormField(); } if (type === 'return') { // 获取退回节点 @@ -277,6 +279,20 @@ async function openPopover(type: string) { Object.keys(popOverVisible.value).forEach((item) => { 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() { + // 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废 + const requestId = ++nextApprovalRequestId; // 获取修改的流程变量, 暂时只支持流程表单 const variables = getUpdatedProcessInstanceVariables(); const data = await getNextApprovalNodes({ @@ -301,6 +319,12 @@ async function initNextAssigneesFormField() { taskId: runningTask.value.id, processVariablesStr: JSON.stringify(variables), }); + // 已有更新的请求发出,丢弃本次过期结果,避免把旧分支节点回写到当前列表 + if (requestId !== nextApprovalRequestId) { + return; + } + // 在最新结果到达时再清空,避免请求期间出现节点信息抖动 + nextAssigneesActivityNode.value = []; if (data && data.length > 0) { const customApproveUsersData: Record = {}; // 用于收集需要设置到 Timeline 组件的自定义审批人数据 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[]) { approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id); @@ -370,6 +400,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { } if (pass) { + // 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交 + if (pendingNextNodesTask) { + await pendingNextNodesTask; + } const nextAssigneesValid = validateNextAssignees(); if (!nextAssigneesValid) return; const variables = getUpdatedProcessInstanceVariables(); @@ -384,12 +418,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) { if (runningTask.value.signEnable) { data.signPicUrl = approveReasonForm.signPicUrl; } - // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 - // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突 + // 多表单处理:节点表单需要校验;变量已经在 getUpdatedProcessInstanceVariables 中合并到 data.variables,无需再覆盖 const formCreateApi = approveFormFApi.value; if (Object.keys(formCreateApi)?.length > 0) { await formCreateApi.validate(); - data.variables = approveForm.value.value; } await approveTask(data); popOverVisible.value.approve = false; @@ -656,18 +688,32 @@ function loadTodoTask(task: any) { approveForm.value = {}; runningTask.value = task; approveFormFApi.value = {}; + // 切换任务时重置请求序号与 pending 重算,避免旧任务飞行中的请求/Promise 串到新任务 + nextApprovalRequestId += 1; + pendingNextNodesTask = null; reasonRequire.value = task?.reasonRequire ?? false; nodeTypeName.value = task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批'; // 处理 approve 表单 if (task && task.formId && task.formConf) { - const tempApproveForm = {}; + const tempApproveForm: { option?: any; rule?: any; value?: any } = {}; setConfAndFields2( tempApproveForm, task.formConf, task.formFields, task.formVariables, ); + // 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段 + tempApproveForm.option.onChange = () => { + // 弹窗打开时,才重新计算下一个节点的信息 + if (!popOverVisible.value.approve) { + return; + } + // useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled' + pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch( + () => {}, + ); + }; approveForm.value = tempApproveForm; } else { approveForm.value = {}; // 占位,避免为空 @@ -692,9 +738,17 @@ async function validateNormalForm() { /** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */ function getUpdatedProcessInstanceVariables() { 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; }