From 2c3842582ff21dd15e630628f616303f2467138d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 00:35:16 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=90=9B=20fix(system)=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A7=9F=E6=88=B7=20get-by-website=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8D=E6=94=AF=E6=8C=81=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=20=F0=9F=90=9B=20fix(mes)=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B8=B8=E8=A7=81=E7=BC=BA=E9=99=B7=E7=9A=84?= =?UTF-8?q?=E3=80=8C=E6=A3=80=E6=B5=8B=E9=A1=B9=E7=B1=BB=E5=9E=8B=E3=80=8D?= =?UTF-8?q?=E9=94=99=E7=94=A8=E7=8B=AC=E7=AB=8B=E5=AD=97=E5=85=B8=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「常见缺陷」与「检测项设置」的「检测项类型」语义一致,应共用同一份字典;DefectForm 与列表页统一改为 MES_INDICATOR_TYPE,并清理未使用的 MES_DEFECT_TYPE 常量。 --- src/utils/dict.ts | 1 - src/views/mes/qc/defect/DefectForm.vue | 2 +- src/views/mes/qc/defect/index.vue | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 7965a9376..91909f538 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -268,7 +268,6 @@ export enum DICT_TYPE { MES_INDICATOR_TYPE = 'mes_indicator_type', // MES 检测项类型 MES_QC_RESULT_TYPE = 'mes_qc_result_type', // MES 质检结果值类型 MES_DEFECT_LEVEL = 'mes_defect_level', // MES 缺陷等级 - MES_DEFECT_TYPE = 'mes_defect_type', // MES 缺陷检测项类型 MES_PRO_WORK_ORDER_STATUS = 'mes_pro_work_order_status', // MES 生产工单状态 MES_PRO_WORK_ORDER_SOURCE_TYPE = 'mes_pro_work_order_source_type', // MES 工单来源类型 MES_PRO_WORK_ORDER_TYPE = 'mes_pro_work_order_type', // MES 工单类型 diff --git a/src/views/mes/qc/defect/DefectForm.vue b/src/views/mes/qc/defect/DefectForm.vue index 6f49ad65c..2a0d623d3 100644 --- a/src/views/mes/qc/defect/DefectForm.vue +++ b/src/views/mes/qc/defect/DefectForm.vue @@ -21,7 +21,7 @@ From d5a9e2e3134ed745f1bf8e2f8de3e8034928b343 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 14:32:42 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91IoT=20?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=81=94=E5=8A=A8=EF=BC=9A=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E5=99=A8=E6=AF=94=E8=BE=83=E5=80=BC=E6=94=B9?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E6=96=87=E6=9C=AC=E8=BE=93=E5=85=A5=EF=BC=8C?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E7=95=99=E7=A9=BA=EF=BC=88=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E7=94=9F=E5=8D=B3=E5=8C=B9=E9=85=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/rule/scene/form/RuleSceneForm.vue | 20 +++++++---- .../form/configs/MainConditionInnerConfig.vue | 35 ++++++++----------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/views/iot/rule/scene/form/RuleSceneForm.vue b/src/views/iot/rule/scene/form/RuleSceneForm.vue index 22ba268e2..a5b2efede 100644 --- a/src/views/iot/rule/scene/form/RuleSceneForm.vue +++ b/src/views/iot/rule/scene/form/RuleSceneForm.vue @@ -128,13 +128,19 @@ const validateTriggers = (_rule: any, value: any, callback: any) => { callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`)) return } - if (!trigger.operator) { - callback(new Error(`触发器 ${i + 1}: 操作符不能为空`)) - return - } - if (trigger.value === undefined || trigger.value === null || trigger.value === '') { - callback(new Error(`触发器 ${i + 1}: 参数值不能为空`)) - return + // 事件上报 / 服务调用:operator 由前端自动设为 '=',参数值留空表示"事件 / 调用发生即匹配" + const isEventOrService = + trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST || + trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE + if (!isEventOrService) { + if (!trigger.operator) { + callback(new Error(`触发器 ${i + 1}: 操作符不能为空`)) + return + } + if (trigger.value === undefined || trigger.value === null || trigger.value === '') { + callback(new Error(`触发器 ${i + 1}: 参数值不能为空`)) + return + } } } diff --git a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue index 4c61d31e4..4ccff2b43 100644 --- a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue +++ b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue @@ -80,14 +80,20 @@ :config="serviceConfig" placeholder="请输入 JSON 格式的服务参数" /> - - + + { return undefined }) -// 计算属性:事件配置 - 用于 JsonParamsInput -const eventConfig = computed(() => { - if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) { - return { - event: { - name: propertyConfig.value.name || '事件', - outputParams: propertyConfig.value.outputParams || [] - } - } - } - return undefined -}) - /** * 更新条件字段 * @param field 字段名 From e98d575b3a02dec8d17f704f0bacbc97dda16e4f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 22:56:56 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91form-cr?= =?UTF-8?q?eate=20=E5=8D=95=E5=9B=BE=E4=B8=8A=E4=BC=A0=E8=A7=84=E5=88=99?= =?UTF-8?q?=20disabled=20=E5=AD=97=E6=AE=B5=E6=A0=87=E9=A2=98=E4=B8=8E?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E9=94=99=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/FormCreate/src/config/useUploadImgRule.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/FormCreate/src/config/useUploadImgRule.ts b/src/components/FormCreate/src/config/useUploadImgRule.ts index 546cf9d66..47dce1c87 100644 --- a/src/components/FormCreate/src/config/useUploadImgRule.ts +++ b/src/components/FormCreate/src/config/useUploadImgRule.ts @@ -74,8 +74,8 @@ export const useUploadImgRule = () => { { type: 'switch', field: 'disabled', - title: '是否显示删除按钮', - value: true + title: '是否禁用', + value: false }, { type: 'switch', From a704620f84ebd63ccfdba6efc63fbc09127d46eb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 00:28:27 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=E3=80=90framework=E3=80=91=E5=85=B3?= =?UTF-8?q?=E9=97=AD=20TagsView=20=E6=A0=87=E7=AD=BE=E5=90=8E=20keep-alive?= =?UTF-8?q?=20=E7=BC=93=E5=AD=98=E6=9C=AA=E6=94=B6=E7=BC=A9=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20DOM=EF=BC=8FJS=20heap=20=E4=B8=8D=E5=9B=9E?= =?UTF-8?q?=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit delView/delAllViews 误用 delCachedView,关闭非当前标签时会去删 currentRoute 对应的缓存,把要关的 name 留在 cachedViews 里,keep-alive include 不收缩, 旧组件实例无法 unmount。 回退到基于剩余 visitedViews 重建 cachedViews 的实现(对应 5718c7881 之前的写法); delCachedView 自身保留 issue #180 的修复,仍供 refreshPage 使用。 --- src/store/modules/tagsView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index b54063512..9a07d9604 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -78,7 +78,7 @@ export const useTagsViewStore = defineStore('tagsView', { // 删除某个 delView(view: RouteLocationNormalizedLoaded) { this.delVisitedView(view) - this.delCachedView() + this.addCachedView() }, // 删除tag delVisitedView(view: RouteLocationNormalizedLoaded) { @@ -106,7 +106,7 @@ export const useTagsViewStore = defineStore('tagsView', { // 删除所有缓存和tag delAllViews() { this.delAllVisitedViews() - this.delCachedView() + this.addCachedView() }, // 删除所有tag delAllVisitedViews() { From d2e82b710b549646cc1eb386f1a4d18de6b34a5e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 00:35:16 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=90=9B=20fix(system)=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A7=9F=E6=88=B7=20get-by-website=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8D=E6=94=AF=E6=8C=81=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=20=F0=9F=90=9B=20fix(mes)=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B8=B8=E8=A7=81=E7=BC=BA=E9=99=B7=E7=9A=84?= =?UTF-8?q?=E3=80=8C=E6=A3=80=E6=B5=8B=E9=A1=B9=E7=B1=BB=E5=9E=8B=E3=80=8D?= =?UTF-8?q?=E9=94=99=E7=94=A8=E7=8B=AC=E7=AB=8B=E5=AD=97=E5=85=B8=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「常见缺陷」与「检测项设置」的「检测项类型」语义一致,应共用同一份字典;DefectForm 与列表页统一改为 MES_INDICATOR_TYPE,并清理未使用的 MES_DEFECT_TYPE 常量。 --- src/utils/dict.ts | 1 - src/views/mes/qc/defect/DefectForm.vue | 2 +- src/views/mes/qc/defect/index.vue | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 7965a9376..91909f538 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -268,7 +268,6 @@ export enum DICT_TYPE { MES_INDICATOR_TYPE = 'mes_indicator_type', // MES 检测项类型 MES_QC_RESULT_TYPE = 'mes_qc_result_type', // MES 质检结果值类型 MES_DEFECT_LEVEL = 'mes_defect_level', // MES 缺陷等级 - MES_DEFECT_TYPE = 'mes_defect_type', // MES 缺陷检测项类型 MES_PRO_WORK_ORDER_STATUS = 'mes_pro_work_order_status', // MES 生产工单状态 MES_PRO_WORK_ORDER_SOURCE_TYPE = 'mes_pro_work_order_source_type', // MES 工单来源类型 MES_PRO_WORK_ORDER_TYPE = 'mes_pro_work_order_type', // MES 工单类型 diff --git a/src/views/mes/qc/defect/DefectForm.vue b/src/views/mes/qc/defect/DefectForm.vue index 6f49ad65c..2a0d623d3 100644 --- a/src/views/mes/qc/defect/DefectForm.vue +++ b/src/views/mes/qc/defect/DefectForm.vue @@ -21,7 +21,7 @@ From 192a11882382d35b81ff24a6aaecb73a3e54b7bb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 14:32:42 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91IoT=20?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=81=94=E5=8A=A8=EF=BC=9A=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E5=99=A8=E6=AF=94=E8=BE=83=E5=80=BC=E6=94=B9?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E6=96=87=E6=9C=AC=E8=BE=93=E5=85=A5=EF=BC=8C?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E7=95=99=E7=A9=BA=EF=BC=88=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E7=94=9F=E5=8D=B3=E5=8C=B9=E9=85=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/rule/scene/form/RuleSceneForm.vue | 20 +++++++---- .../form/configs/MainConditionInnerConfig.vue | 35 ++++++++----------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/views/iot/rule/scene/form/RuleSceneForm.vue b/src/views/iot/rule/scene/form/RuleSceneForm.vue index 22ba268e2..a5b2efede 100644 --- a/src/views/iot/rule/scene/form/RuleSceneForm.vue +++ b/src/views/iot/rule/scene/form/RuleSceneForm.vue @@ -128,13 +128,19 @@ const validateTriggers = (_rule: any, value: any, callback: any) => { callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`)) return } - if (!trigger.operator) { - callback(new Error(`触发器 ${i + 1}: 操作符不能为空`)) - return - } - if (trigger.value === undefined || trigger.value === null || trigger.value === '') { - callback(new Error(`触发器 ${i + 1}: 参数值不能为空`)) - return + // 事件上报 / 服务调用:operator 由前端自动设为 '=',参数值留空表示"事件 / 调用发生即匹配" + const isEventOrService = + trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST || + trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE + if (!isEventOrService) { + if (!trigger.operator) { + callback(new Error(`触发器 ${i + 1}: 操作符不能为空`)) + return + } + if (trigger.value === undefined || trigger.value === null || trigger.value === '') { + callback(new Error(`触发器 ${i + 1}: 参数值不能为空`)) + return + } } } diff --git a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue index 4c61d31e4..4ccff2b43 100644 --- a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue +++ b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue @@ -80,14 +80,20 @@ :config="serviceConfig" placeholder="请输入 JSON 格式的服务参数" /> - - + + { return undefined }) -// 计算属性:事件配置 - 用于 JsonParamsInput -const eventConfig = computed(() => { - if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) { - return { - event: { - name: propertyConfig.value.name || '事件', - outputParams: propertyConfig.value.outputParams || [] - } - } - } - return undefined -}) - /** * 更新条件字段 * @param field 字段名 From 0cc2bff0f4e80366f953158b4cd3d72dfef3a6de Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 May 2026 22:56:56 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91form-cr?= =?UTF-8?q?eate=20=E5=8D=95=E5=9B=BE=E4=B8=8A=E4=BC=A0=E8=A7=84=E5=88=99?= =?UTF-8?q?=20disabled=20=E5=AD=97=E6=AE=B5=E6=A0=87=E9=A2=98=E4=B8=8E?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E9=94=99=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/FormCreate/src/config/useUploadImgRule.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/FormCreate/src/config/useUploadImgRule.ts b/src/components/FormCreate/src/config/useUploadImgRule.ts index 546cf9d66..47dce1c87 100644 --- a/src/components/FormCreate/src/config/useUploadImgRule.ts +++ b/src/components/FormCreate/src/config/useUploadImgRule.ts @@ -74,8 +74,8 @@ export const useUploadImgRule = () => { { type: 'switch', field: 'disabled', - title: '是否显示删除按钮', - value: true + title: '是否禁用', + value: false }, { type: 'switch', From fa9facfa0b445129c89a66b44f509ddc506843a3 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 00:28:27 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=E3=80=90framework=E3=80=91=E5=85=B3?= =?UTF-8?q?=E9=97=AD=20TagsView=20=E6=A0=87=E7=AD=BE=E5=90=8E=20keep-alive?= =?UTF-8?q?=20=E7=BC=93=E5=AD=98=E6=9C=AA=E6=94=B6=E7=BC=A9=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20DOM=EF=BC=8FJS=20heap=20=E4=B8=8D=E5=9B=9E?= =?UTF-8?q?=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit delView/delAllViews 误用 delCachedView,关闭非当前标签时会去删 currentRoute 对应的缓存,把要关的 name 留在 cachedViews 里,keep-alive include 不收缩, 旧组件实例无法 unmount。 回退到基于剩余 visitedViews 重建 cachedViews 的实现(对应 5718c7881 之前的写法); delCachedView 自身保留 issue #180 的修复,仍供 refreshPage 使用。 --- src/store/modules/tagsView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index b54063512..9a07d9604 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -78,7 +78,7 @@ export const useTagsViewStore = defineStore('tagsView', { // 删除某个 delView(view: RouteLocationNormalizedLoaded) { this.delVisitedView(view) - this.delCachedView() + this.addCachedView() }, // 删除tag delVisitedView(view: RouteLocationNormalizedLoaded) { @@ -106,7 +106,7 @@ export const useTagsViewStore = defineStore('tagsView', { // 删除所有缓存和tag delAllViews() { this.delAllVisitedViews() - this.delCachedView() + this.addCachedView() }, // 删除所有tag delAllVisitedViews() { From 9df6828255a5745f02e6018b3258a9071ed4c007 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 16:34:55 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(bpm)=EF=BC=9A=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=AE=9E=E4=BE=8B=E5=AE=A1=E6=89=B9=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E7=BD=91=E5=85=B3=E5=88=86=E6=94=AF=E9=87=8D=E7=AE=97?= =?UTF-8?q?=E7=9A=84=E5=B9=B6=E5=8F=91=E4=B8=8E=E6=8F=90=E4=BA=A4=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提交时不再用节点表单值覆盖 data.variables;与预览阶段使用同一份合并变量 - onChange 加 useDebounceFn(300ms) + 请求序号去重,handleAudit 提交前 await 最新一轮重算 - 切换任务时重置请求序号与 pending 重算 - 改用 form-create 官方 formData() 取节点表单当前值 - 双 nextTick 改为 until 等 fApi 就绪,1s 兜底超时 --- .../detail/ProcessInstanceOperationButton.vue | 103 +++++++++--------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue index 34929a677..79f1df559 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue @@ -526,6 +526,7 @@ import { } from '@/components/SimpleProcessDesignerV2/src/consts' import { BpmModelFormType, BpmProcessInstanceStatus } from '@/utils/constants' import type { FormInstance, FormRules } from 'element-plus' +import { until, useDebounceFn } from '@vueuse/core' import SignDialog from './SignDialog.vue' import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue' import { isEmpty } from '@/utils/is' @@ -574,6 +575,8 @@ const signRef = ref() const approveSignFormRef = ref() const nextAssigneesActivityNode = ref([]) // 下一个审批节点信息 const nextAssigneesTimelineRef = ref() // 下一个节点审批人时间线组件的引用 +let nextApprovalRequestId = 0 // 请求序号;onChange 高频触发时,丢弃过期请求结果 +let pendingNextNodesTask: Promise | null = null // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成 const approveReasonForm = reactive({ reason: '', signPicUrl: '', @@ -582,7 +585,11 @@ const approveReasonForm = reactive({ const approveReasonRule = computed(() => { return { reason: [ - { required: reasonRequire.value, message: nodeTypeName.value + '意见不能为空', trigger: 'blur' } + { + required: reasonRequire.value, + message: nodeTypeName.value + '意见不能为空', + trigger: 'blur' + } ], signPicUrl: [{ required: true, message: '签名不能为空', trigger: 'change' }], nextAssignees: [{ required: true, message: '审批人不能为空', trigger: 'blur' }] @@ -709,11 +716,16 @@ const openPopover = async (type: string) => { popOverVisible.value[item] = item === type }) if (type === 'approve') { - // 等待表单渲染完成后,再初始化下一个节点信息 - await nextTick() - // 再等待一个 tick,确保 form-create 的 API 已经初始化 - await nextTick() - initNextAssigneesFormField() + // 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点; + // 没有节点表单时,approveFormFApi 永远不会被赋值,跳过等待 + if (runningTask.value?.formId > 0) { + // 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行 + await until(() => typeof approveFormFApi.value?.validate === 'function') + .toBeTruthy({ timeout: 1000 }) + .catch(() => {}) + } + // 初始化下一个审批人表单字段 + await initNextAssigneesFormField() } // await nextTick() // formRef.value.resetFields() @@ -734,6 +746,8 @@ const closePopover = (type: string, formRef: FormInstance | undefined) => { /** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */ const initNextAssigneesFormField = async () => { + // 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废 + const requestId = ++nextApprovalRequestId // 获取修改的流程变量, 暂时只支持流程表单 const variables = getUpdatedProcessInstanceVariables() const data = await ProcessInstanceApi.getNextApprovalNodes({ @@ -741,6 +755,12 @@ const initNextAssigneesFormField = async () => { 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: any) => { @@ -769,6 +789,9 @@ const initNextAssigneesFormField = async () => { } } +/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */ +const debouncedInitNextAssigneesFormField = useDebounceFn(initNextAssigneesFormField, 300) + /** 选择下一个节点的审批人 */ const selectNextAssigneesConfirm = (id: string, userList: any[]) => { approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id) @@ -803,6 +826,10 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => } if (pass) { + // 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交 + if (pendingNextNodesTask) { + await pendingNextNodesTask + } const nextAssigneesValid = validateNextAssignees() if (!nextAssigneesValid) return const variables = getUpdatedProcessInstanceVariables() @@ -817,13 +844,10 @@ const handleAudit = async (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() - // @ts-ignore - data.variables = approveForm.value.value } await TaskApi.approveTask(data) popOverVisible.value.approve = false @@ -1081,27 +1105,23 @@ const 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 === NodeType.TRANSACTOR_NODE ? '办理' : '审批' - // 处理 approve 表单. + // 处理 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 事件,当表单值变化时,重新计算下一个节点的信息 - // @ts-ignore - if (!tempApproveForm.option) { - // @ts-ignore - tempApproveForm.option = {} - } - // @ts-ignore + // 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段 tempApproveForm.option.onChange = () => { - // 当弹窗打开时,才重新计算下一个节点的信息 - if (popOverVisible.value.approve) { - // 清空之前的节点信息 - nextAssigneesActivityNode.value = [] - // 重新计算下一个节点的信息 - initNextAssigneesFormField() + // 弹窗打开时,才重新计算下一个节点的信息 + if (!popOverVisible.value.approve) { + return } + // useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled' + pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(() => {}) } approveForm.value = tempApproveForm } else { @@ -1128,38 +1148,15 @@ const validateNormalForm = async () => { const getUpdatedProcessInstanceVariables = () => { const variables = {} // 从流程表单(流程定义级别)中获取变量 - if (props.writableFields && props.writableFields.length > 0 && props.normalFormApi) { + if (props.writableFields?.length && props.normalFormApi) { props.writableFields.forEach((field) => { variables[field] = props.normalFormApi.getValue(field) }) } - // 从节点表单(节点级别)中获取变量 - // 优先从 approveForm.value.value 中获取(这是 form-create 存储的值) - if (approveForm.value?.value) { - Object.assign(variables, approveForm.value.value) - } - // 再从 formVariables 中获取(这是后端返回的已保存的变量) - if (approveForm.value?.formVariables) { - Object.assign(variables, approveForm.value.formVariables) - } - // 再从 formFields 中获取(这是表单的字段值) - if (approveForm.value?.formFields) { - Object.assign(variables, approveForm.value.formFields) - } - // 最后尝试从 approveFormFApi 中获取(这是用户在表单中修改的值) - if (approveFormFApi.value && approveForm.value?.rule) { - approveForm.value.rule.forEach((field: any) => { - if (field.field) { - try { - const value = approveFormFApi.value.getValue(field.field) - if (value !== undefined && value !== null) { - variables[field.field] = value - } - } catch (e) { - // 忽略获取值时的错误 - } - } - }) + // 从节点表单(节点级别)中获取变量;通过 form-create 官方的 formData() 拿当前值 + const nodeFormData = approveFormFApi.value?.formData?.() + if (nodeFormData) { + Object.assign(variables, nodeFormData) } return variables }