diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 index ded251e..ea09520 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 @@ -893,3 +893,60 @@ jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue + +-- author:GHT---date:20260610--for: 【MESToDing审批配置】钉钉模板列表操作列绑定审批流程弹窗 ----- +jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/BindApprovalFlowModal.vue +jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue + +-- author:GHT---date:20260610--for: 【MESToDing审批配置】模板名称 MES↔钉钉 双向同步 ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java +jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts +jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue + +-- author:GHT---date:20260610--for: 【钉钉审批模板绑定】字段绑定支持原值/显示文本 ----- +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizEntityFieldIntrospector.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingTplBindController.java +jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplFieldValue.ts +jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplBind.api.ts +jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/index.vue +jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue + +-- author:GHT---date:20260610--for: 【审批流设计】节点内生成集成方案并配置动作 ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java +jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts +jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue +jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue +jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue + +-- author:GHT---date:20260610--for: 【审批注册中心】物理表名改为数据库表下拉选择 ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslBizDocRegistryController.java +jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.api.ts +jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts + +-- author:GHT---date:20260610--for: 【配合示方】审批进度展示改为关联痕迹表 ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/advice/ApprovalTraceResponseAdvice.java +jeecgboot-vue3/src/views/xslmes/approval/integration/traceRecordHelper.ts +jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/components/MesXslFormulaSpecModal.vue + +-- author:GHT---date:20260610--for: 【混炼示方】页脚签章区展示痕迹表审批人/时间 ----- +jeecgboot-vue3/src/views/xslmes/approval/integration/traceRecordHelper.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue + +-- author:GHT---date:20260610--for: 【配合示方】移除手写痕迹列,改由 useListPage 统一注入 6 列 ----- +jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts + +-- author:GHT---date:20260610--for: 【混炼示方】页脚起草人/变更人展示姓名 ----- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java +jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue + +-- author:GHT---date:20260610--for: 【混炼示方】TCU温度条件新增是否附加/重量字段 ----- +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_146__mes_xsl_mixing_spec_tcu_attach.sql +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java +jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/advice/ApprovalTraceResponseAdvice.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/advice/ApprovalTraceResponseAdvice.java index f86b83f..10af8d3 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/advice/ApprovalTraceResponseAdvice.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/advice/ApprovalTraceResponseAdvice.java @@ -84,6 +84,9 @@ public class ApprovalTraceResponseAdvice implements ResponseBodyAdvice { } String path = extractServletPath(request); CacheEntry entry = resolveEntry(path); + if (entry == null) { + entry = resolveEntryByQueryById(path); + } if (entry == null) { return body; } @@ -93,11 +96,15 @@ public class ApprovalTraceResponseAdvice implements ResponseBodyAdvice { List records = null; IPage page = null; + boolean singleEntity = false; if (data instanceof IPage) { page = (IPage) data; records = page.getRecords(); } else if (data instanceof List) { records = (List) data; + } else if (data != null && extractId(data) != null) { + records = Collections.singletonList(data); + singleEntity = true; } if (records == null || records.isEmpty()) { @@ -118,6 +125,8 @@ public class ApprovalTraceResponseAdvice implements ResponseBodyAdvice { if (page != null) { ((Page) page).setRecords(enriched); + } else if (singleEntity) { + result.setResult(enriched.get(0)); } else { result.setResult(enriched); } @@ -141,6 +150,37 @@ public class ApprovalTraceResponseAdvice implements ResponseBodyAdvice { return pathToEntryCache.get(path); } + /** queryById 与 list 同模块时,按 list 路径匹配注册中心配置 */ + private CacheEntry resolveEntryByQueryById(String path) { + if (oConvertUtils.isEmpty(path) || !path.endsWith("/queryById")) { + return null; + } + ensureCacheLoaded(); + String listPath = path.substring(0, path.length() - "/queryById".length()) + "/list"; + return pathToEntryCache.get(listPath); + } + + private String extractId(Object r) { + if (r == null) { + return null; + } + Object id = null; + if (r instanceof Map) { + id = ((Map) r).get("id"); + } else { + try { + id = r.getClass().getMethod("getId").invoke(r); + } catch (Exception ignored) { + // 无 getId 方法 + } + } + if (id == null) { + return null; + } + String idStr = String.valueOf(id); + return oConvertUtils.isNotEmpty(idStr) ? idStr : null; + } + private void ensureCacheLoaded() { long now = System.currentTimeMillis(); if (now - cacheLoadTime > CACHE_TTL_MS) { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslBizDocRegistryController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslBizDocRegistryController.java index eafc6d8..74c8805 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslBizDocRegistryController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslBizDocRegistryController.java @@ -14,8 +14,14 @@ import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry; import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + /** * 审批注册中心 * @@ -30,6 +36,8 @@ public class MesXslBizDocRegistryController extends JeecgController>> listDbTables( + @RequestParam(name = "keyword", required = false) String keyword) { + StringBuilder sql = new StringBuilder( + "SELECT TABLE_NAME tableName, IFNULL(TABLE_COMMENT,'') tableComment " + + "FROM information_schema.tables " + + "WHERE table_schema = (SELECT DATABASE()) AND table_type = 'BASE TABLE' "); + List args = new ArrayList<>(); + if (keyword != null && !keyword.isBlank()) { + String like = "%" + keyword.trim() + "%"; + sql.append("AND (TABLE_NAME LIKE ? OR TABLE_COMMENT LIKE ?) "); + args.add(like); + args.add(like); + } + sql.append("ORDER BY TABLE_NAME"); + List> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray()); + List> options = new ArrayList<>(rows.size()); + for (Map row : rows) { + String tableName = String.valueOf(row.get("tableName")); + String comment = String.valueOf(row.get("tableComment")); + Map opt = new LinkedHashMap<>(); + opt.put("value", tableName); + opt.put("comment", comment); + opt.put("label", comment.isBlank() ? tableName : tableName + "(" + comment + ")"); + options.add(opt); + } + return Result.OK(options); + } + //update-end---author:GHT ---date:20260610 for:【审批注册中心】物理表名下拉选择,查询当前库表清单----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java index 4cb8565..f103d8f 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java @@ -160,6 +160,25 @@ public class MesXslIntegrationPlanController extends JeecgController> generateForNode(@RequestBody Map body) { + if (body == null) { + return Result.error("请求体不能为空"); + } + String sourceTable = body.get("sourceTable") != null ? String.valueOf(body.get("sourceTable")) : null; + String flowId = body.get("flowId") != null ? String.valueOf(body.get("flowId")) : null; + String nodeId = body.get("nodeId") != null ? String.valueOf(body.get("nodeId")) : null; + String stageKey = body.get("stageKey") != null ? String.valueOf(body.get("stageKey")) : null; + String flowConfig = body.get("flowConfig") != null ? String.valueOf(body.get("flowConfig")) : null; + boolean overwriteDraft = Boolean.TRUE.equals(body.get("overwriteDraft")); + return planGenerator.generateForNode(sourceTable, flowId, nodeId, stageKey, flowConfig, overwriteDraft); + } + //update-end---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案----------- //update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按审批流程节点生成默认集成方案----------- //update-begin---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增表字段元数据查询接口(可视化配置向导用)----------- diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationActionConfigHelper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationActionConfigHelper.java index 271f6c3..bc838c9 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationActionConfigHelper.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationActionConfigHelper.java @@ -106,4 +106,78 @@ public final class IntegrationActionConfigHelper { return null; } //update-end---author:GHT ---date:20260609 for:【驳回回退】targetStage 按 containsKey 解析字典键值(含 0)----------- + + //update-begin---author:GHT ---date:20260610 for:【关联表痕迹同步】解析 SQL_UPDATE 动作是否同步目标表痕迹----------- + /** 关联表动作是否开启痕迹同步(actionConfig.syncTrace) */ + public static boolean resolveSyncTrace(MesXslIntegrationAction action) { + if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) { + return false; + } + try { + JSONObject cfg = JSONObject.parseObject(action.getActionConfig()); + return cfg.getBooleanValue("syncTrace"); + } catch (Exception ignored) { + return false; + } + } + + /** 可视化配置中的目标表名 */ + public static String resolveTargetTable(MesXslIntegrationAction action) { + if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) { + return null; + } + try { + JSONObject cfg = JSONObject.parseObject(action.getActionConfig()); + String table = cfg.getString("targetTable"); + return oConvertUtils.isEmpty(table) ? null : table.trim(); + } catch (Exception ignored) { + return null; + } + } + + /** 关联条件:触发表字段 */ + public static String resolveLinkSourceField(MesXslIntegrationAction action) { + return resolveLinkField(action, "sourceField"); + } + + /** 关联条件:目标表字段 */ + public static String resolveLinkTargetField(MesXslIntegrationAction action) { + return resolveLinkField(action, "targetField"); + } + + /** 状态修改动作的新状态值(驳回回退时作为痕迹清空目标) */ + public static String resolveStatusConfigNewValue(MesXslIntegrationAction action) { + if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) { + return null; + } + try { + JSONObject cfg = JSONObject.parseObject(action.getActionConfig()); + JSONObject statusConfig = cfg.getJSONObject("statusConfig"); + if (statusConfig == null) { + return null; + } + String v = statusConfig.getString("newValue"); + return oConvertUtils.isEmpty(v) ? null : v.trim(); + } catch (Exception ignored) { + return null; + } + } + + private static String resolveLinkField(MesXslIntegrationAction action, String fieldKey) { + if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) { + return null; + } + try { + JSONObject cfg = JSONObject.parseObject(action.getActionConfig()); + JSONObject link = cfg.getJSONObject("linkCondition"); + if (link == null) { + return null; + } + String v = link.getString(fieldKey); + return oConvertUtils.isEmpty(v) ? null : v.trim(); + } catch (Exception ignored) { + return null; + } + } + //update-end---author:GHT ---date:20260610 for:【关联表痕迹同步】解析 SQL_UPDATE 动作是否同步目标表痕迹----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/RelatedTableTraceSyncHelper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/RelatedTableTraceSyncHelper.java new file mode 100644 index 0000000..1637cab --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/RelatedTableTraceSyncHelper.java @@ -0,0 +1,178 @@ +package org.jeecg.modules.xslmes.approval.integration.engine; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext; +import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction; +import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan; +import org.jeecg.modules.xslmes.approval.integration.service.IApprovalTraceSyncService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +/** + * 关联表 SQL_UPDATE 动作执行后的审批痕迹同步/清空。 + */ +@Slf4j +@Component +public class RelatedTableTraceSyncHelper { + + @Autowired + private IApprovalTraceSyncService approvalTraceSyncService; + + @Autowired + private JdbcTemplate jdbcTemplate; + + //update-begin---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL_UPDATE 成功后按动作配置同步目标表痕迹----------- + /** + * SQL_UPDATE 成功后,按动作配置将主表审批人/时间写入或清空关联表痕迹。 + * + * @param affectedRows SQL 实际影响行数,零行时跳过 + */ + public void syncAfterSqlUpdate(IntegrationContext ctx, MesXslIntegrationAction action, int affectedRows) { + if (ctx == null || action == null || !IntegrationActionConfigHelper.resolveSyncTrace(action)) { + return; + } + if (affectedRows <= 0) { + log.info("[关联表痕迹] 跳过:SQL 零行更新 action={}", action.getActionName()); + return; + } + String targetTable = IntegrationActionConfigHelper.resolveTargetTable(action); + String sourceField = IntegrationActionConfigHelper.resolveLinkSourceField(action); + String targetField = IntegrationActionConfigHelper.resolveLinkTargetField(action); + if (oConvertUtils.isEmpty(targetTable) || oConvertUtils.isEmpty(sourceField) || oConvertUtils.isEmpty(targetField)) { + log.warn("[关联表痕迹] 跳过:未配置目标表或关联条件 action={}", action.getActionName()); + return; + } + RegistryStageFieldHelper.assertIdentifier(targetTable); + RegistryStageFieldHelper.assertIdentifier(sourceField); + RegistryStageFieldHelper.assertIdentifier(targetField); + + String linkValue = resolveLinkValue(ctx, sourceField); + if (oConvertUtils.isEmpty(linkValue)) { + log.warn("[关联表痕迹] 跳过:触发表关联字段为空 action={} sourceField={}", action.getActionName(), sourceField); + return; + } + List targetIds = listTargetBizIds(targetTable, targetField, linkValue); + if (targetIds.isEmpty()) { + log.warn("[关联表痕迹] 跳过:未匹配到目标表记录 action={} table={} {}={}", + action.getActionName(), targetTable, targetField, linkValue); + return; + } + + if (isRejectLikePhase(ctx)) { + syncRevertTrace(ctx, action, targetTable, targetIds); + } else { + syncPassTrace(ctx, action, targetTable, targetIds); + } + } + + private void syncPassTrace(IntegrationContext ctx, MesXslIntegrationAction action, + String targetTable, List targetIds) { + String stage = resolveTraceStage(ctx, action); + if (oConvertUtils.isEmpty(stage)) { + log.warn("[关联表痕迹] 跳过:无法解析审批环节 action={}", action.getActionName()); + return; + } + String stageErr = approvalTraceSyncService.checkStageAllowed(targetTable, stage); + if (stageErr != null) { + log.warn("[关联表痕迹] 跳过:{} action={}", stageErr, action.getActionName()); + return; + } + String operator = resolveOperator(ctx); + Date operatorTime = resolveOperatorTime(ctx); + for (String targetId : targetIds) { + approvalTraceSyncService.syncStage(targetTable, targetId, stage, operator, operatorTime); + } + log.info("[关联表痕迹] 写入完成 action={} table={} stage={} operator={} count={}", + action.getActionName(), targetTable, stage, operator, targetIds.size()); + } + + private void syncRevertTrace(IntegrationContext ctx, MesXslIntegrationAction action, + String targetTable, List targetIds) { + // 驳回场景取状态修改动作的「新状态」,与 SQL SET 值一致,用于痕迹回退粒度对齐 + String revertTarget = IntegrationActionConfigHelper.resolveStatusConfigNewValue(action); + if (oConvertUtils.isEmpty(revertTarget)) { + log.warn("[关联表痕迹] 驳回清空跳过:状态修改未配置「新状态」action={}", action.getActionName()); + return; + } + for (String targetId : targetIds) { + approvalTraceSyncService.revertToStage(targetTable, targetId, revertTarget); + } + log.info("[关联表痕迹] 驳回清空完成 action={} table={} targetStage={} count={}", + action.getActionName(), targetTable, revertTarget, targetIds.size()); + } + + private boolean isRejectLikePhase(IntegrationContext ctx) { + if (ctx.getTriggerPhase() == TriggerPhase.ON_REJECT) { + return true; + } + ApprovalCallbackContext ac = ctx.getApprovalCtx(); + if (ac == null || ac.getAction() == null) { + return false; + } + return ac.getAction() == ApprovalCallbackContext.Action.REJECTED + || ac.getAction() == ApprovalCallbackContext.Action.CANCELLED; + } + + private String resolveTraceStage(IntegrationContext ctx, MesXslIntegrationAction action) { + ApprovalCallbackContext ac = ctx.getApprovalCtx(); + if (ac != null && oConvertUtils.isNotEmpty(ac.getStageKey())) { + return ac.getStageKey().trim(); + } + MesXslIntegrationPlan plan = ctx.getPlan(); + if (plan != null && oConvertUtils.isNotEmpty(plan.getTriggerStage())) { + return plan.getTriggerStage().trim(); + } + return IntegrationActionConfigHelper.resolveStage(action, plan); + } + + private String resolveOperator(IntegrationContext ctx) { + ApprovalCallbackContext ac = ctx.getApprovalCtx(); + if (ac != null && oConvertUtils.isNotEmpty(ac.getOperatorName())) { + return ac.getOperatorName(); + } + if (ac != null && oConvertUtils.isNotEmpty(ac.getOperatorUsername())) { + return ac.getOperatorUsername(); + } + return "系统"; + } + + private Date resolveOperatorTime(IntegrationContext ctx) { + ApprovalCallbackContext ac = ctx.getApprovalCtx(); + if (ac != null && ac.getOperatorTime() != null) { + return ac.getOperatorTime(); + } + return new Date(); + } + + private String resolveLinkValue(IntegrationContext ctx, String sourceField) { + if ("id".equalsIgnoreCase(sourceField)) { + return ctx.getSourceBizId(); + } + Map rec = ctx.getSourceRecord(); + if (rec == null || !rec.containsKey(sourceField)) { + return null; + } + Object v = rec.get(sourceField); + return v == null ? null : String.valueOf(v).trim(); + } + + private List listTargetBizIds(String targetTable, String targetField, String linkValue) { + String sql = "SELECT id FROM `" + targetTable + "` WHERE `" + targetField + "` = ?"; + List> rows = jdbcTemplate.queryForList(sql, linkValue); + List ids = new ArrayList<>(); + for (Map row : rows) { + Object id = row.get("id"); + if (id != null && oConvertUtils.isNotEmpty(String.valueOf(id))) { + ids.add(String.valueOf(id)); + } + } + return ids; + } + //update-end---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL_UPDATE 成功后按动作配置同步目标表痕迹----------- +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/executor/SqlUpdateActionExecutor.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/executor/SqlUpdateActionExecutor.java index e43ab15..e6d4c11 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/executor/SqlUpdateActionExecutor.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/executor/SqlUpdateActionExecutor.java @@ -3,6 +3,7 @@ package org.jeecg.modules.xslmes.approval.integration.engine.executor; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationContext; +import org.jeecg.modules.xslmes.approval.integration.engine.RelatedTableTraceSyncHelper; import org.jeecg.modules.xslmes.approval.integration.engine.VariableResolver; import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +33,9 @@ public class SqlUpdateActionExecutor implements IIntegrationActionExecutor { @Autowired private JdbcTemplate jdbcTemplate; + @Autowired + private RelatedTableTraceSyncHelper relatedTableTraceSyncHelper; + @Override public String supportActionType() { return "SQL_UPDATE"; @@ -61,6 +65,9 @@ public class SqlUpdateActionExecutor implements IIntegrationActionExecutor { } //update-end---author:GHT ---date:20260608 for:【审核集成】SQL_UPDATE零行时输出可诊断提示----------- log.info("[集成引擎][SQL_UPDATE] 完成 action={} {}", action.getActionName(), result); + //update-begin---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL 成功后按动作配置写入/清空目标表痕迹----------- + relatedTableTraceSyncHelper.syncAfterSqlUpdate(ctx, action, affected); + //update-end---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL 成功后按动作配置写入/清空目标表痕迹----------- return result; } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java index 8927175..c8992b3 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java @@ -143,6 +143,121 @@ public class IntegrationPlanGenerator { result.put("planCodes", planCodes); return Result.OK("生成完成:新增 " + created + " 个方案,跳过 " + skipped + " 个已存在方案", result); } + + //update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案并返回方案ID----------- + /** + * 为流程设计器中当前审批节点生成(或复用)集成方案,便于生成后直接配置动作。 + */ + @Transactional(rollbackFor = Exception.class) + public Result> generateForNode(String sourceTable, String flowId, String nodeId, + String stageKey, String flowConfigJson, boolean overwriteDraft) { + if (oConvertUtils.isEmpty(sourceTable) || oConvertUtils.isEmpty(flowId) || oConvertUtils.isEmpty(nodeId)) { + return Result.error("缺少业务表、审批流或节点信息"); + } + if (oConvertUtils.isEmpty(stageKey)) { + return Result.error("请先在节点上绑定审批环节(校对/审核/批准)"); + } + MesXslBizDocRegistry registry = registryService.findActiveByTableName(sourceTable); + if (registry == null) { + return Result.error("业务表未在审批注册中心启用: " + sourceTable); + } + MesXslApprovalFlow flow = resolveFlow(sourceTable, flowId); + String configJson = oConvertUtils.isNotEmpty(flowConfigJson) ? flowConfigJson : flow.getFlowConfig(); + if (oConvertUtils.isEmpty(configJson)) { + return Result.error("审批流程未设计,请先保存或完成流程节点配置"); + } + + List enabledStages = orderedEnabledStages(registry); + List flowNodes = parseApproverNodes(configJson); + boolean nodeFound = flowNodes.stream().anyMatch(n -> nodeId.equals(n.nodeId)); + if (!nodeFound) { + return Result.error("当前节点不在流程配置中,请确认流程设计已包含该节点"); + } + + List statusChain = loadStatusChain(registry); + String initialStatus = resolveInitialStatus(statusChain, enabledStages); + Map overrides = Map.of(nodeId, stageKey.trim()); + List bindings = bindAllFlowNodes(flowNodes, registry, enabledStages, statusChain, initialStatus, overrides); + StageBinding binding = bindings.stream() + .filter(b -> nodeId.equals(b.nodeId)) + .findFirst() + .orElse(null); + if (binding == null || !binding.stageConfigured) { + String reason = binding != null ? binding.unconfiguredReason : "节点环节未配置"; + return Result.error(reason); + } + + String codePrefix = planCodePrefix(registry); + String displayName = oConvertUtils.isNotEmpty(registry.getDisplayName()) ? registry.getDisplayName() : sourceTable; + String planCode = codePrefix + "_reg_" + binding.stage; + String phase = "onNodeApprove"; + + MesXslIntegrationPlan existing = planService.lambdaQuery() + .eq(MesXslIntegrationPlan::getPlanCode, planCode) + .one(); + if (existing != null) { + if ("0".equals(existing.getStatus()) && overwriteDraft) { + actionService.removeByPlanId(existing.getId()); + planService.removeById(existing.getId()); + } else { + return buildNodeGenerateResult(existing, false, phase, binding); + } + } + + Map actionConfig = new LinkedHashMap<>(); + actionConfig.put("visualType", "REGISTRY_STAGE_SYNC"); + actionConfig.put("stage", binding.stage); + actionConfig.put("expectedFrom", binding.expectedFrom); + if (oConvertUtils.isNotEmpty(binding.statusAfter)) { + actionConfig.put("statusAfter", binding.statusAfter); + } + + MesXslIntegrationPlan plan = new MesXslIntegrationPlan(); + plan.setPlanCode(planCode); + plan.setPlanName(displayName + "-" + binding.stageLabel + "通过(流程生成)"); + plan.setSourceTable(sourceTable); + plan.setRegistryId(registry.getId()); + plan.setTriggerPhase(phase); + plan.setTriggerStage(binding.stage); + plan.setExecMode("async"); + plan.setStatus("0"); + plan.setRemark("按审批流程节点「" + binding.nodeName + "」自动生成"); + Result validate = planService.normalizeAndValidate(plan); + if (!validate.isSuccess()) { + return Result.error("方案校验失败: " + validate.getMessage()); + } + planService.save(plan); + + MesXslIntegrationAction action = new MesXslIntegrationAction(); + action.setPlanId(plan.getId()); + action.setActionName(binding.stageLabel + "环节同步"); + action.setActionType("REGISTRY_STAGE_SYNC"); + action.setActionConfig(JSON.toJSONString(actionConfig)); + action.setExecOrder(1); + action.setOnFail("stop"); + action.setEnabled(1); + actionService.save(action); + + return buildNodeGenerateResult(plan, true, phase, binding); + } + + private Result> buildNodeGenerateResult(MesXslIntegrationPlan plan, boolean created, + String phase, StageBinding binding) { + Map out = new LinkedHashMap<>(); + out.put("planId", plan.getId()); + out.put("planCode", plan.getPlanCode()); + out.put("planName", plan.getPlanName()); + out.put("sourceTable", plan.getSourceTable()); + out.put("triggerPhase", phase); + out.put("triggerStage", plan.getTriggerStage()); + out.put("status", plan.getStatus()); + out.put("created", created); + out.put("nodeName", binding.nodeName); + out.put("stageLabel", binding.stageLabel); + String msg = created ? "已生成集成方案,请配置动作并发布" : "该环节已有集成方案,可直接配置动作"; + return Result.OK(msg, out); + } + //update-end---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案并返回方案ID----------- //update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按流程生成默认集成方案与动作----------- private Map buildPreview(String sourceTable, String flowId, Map stageOverrides) { @@ -385,7 +500,10 @@ public class IntegrationPlanGenerator { return; } if ("approver".equals(node.getString("type"))) { - JSONObject props = node.getJSONObject("properties"); + JSONObject props = node.getJSONObject("props"); + if (props == null) { + props = node.getJSONObject("properties"); + } if (props == null) { props = new JSONObject(); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java index 36c5952..eaf2d23 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java @@ -36,7 +36,9 @@ import org.jeecg.modules.system.entity.SysThirdAccount; import org.jeecg.modules.system.service.ISysThirdAccountService; import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl; import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl; +import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind; import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService; +import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingTplBindService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; @@ -80,6 +82,9 @@ public class MesXslDingProcessTplController extends JeecgController edit(@RequestBody MesXslDingProcessTpl mesXslDingProcessTpl) { + if (oConvertUtils.isEmpty(mesXslDingProcessTpl.getId())) { + return Result.error("缺少模板ID"); + } + //update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】MES改模板名同步到钉钉----------- + MesXslDingProcessTpl old = mesXslDingProcessTplService.getById(mesXslDingProcessTpl.getId()); mesXslDingProcessTplService.updateById(mesXslDingProcessTpl); - return Result.OK("编辑成功!"); + String msg = "编辑成功!"; + if (old != null && oConvertUtils.isNotEmpty(mesXslDingProcessTpl.getTplName())) { + String newName = mesXslDingProcessTpl.getTplName().trim(); + String oldName = oConvertUtils.isEmpty(old.getTplName()) ? "" : old.getTplName().trim(); + if (!newName.equals(oldName)) { + refreshBindTemplateName(mesXslDingProcessTpl.getId(), newName); + String processCode = oConvertUtils.isNotEmpty(mesXslDingProcessTpl.getProcessCode()) + ? mesXslDingProcessTpl.getProcessCode() : old.getProcessCode(); + if (oConvertUtils.isNotEmpty(processCode)) { + MesXslDingProcessTpl latest = mesXslDingProcessTplService.getById(mesXslDingProcessTpl.getId()); + if (latest != null && oConvertUtils.isEmpty(latest.getProcessCode())) { + latest.setProcessCode(processCode); + } + Result pushResult = pushTemplateMetaToDingtalk(latest); + if (!pushResult.isSuccess()) { + return Result.OK("编辑成功,但同步钉钉模板名称失败:" + pushResult.getMessage()); + } + msg = "编辑成功,已同步钉钉模板名称"; + } + } + } + return Result.OK(msg); + //update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】MES改模板名同步到钉钉----------- } + //update-begin---author:GHT ---date:20260610 for:【钉钉审批模板】操作列停用/启用,停用后业务页不显示钉钉审批按钮----------- + @AutoLog(value = "钉钉审批模板配置-切换启用状态") + @Operation(summary = "钉钉审批模板配置-切换启用/停用") + @RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:edit") + @PostMapping(value = "/toggleStatus") + public Result toggleStatus(@RequestParam(name = "id") String id) { + if (oConvertUtils.isEmpty(id)) { + return Result.error("缺少模板ID"); + } + MesXslDingProcessTpl tpl = mesXslDingProcessTplService.getById(id); + if (tpl == null) { + return Result.error("未找到对应模板"); + } + String newStatus = "1".equals(tpl.getStatus()) ? "0" : "1"; + MesXslDingProcessTpl update = new MesXslDingProcessTpl(); + update.setId(id); + update.setStatus(newStatus); + mesXslDingProcessTplService.updateById(update); + return Result.OK("1".equals(newStatus) ? "已启用" : "已停用"); + } + //update-end---author:GHT ---date:20260610 for:【钉钉审批模板】操作列停用/启用,停用后业务页不显示钉钉审批按钮----------- + @AutoLog(value = "钉钉审批模板配置-通过id删除") @Operation(summary = "钉钉审批模板配置-通过id删除") @RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:delete") @@ -245,11 +299,15 @@ public class MesXslDingProcessTplController extends JeecgController> list = new ArrayList<>(); + Map dingNameByCode = new LinkedHashMap<>(); if (processList != null) { for (int i = 0; i < processList.size(); i++) { JSONObject item = processList.getJSONObject(i); String code = item.getString("process_code"); String name = oConvertUtils.getString(item.getString("name"), "").trim(); + if (oConvertUtils.isNotEmpty(code) && oConvertUtils.isNotEmpty(name)) { + dingNameByCode.put(code, name); + } Map row = new LinkedHashMap<>(); row.put("processCode", code); row.put("name", name); @@ -263,6 +321,9 @@ public class MesXslDingProcessTplController extends JeecgController components = buildFormComponentList(tpl.getFormFields()); - if (components.isEmpty()) return Result.error("请先在字段映射中配置至少一个字段"); - - // 带 processCode → 钉钉更新已有模板(官方文档:POST同一接口,有processCode=更新) - DingFormUpdateRequest req = new DingFormUpdateRequest() - .setProcessCode(tpl.getProcessCode()) - .setName(tpl.getTplName()) - .setDescription(oConvertUtils.isNotEmpty(tpl.getRemark()) ? tpl.getRemark() : "") - .setFormComponents(components); - - try { - String reqJson = JSON.toJSONString(req); - log.info("【钉钉更新模板】processCode={} 请求体: {}", tpl.getProcessCode(), reqJson); - String respBody = callDingApi("POST", "https://api.dingtalk.com/v1.0/workflow/forms", accessToken, reqJson); - JSONObject resp = JSONObject.parseObject(respBody); - log.info("【钉钉更新模板】响应: {}", respBody); - - if (resp.containsKey("code")) { - return Result.error("钉钉返回错误: " + resp.getString("message") + " (code=" + resp.getString("code") + ")"); - } - - Map result = new LinkedHashMap<>(); - result.put("processCode", tpl.getProcessCode()); - result.put("rawResponse", resp); - return Result.OK("钉钉审批模板更新成功", result); - } catch (Exception e) { - log.error("更新钉钉模板异常", e); - return Result.error("请求异常: " + e.getMessage()); + //update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】更新钉钉模板复用统一推送逻辑----------- + Result pushResult = pushTemplateMetaToDingtalk(tpl); + if (!pushResult.isSuccess()) { + return Result.error(pushResult.getMessage()); } + Map result = new LinkedHashMap<>(); + result.put("processCode", tpl.getProcessCode()); + return Result.OK("钉钉审批模板更新成功", result); + //update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】更新钉钉模板复用统一推送逻辑----------- } //update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】创建/更新钉钉审批模板----- @@ -851,6 +895,184 @@ public class MesXslDingProcessTplController extends JeecgController().eq("template_id", templateId)); + } + + /** 从钉钉拉取模板名称并回写本地(getTemplateDetail / 设计器打开时) */ + private void mergeDingTemplateName(Map detail, MesXslDingProcessTpl tpl, + String accessToken, JSONObject schemaRoot, JSONObject schemaResp) { + if (tpl == null || oConvertUtils.isEmpty(tpl.getProcessCode())) { + detail.put("dingNameSynced", false); + return; + } + String dingName = extractDingTemplateName(schemaRoot); + if (oConvertUtils.isEmpty(dingName)) { + dingName = extractDingTemplateName(schemaResp); + } + if (oConvertUtils.isEmpty(dingName)) { + dingName = fetchDingTemplateNameByProcessCode(tpl.getProcessCode(), accessToken); + } + detail.put("dingTplName", oConvertUtils.isNotEmpty(dingName) ? dingName : tpl.getTplName()); + String localName = oConvertUtils.isEmpty(tpl.getTplName()) ? "" : tpl.getTplName().trim(); + if (oConvertUtils.isNotEmpty(dingName) && !dingName.equals(localName)) { + applyLocalTplName(tpl.getId(), dingName); + detail.put("tplName", dingName); + detail.put("dingNameSynced", true); + } else { + detail.put("dingNameSynced", false); + } + } + + /** 按 processCode 批量回写本地模板名称(从钉钉同步列表) */ + private void syncLocalTplNamesFromDingMap(Map dingNameByCode) { + if (dingNameByCode == null || dingNameByCode.isEmpty()) { + return; + } + for (MesXslDingProcessTpl local : mesXslDingProcessTplService.list()) { + if (oConvertUtils.isEmpty(local.getProcessCode())) { + continue; + } + String dingName = dingNameByCode.get(local.getProcessCode()); + if (oConvertUtils.isEmpty(dingName)) { + continue; + } + String localName = oConvertUtils.isEmpty(local.getTplName()) ? "" : local.getTplName().trim(); + if (!dingName.equals(localName)) { + applyLocalTplName(local.getId(), dingName); + log.info("【模板名称同步】processCode={} 本地「{}」→ 钉钉「{}」", local.getProcessCode(), localName, dingName); + } + } + } + + /** 从钉钉 JSON 响应中提取模板名称 */ + private String extractDingTemplateName(JSONObject obj) { + if (obj == null) { + return null; + } + for (String key : Arrays.asList("name", "processName", "flowTitle", "title", "templateName")) { + String val = obj.getString(key); + if (oConvertUtils.isNotEmpty(val)) { + return val.trim(); + } + } + return null; + } + + /** 通过可见审批列表按 processCode 查找钉钉模板名称 */ + private String fetchDingTemplateNameByProcessCode(String processCode, String accessToken) { + if (oConvertUtils.isEmpty(processCode) || oConvertUtils.isEmpty(accessToken)) { + return null; + } + try { + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), CommonConstant.TENANT_ID_DEFAULT_VALUE); + String dtUserId = resolveDtUserId(loginUser.getUsername(), loginUser.getPhone(), accessToken, tenantId); + if (oConvertUtils.isEmpty(dtUserId)) { + return null; + } + Map nameMap = fetchVisibleProcessNameMap(dtUserId, accessToken); + return nameMap.get(processCode); + } catch (Exception e) { + log.warn("按 processCode 拉取钉钉模板名称失败 processCode={}: {}", processCode, e.getMessage()); + return null; + } + } + + /** 拉取当前用户可见的钉钉审批模板 processCode → name 映射 */ + private Map fetchVisibleProcessNameMap(String dtUserId, String accessToken) throws Exception { + Map map = new LinkedHashMap<>(); + String url = "https://oapi.dingtalk.com/topapi/process/listbyuserid?access_token=" + accessToken; + JSONObject reqBody = new JSONObject(); + reqBody.put("userid", dtUserId); + reqBody.put("offset", 0); + reqBody.put("size", 100); + HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + HttpRequest httpReq = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(reqBody.toJSONString())) + .timeout(Duration.ofSeconds(10)) + .build(); + String respBody = httpClient.send(httpReq, HttpResponse.BodyHandlers.ofString()).body(); + JSONObject ddResp = JSONObject.parseObject(respBody); + if (ddResp.getIntValue("errcode") != 0) { + return map; + } + JSONObject result = ddResp.getJSONObject("result"); + JSONArray processList = result == null ? null : result.getJSONArray("process_list"); + if (processList == null) { + return map; + } + for (int i = 0; i < processList.size(); i++) { + JSONObject item = processList.getJSONObject(i); + String code = item.getString("process_code"); + String name = oConvertUtils.getString(item.getString("name"), "").trim(); + if (oConvertUtils.isNotEmpty(code) && oConvertUtils.isNotEmpty(name)) { + map.put(code, name); + } + } + return map; + } + + /** 将本地模板元数据(名称/描述/表单)推送到钉钉 */ + private Result pushTemplateMetaToDingtalk(MesXslDingProcessTpl tpl) { + if (tpl == null) { + return Result.error("模板不存在"); + } + if (oConvertUtils.isEmpty(tpl.getProcessCode())) { + return Result.error("该记录尚无 processCode"); + } + String accessToken = dingtalkService.getAccessToken(); + if (oConvertUtils.isEmpty(accessToken)) { + return Result.error("AccessToken 获取失败"); + } + List components = buildFormComponentList(tpl.getFormFields()); + if (components.isEmpty()) { + return Result.error("请先在表单设计中配置至少一个字段后再同步到钉钉"); + } + DingFormUpdateRequest req = new DingFormUpdateRequest() + .setProcessCode(tpl.getProcessCode()) + .setName(tpl.getTplName()) + .setDescription(oConvertUtils.isNotEmpty(tpl.getRemark()) ? tpl.getRemark() : "") + .setFormComponents(components); + try { + String reqJson = JSON.toJSONString(req); + log.info("【钉钉更新模板】processCode={} name={} 请求体: {}", tpl.getProcessCode(), tpl.getTplName(), reqJson); + String respBody = callDingApi("POST", "https://api.dingtalk.com/v1.0/workflow/forms", accessToken, reqJson); + JSONObject resp = JSONObject.parseObject(respBody); + log.info("【钉钉更新模板】响应: {}", respBody); + if (resp.containsKey("code")) { + return Result.error("钉钉返回错误: " + resp.getString("message") + " (code=" + resp.getString("code") + ")"); + } + return Result.OK("同步成功"); + } catch (Exception e) { + log.error("推送钉钉模板元数据异常 processCode={}", tpl.getProcessCode(), e); + return Result.error("请求异常: " + e.getMessage()); + } + } + //update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】模板名称 MES↔钉钉 双向同步----------- + /** 统一 HTTP 调用钉钉 v1.0 接口 */ private String callDingApi(String method, String url, String accessToken, String jsonBody) throws Exception { HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); @@ -1103,6 +1325,9 @@ public class MesXslDingProcessTplController extends JeecgController cls = PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim()); - if (cls == null) { - return Result.OK(Collections.emptyList()); - } - return Result.OK(PrintBizEntityFieldIntrospector.listFields(cls)); + return Result.OK(listMainFieldsEnriched(bizCode.trim())); } @Operation(summary = "主实体上的明细槽位(供 TableField 绑定明细集合)") @@ -177,20 +168,53 @@ public class MesXslDingTplBindController { return Result.error("bizCode 与 detailProperty 不能为空"); } String code = bizCode.trim(); + String prop = detailProperty.trim(); + String kind = slotKind.trim(); + List fields; if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) { - return Result.OK(fieldCatalogProvider.listPrefixedDetailFields(code, detailProperty.trim(), slotKind.trim())); + fields = fieldCatalogProvider.listPrefixedDetailFields(code, prop, kind); + } else { + Class cls = resolveEntityClass(code); + if (cls == null) { + return Result.OK(Collections.emptyList()); + } + fields = PrintBizDetailPropertyScanner.listPrefixedDetailFields(cls, prop, kind); } - PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(code); - if (vo == null || StringUtils.isBlank(vo.getDescription())) { - return Result.OK(Collections.emptyList()); + Class itemCls = resolveDetailItemClass(code, prop, kind); + if (itemCls != null && fields != null && !fields.isEmpty()) { + PrintBizEntityFieldIntrospector.enrichDictMeta(fields, itemCls, prop); } - Class cls = PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim()); - if (cls == null) { - return Result.OK(Collections.emptyList()); - } - return Result.OK(PrintBizDetailPropertyScanner.listPrefixedDetailFields(cls, detailProperty.trim(), slotKind.trim())); + return Result.OK(fields != null ? fields : Collections.emptyList()); } + //update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】批量解析字段取值(原值/显示文本)----------- + @Operation(summary = "批量解析绑定字段取值(字典/表字典显示文本)") + @PostMapping("/resolveFieldValues") + @RequiresPermissions("xslmes:mesXslDingTplBind:list") + public Result> resolveFieldValues(@RequestBody ResolveFieldValuesRequest req) { + if (req == null || StringUtils.isBlank(req.getBizCode())) { + return Result.error("bizCode 不能为空"); + } + if (req.getRowData() == null || req.getItems() == null || req.getItems().isEmpty()) { + return Result.OK(Collections.emptyMap()); + } + String code = req.getBizCode().trim(); + Map metaMap = buildFieldMetaMap(code); + Map out = new LinkedHashMap<>(); + for (ResolveFieldItem item : req.getItems()) { + if (item == null || StringUtils.isBlank(item.getMapKey()) || StringUtils.isBlank(item.getBizField())) { + continue; + } + PrintBizFieldItemVO meta = metaMap.get(item.getBizField().trim()); + Object val = + fieldValueResolver.resolveValue( + req.getRowData(), item.getBizField().trim(), item.getValueMode(), meta); + out.put(item.getMapKey(), val); + } + return Result.OK(out); + } + //update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】批量解析字段取值(原值/显示文本)----------- + // ═══════════════════════ 按路由检测绑定(全局悬浮按钮使用) ═══════════════════════ /** @@ -216,6 +240,15 @@ public class MesXslDingTplBindController { return Result.OK(null); } MesXslDingTplBind bind = bindService.getByBizCode(ids.get(0)); + if (bind == null || StringUtils.isBlank(bind.getTemplateId())) { + return Result.OK(null); + } + //update-begin---author:GHT ---date:20260610 for:【钉钉审批模板】模板停用时业务页不返回绑定,隐藏钉钉审批按钮----------- + MesXslDingProcessTpl tpl = tplService.getById(bind.getTemplateId()); + if (tpl == null || !"1".equals(tpl.getStatus())) { + return Result.OK(null); + } + //update-end---author:GHT ---date:20260610 for:【钉钉审批模板】模板停用时业务页不返回绑定,隐藏钉钉审批按钮----------- return Result.OK(bind); } @@ -247,6 +280,9 @@ public class MesXslDingTplBindController { if (tpl == null) { return Result.error("选择的钉钉审批模板不存在"); } + if (!"1".equals(tpl.getStatus())) { + return Result.error("所选钉钉审批模板已停用,请先启用或更换其他模板"); + } MesXslDingTplBind existing = bindService.getByBizCode(req.getBizCode().trim()); if (existing != null) { // 更新已有绑定 @@ -297,6 +333,82 @@ public class MesXslDingTplBindController { return "[]"; } + private Class resolveEntityClass(String bizCode) { + PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(bizCode); + if (vo == null || StringUtils.isBlank(vo.getDescription())) { + return null; + } + return PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim()); + } + + private List listMainFieldsEnriched(String bizCode) { + Class cls = resolveEntityClass(bizCode); + List fields; + if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(bizCode)) { + fields = fieldCatalogProvider.listMainFields(bizCode); + } else if (cls != null) { + fields = PrintBizEntityFieldIntrospector.listFields(cls); + } else { + return Collections.emptyList(); + } + if (cls != null && fields != null && !fields.isEmpty()) { + PrintBizEntityFieldIntrospector.enrichDictMeta(fields, cls, null); + } + return fields != null ? fields : Collections.emptyList(); + } + + private Class resolveDetailItemClass(String bizCode, String detailProperty, String slotKind) { + Class mainCls = resolveEntityClass(bizCode); + if (mainCls == null) { + return null; + } + return PrintBizDetailPropertyScanner.resolveItemClassForSlot(mainCls, detailProperty, slotKind); + } + + private Map buildFieldMetaMap(String bizCode) { + Map map = new LinkedHashMap<>(); + for (PrintBizFieldItemVO f : listMainFieldsEnriched(bizCode)) { + if (f != null && StringUtils.isNotBlank(f.getFieldKey())) { + map.put(f.getFieldKey(), f); + } + } + Class mainCls = resolveEntityClass(bizCode); + if (mainCls != null && fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(bizCode)) { + for (PrintBizDetailSlotVO slot : fieldCatalogProvider.listDetailSlots(bizCode)) { + mergeDetailMeta(map, bizCode, slot.getPropertyName(), slot.getSlotKind()); + } + } else if (mainCls != null) { + for (PrintBizDetailSlotVO slot : PrintBizDetailPropertyScanner.listSlots(mainCls)) { + mergeDetailMeta(map, bizCode, slot.getPropertyName(), slot.getSlotKind()); + } + } + return map; + } + + private void mergeDetailMeta(Map map, String bizCode, String prop, String kind) { + List detailFields; + if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(bizCode)) { + detailFields = fieldCatalogProvider.listPrefixedDetailFields(bizCode, prop, kind); + } else { + Class mainCls = resolveEntityClass(bizCode); + detailFields = + mainCls == null + ? Collections.emptyList() + : PrintBizDetailPropertyScanner.listPrefixedDetailFields(mainCls, prop, kind); + } + Class itemCls = resolveDetailItemClass(bizCode, prop, kind); + if (itemCls != null && detailFields != null && !detailFields.isEmpty()) { + PrintBizEntityFieldIntrospector.enrichDictMeta(detailFields, itemCls, prop); + } + if (detailFields != null) { + for (PrintBizFieldItemVO f : detailFields) { + if (f != null && StringUtils.isNotBlank(f.getFieldKey())) { + map.put(f.getFieldKey(), f); + } + } + } + } + // ═══════════════════════ 内部 VO ═══════════════════════ @Data @@ -316,4 +428,20 @@ public class MesXslDingTplBindController { private String templateId; private String fieldMappingJson; } + + @Data + public static class ResolveFieldValuesRequest { + private String bizCode; + private Map rowData; + private List items; + } + + @Data + public static class ResolveFieldItem { + /** 前端映射键,通常为 componentId */ + private String mapKey; + private String bizField; + /** raw 或 text */ + private String valueMode; + } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java new file mode 100644 index 0000000..b691de8 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java @@ -0,0 +1,109 @@ +package org.jeecg.modules.xslmes.dingtalk.service; + +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecg.modules.system.service.ISysDictService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 审批模板绑定字段取值:支持原值与字典/表字典显示文本。 + */ +@Service +@Slf4j +public class DingTplBindFieldValueResolver { + + @Autowired private ISysDictService sysDictService; + + /** + * 按绑定配置解析字段值。 + * + * @param rowData 业务行数据(Map) + * @param bizField 字段路径 + * @param valueMode raw=原值,text=显示文本 + * @param meta 字段元数据(含 translateKind) + */ + public Object resolveValue( + Object rowData, String bizField, String valueMode, PrintBizFieldItemVO meta) { + Object raw = getNestedValue(rowData, bizField); + if (!"text".equalsIgnoreCase(StringUtils.trimToEmpty(valueMode)) + || meta == null + || StringUtils.isBlank(meta.getTranslateKind()) + || "NONE".equalsIgnoreCase(meta.getTranslateKind())) { + return raw; + } + String fromRow = getDictTextFromRow(rowData, bizField); + if (StringUtils.isNotBlank(fromRow)) { + return fromRow; + } + if (raw == null) { + return null; + } + String key = String.valueOf(raw); + if (StringUtils.isBlank(key)) { + return raw; + } + try { + if ("DICT".equalsIgnoreCase(meta.getTranslateKind()) + && StringUtils.isNotBlank(meta.getDictCode())) { + String text = sysDictService.queryDictTextByKey(meta.getDictCode(), key); + return StringUtils.isNotBlank(text) ? text : raw; + } + if ("TABLE".equalsIgnoreCase(meta.getTranslateKind()) + && StringUtils.isNotBlank(meta.getDictTable())) { + String text = + sysDictService.queryTableDictTextByKey( + meta.getDictTable(), + StringUtils.defaultString(meta.getDictText(), ""), + StringUtils.defaultIfBlank(meta.getDictCodeField(), "id"), + key); + return StringUtils.isNotBlank(text) ? text : raw; + } + } catch (Exception e) { + log.warn("审批绑定字段翻译失败 bizField={} key={}: {}", bizField, key, e.getMessage()); + } + return raw; + } + + @SuppressWarnings("unchecked") + private Object getNestedValue(Object obj, String path) { + if (obj == null || StringUtils.isBlank(path)) { + return null; + } + String[] parts = path.split("\\."); + Object cur = obj; + for (String p : parts) { + if (cur == null) { + return null; + } + if (cur instanceof Map) { + cur = ((Map) cur).get(p); + } else { + return null; + } + } + return cur; + } + + @SuppressWarnings("unchecked") + private String getDictTextFromRow(Object rowData, String bizField) { + if (!(rowData instanceof Map) || StringUtils.isBlank(bizField)) { + return null; + } + Map map = (Map) rowData; + String[] parts = bizField.split("\\."); + if (parts.length == 1) { + Object v = map.get(parts[0] + "_dictText"); + return v != null ? String.valueOf(v) : null; + } + String parentPath = String.join(".", java.util.Arrays.copyOf(parts, parts.length - 1)); + Object parent = getNestedValue(rowData, parentPath); + if (parent instanceof Map) { + Object v = ((Map) parent).get(parts[parts.length - 1] + "_dictText"); + return v != null ? String.valueOf(v) : null; + } + return null; + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java index e933b45..84e4713 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java @@ -1,6 +1,7 @@ package org.jeecg.modules.xslmes.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; @@ -157,14 +158,35 @@ public class MesXslMixingSpec implements Serializable { //update-end---author:cursor ---date:20260608 for:【XSLMES-20260608-A01】混炼示方新增状态字段----------- private String sysOrgCode; + + //update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】queryById补充起草人/变更人姓名----------- + @Excel(name = "创建人", width = 12, dictTable = "sys_user", dicText = "realname", dicCode = "username") + @Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username") private String createBy; + /** queryById 等非分页接口补充创建人姓名(DictAspect 仅翻译分页列表) */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createBy_dictText; + + @Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username") + private String updateBy; + + /** queryById 补充最后修改人姓名 */ + @TableField(exist = false) + @Schema(description = "修改人姓名") + private String updateBy_dictText; + + /** queryById 补充起草人姓名(draftBy 存用户名时翻译) */ + @TableField(exist = false) + @Schema(description = "起草人姓名") + private String draftBy_dictText; + //update-end---author:GHT ---date:2026-06-10 for:【混炼示方】queryById补充起草人/变更人姓名----------- + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; - private String updateBy; - @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java index 6db05b7..c634442 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java @@ -52,6 +52,14 @@ public class MesXslMixingSpecTcu implements Serializable { @Schema(description = "药品称量位置(字典xslmes_mixing_drug_weigh_pos)") private String drugWeighPos; + //update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量----------- + @Schema(description = "是否附加(字典yn)") + private String isAttach; + + @Schema(description = "附加重量") + private BigDecimal attachWeight; + //update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量----------- + @Schema(description = "租户ID") private Integer tenantId; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java index ab5ddd3..06c4543 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java @@ -29,6 +29,8 @@ import org.jeecg.modules.xslmes.entity.MesXslMixingSpecDownStep; import org.jeecg.modules.xslmes.entity.MesXslMixingSpecMaterial; import org.jeecg.modules.xslmes.entity.MesXslMixingSpecStep; import org.jeecg.modules.xslmes.entity.MesXslMixingSpecTcu; +import org.jeecg.modules.system.entity.SysUser; +import org.jeecg.modules.system.service.ISysUserService; import org.jeecg.modules.xslmes.entity.MesXslMixerPsCompile; import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecDownStepMapper; import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecMapper; @@ -50,6 +52,8 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl { int ai = TCU_UP.equals(a.getSectionType()) ? 0 : TCU_DOWN.equals(a.getSectionType()) ? 1 : 2; int bi = TCU_UP.equals(b.getSectionType()) ? 0 : TCU_DOWN.equals(b.getSectionType()) ? 1 : 2; @@ -681,6 +724,20 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl fields, Class clazz, String fieldKeyPrefix) { + if (fields == null || fields.isEmpty() || clazz == null) { + return; + } + String prefix = StringUtils.isBlank(fieldKeyPrefix) ? "" : fieldKeyPrefix.trim(); + if (StringUtils.isNotBlank(prefix) && !prefix.endsWith(".")) { + prefix = prefix + "."; + } + Map fieldMap = new LinkedHashMap<>(); + Class c = clazz; + while (c != null && c != Object.class) { + for (Field f : c.getDeclaredFields()) { + fieldMap.putIfAbsent(f.getName(), f); + } + c = c.getSuperclass(); + } + for (PrintBizFieldItemVO vo : fields) { + if (vo == null || StringUtils.isBlank(vo.getFieldKey())) { + continue; + } + String key = vo.getFieldKey(); + if (StringUtils.isNotBlank(prefix) && key.startsWith(prefix)) { + key = key.substring(prefix.length()); + } + int dot = key.indexOf('.'); + if (dot >= 0) { + key = key.substring(dot + 1); + } + Field f = fieldMap.get(key); + if (f != null) { + fillDictMeta(vo, f); + } else if (StringUtils.isBlank(vo.getTranslateKind())) { + vo.setTranslateKind("NONE"); + } + } + } + //update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】扫描@Dict并补全字典元数据----------- + /** 按全限定类名加载 Class,失败返回 null */ public static Class tryLoadClass(String entityClassFqn) { if (StringUtils.isBlank(entityClassFqn)) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java index c7db51e..f582caa 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java @@ -35,6 +35,24 @@ public class PrintBizFieldItemVO implements Serializable { @Schema(description = "简化种类,便于前端格式化") private String simpleKind; + //update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】字段元数据支持字典/表字典----------- + /** 取值翻译类型:NONE=普通字段,DICT=字典,TABLE=表字典(部门/用户等) */ + @Schema(description = "取值翻译类型:NONE/DICT/TABLE") + private String translateKind; + + @Schema(description = "字典编码(dicCode),translateKind=DICT 时有效") + private String dictCode; + + @Schema(description = "字典表名,translateKind=TABLE 时有效") + private String dictTable; + + @Schema(description = "字典表文本列,translateKind=TABLE 时有效") + private String dictText; + + @Schema(description = "字典表编码列,translateKind=TABLE 时有效") + private String dictCodeField; + //update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】字段元数据支持字典/表字典----------- + /** 兼容旧三参构造(类型字段为空) */ public PrintBizFieldItemVO(String fieldKey, String label, String description) { this.fieldKey = fieldKey; @@ -53,6 +71,13 @@ public class PrintBizFieldItemVO implements Serializable { o.setJavaType(src.getJavaType()); o.setJdbcType(src.getJdbcType()); o.setSimpleKind(src.getSimpleKind()); + //update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】复制字段时保留字典元数据----------- + o.setTranslateKind(src.getTranslateKind()); + o.setDictCode(src.getDictCode()); + o.setDictTable(src.getDictTable()); + o.setDictText(src.getDictText()); + o.setDictCodeField(src.getDictCodeField()); + //update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】复制字段时保留字典元数据----------- } return o; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_146__mes_xsl_mixing_spec_tcu_attach.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_146__mes_xsl_mixing_spec_tcu_attach.sql new file mode 100644 index 0000000..1ae92c1 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_146__mes_xsl_mixing_spec_tcu_attach.sql @@ -0,0 +1,7 @@ +-- TCU温度条件:新增是否附加、重量字段 +-- author: GHT date: 2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量字段 +SET @db = DATABASE(); + +SET @sql = IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='mes_xsl_mixing_spec_tcu' AND COLUMN_NAME='is_attach')=0, + 'ALTER TABLE `mes_xsl_mixing_spec_tcu` ADD COLUMN `is_attach` varchar(1) DEFAULT ''0'' COMMENT ''是否附加(字典yn)'' AFTER `drug_weigh_pos`, ADD COLUMN `attach_weight` decimal(18,6) DEFAULT NULL COMMENT ''附加重量'' AFTER `is_attach`','SELECT 1'); +PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; diff --git a/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue b/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue index ab0bd35..6f3446a 100644 --- a/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue +++ b/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue @@ -254,6 +254,12 @@ previewFlowApprovers, } from '/@/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api'; import { checkCanLaunch } from '/@/views/approval/gate/approvalGate.api'; + import { resolveFieldValues } from '/@/views/xslmes/dingtalk/dingTplBind/dingTplBind.api'; + import { + type ValueMode, + getNestedValue, + resolveFieldValueLocal, + } from '/@/views/xslmes/dingtalk/dingTplBind/dingTplFieldValue'; const emit = defineEmits(['success']); const { createMessage } = useMessage(); @@ -330,7 +336,7 @@ if (f.componentName === 'TableField') tableValues[f.label] = []; } - applyPrefillForRow(currentRowIndex.value); + applyPrefillForRowAsync(currentRowIndex.value); const presetFlowId = tplRecord?.flowId; if (presetFlowId) { @@ -357,7 +363,7 @@ if (idx === currentRowIndex.value) return; currentRowIndex.value = idx; resetFieldValues(); - applyPrefillForRow(idx); + applyPrefillForRowAsync(idx); activeTab.value = 'form'; } @@ -370,10 +376,10 @@ } } - function applyPrefillForRow(idx: number) { + async function applyPrefillForRowAsync(idx: number) { const rowData = allRows.value[idx]; if (rowData && tplData.value?.fieldMappingJson) { - applyPrefill(dingFields.value, tplData.value.fieldMappingJson, rowData); + await applyPrefill(dingFields.value, tplData.value.fieldMappingJson, rowData); } } @@ -401,13 +407,48 @@ componentName: string; parentId?: string; bizField?: string; + valueMode?: ValueMode; } - function applyPrefill(fields: any[], mappingJson: string, rowData: any) { + async function applyPrefill(fields: any[], mappingJson: string, rowData: any) { let mapping: MappingItem[] = []; - try { mapping = JSON.parse(mappingJson); } catch { return; } + try { + mapping = JSON.parse(mappingJson); + } catch { + return; + } - const byId = new Map(mapping.map(m => [m.componentId, m])); + const byId = new Map(mapping.map((m) => [m.componentId, m])); + + // 主表 text 模式字段批量解析(明细列在各行上本地解析) + const resolveItems = mapping + .filter((m) => m.valueMode === 'text' && m.bizField && !m.parentId) + .map((m) => ({ + mapKey: m.componentId, + bizField: m.bizField!, + valueMode: 'text', + })); + + let resolvedMap: Record = {}; + if (resolveItems.length && tplData.value?.bizCode) { + try { + resolvedMap = (await resolveFieldValues({ + bizCode: tplData.value.bizCode, + rowData, + items: resolveItems, + })) || {}; + } catch { + resolvedMap = {}; + } + } + + function pickValue(m: MappingItem, bizField: string): any { + if (resolvedMap[m.componentId] !== undefined) { + return resolvedMap[m.componentId]; + } + const mode = (m.valueMode || 'raw') as ValueMode; + return resolveFieldValueLocal(rowData, bizField, mode); + } for (const field of fields) { if (field.componentName === 'TextNote') continue; @@ -420,34 +461,30 @@ const arr: any[] = getNestedValue(rowData, slotName); if (!Array.isArray(arr) || !arr.length) continue; - const childMappings = mapping.filter(x => x.parentId === cid && x.bizField); - tableValues[field.label] = arr.map(element => { + const childMappings = mapping.filter((x) => x.parentId === cid && x.bizField); + tableValues[field.label] = arr.map((element) => { const row: Record = {}; for (const child of childMappings) { const parts = (child.bizField || '').split('.'); const colKey = parts.slice(1).join('.'); - const val = colKey ? getNestedValue(element, colKey) : undefined; + const mode = (child.valueMode || 'raw') as ValueMode; + const val = colKey ? resolveFieldValueLocal(element, colKey, mode) : undefined; row[child.componentLabel] = val !== undefined && val !== null ? String(val) : ''; } - for (const child of (field.children || [])) { + for (const child of field.children || []) { if (!(child.label in row)) row[child.label] = ''; } return row; }); } else { if (!m?.bizField) continue; - const val = getNestedValue(rowData, m.bizField); + const val = pickValue(m, m.bizField); if (val === undefined || val === null) continue; formValues[field.label] = formatForDisplay(val, field.componentName); } } } - function getNestedValue(obj: any, path: string): any { - if (!obj || !path) return undefined; - return path.split('.').reduce((acc: any, k: string) => acc?.[k], obj); - } - function formatForDisplay(v: any, componentName: string): any { if (v === null || v === undefined) return ''; if (['NumberField', 'MoneyField'].includes(componentName)) { diff --git a/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue b/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue index 4cd549c..c666022 100644 --- a/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue +++ b/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue @@ -75,7 +75,12 @@ const drawerRef = ref(); // 当前审批流绑定的业务表,供节点配置按表查可选回调动作 const bizTableRef = ref(''); + const flowIdRef = ref(''); provide('approvalBizTable', bizTableRef); + provide('approvalFlowId', flowIdRef); + //update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】向节点配置传递流程ID与实时flowConfig----------- + provide('approvalFlowConfig', () => (root.value ? JSON.stringify(root.value) : '')); + //update-end---author:GHT ---date:2026-06-10 for:【审批流设计】向节点配置传递流程ID与实时flowConfig----------- provide('approvalFlowRoot', root); // update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批注册中心候选环节----- --> const paletteStages = ref([]); @@ -114,6 +119,7 @@ readonly.value = !!data?.readonly; flowCtx.readonly = readonly.value; bizTableRef.value = data?.record?.bizTable || ''; + flowIdRef.value = data?.record?.id || ''; // update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收审批注册中心候选环节----- --> paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : []; // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收审批注册中心候选环节----- --> diff --git a/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue b/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue index cd06178..a7558ee 100644 --- a/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue +++ b/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue @@ -133,7 +133,31 @@ message="当前为流程最后一个审批节点:「批准」类方案触发时机为「审批通过」,已合并到下方下拉(带 [流程最终通过] 前缀)。" />
-
{{ isLastApproverNode ? '本节点通过 / 流程最终通过时执行' : '本节点通过时执行' }}
+
+
{{ isLastApproverNode ? '本节点通过 / 流程最终通过时执行' : '本节点通过时执行' }}
+
+ + 生成集成方案 + + + 编辑方案 + +
+
确定 + @@ -238,6 +263,8 @@ import type { FlowNode } from './flowTypes'; import { useMessage } from '/@/hooks/web/useMessage'; import { listPublishedIntegrationPlans } from '../approvalFlow.api'; + import { generateForNode, queryPlanById } from '/@/views/xslmes/approval/integration/MesXslIntegrationPlan.api'; + import MesXslIntegrationActionDrawer from '/@/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue'; const props = defineProps<{ readonly?: boolean }>(); const emit = defineEmits(['confirm']); @@ -245,8 +272,13 @@ const { createMessage } = useMessage(); const bizTable = inject>('approvalBizTable', ref('')); + const flowId = inject>('approvalFlowId', ref('')); + const getFlowConfig = inject<(() => string) | null>('approvalFlowConfig', null); const flowRoot = inject>('approvalFlowRoot', ref(null)); const integrationPlansCacheKey = ref(''); + const generatingPlan = ref(false); + const editingPlan = ref(false); + const actionDrawerRef = ref(); const STAGE_LABELS: Record = { proofread: '校对', audit: '审核', approve: '批准' }; @@ -290,6 +322,20 @@ return undefined; }); + const canGeneratePlan = computed(() => { + const sk = form.value?.props?.stageKey; + return !!sk && sk !== '' && !!bizTable.value && !!flowId.value && !!node.value?.id; + }); + + /** 当前节点已绑定的集成方案 ID(含草稿,下拉未列出也可编辑) */ + const selectedPlanId = computed(() => { + const ip = form.value?.props?.integrationPlans; + if (!ip) return undefined; + if (ip.onNodeApprove) return ip.onNodeApprove; + if (ip.onApprove) return ip.onApprove; + return undefined; + }); + // update-begin---author:GHT ---date:2026-06-05 for:【XSLMES-20260605-K8R3】绑定审批环节辅助函数----- function stageKeyLabel(key: string): string { const map: Record = { proofread: '校对', audit: '审核', approve: '批准' }; @@ -316,6 +362,78 @@ form.value.props.integrationPlans[phase] = planId; } + //update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】节点内生成集成方案并打开动作配置----------- + async function handleGenerateIntegrationPlan() { + if (readonly.value || !form.value || !node.value) return; + const stageKey = form.value.props?.stageKey; + if (!stageKey) { + createMessage.warning('请先在「绑定审批环节」中选择校对、审核或批准'); + return; + } + if (stageKey === '') { + createMessage.warning('纯过路审批节点无需生成集成方案'); + return; + } + if (!bizTable.value || !flowId.value) { + createMessage.warning('缺少业务表或审批流信息'); + return; + } + generatingPlan.value = true; + try { + const data = await generateForNode({ + sourceTable: bizTable.value, + flowId: flowId.value, + nodeId: node.value.id, + stageKey, + flowConfig: getFlowConfig?.() || undefined, + overwriteDraft: true, + }); + integrationPlansCacheKey.value = ''; + await loadIntegrationPlans(); + + const phase = data?.triggerPhase || 'onNodeApprove'; + if (!form.value.props.integrationPlans) { + form.value.props.integrationPlans = {}; + } + form.value.props.integrationPlans.onNodeApprove = undefined; + form.value.props.integrationPlans.onApprove = undefined; + form.value.props.integrationPlans[phase] = data.planId; + + createMessage.success(data?.created ? '已生成集成方案,请配置动作' : '已加载已有集成方案,请配置动作'); + + const plan = await queryPlanById(data.planId); + if (plan) { + await actionDrawerRef.value?.openAndEditFirstAction(plan); + } + } catch (e: any) { + createMessage.error(e?.message || '生成集成方案失败'); + } finally { + generatingPlan.value = false; + } + } + + async function handleEditIntegrationPlan() { + const planId = selectedPlanId.value; + if (!planId) { + createMessage.warning('请先选择或生成集成方案'); + return; + } + editingPlan.value = true; + try { + const plan = await queryPlanById(planId); + if (!plan) { + createMessage.error('未找到该集成方案'); + return; + } + await actionDrawerRef.value?.open(plan); + } catch (e: any) { + createMessage.error(e?.message || '加载集成方案失败'); + } finally { + editingPlan.value = false; + } + } + //update-end---author:GHT ---date:2026-06-10 for:【审批流设计】节点内生成集成方案并打开动作配置----------- + async function loadIntegrationPlans() { const table = bizTable.value || ''; if (!table) { @@ -474,4 +592,27 @@ color: #595959; margin-bottom: 8px; } + .fd-ip-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; + .fd-ip-title { + margin-bottom: 0; + flex: 1; + min-width: 0; + } + } + .fd-ip-gen-btn { + padding: 0 4px; + height: auto; + flex-shrink: 0; + } + .fd-ip-title-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + } diff --git a/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.api.ts b/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.api.ts index 1049184..e74d386 100644 --- a/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.api.ts +++ b/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.api.ts @@ -9,10 +9,18 @@ enum Api { edit = '/xslmes/mesXslBizDocRegistry/edit', deleteOne = '/xslmes/mesXslBizDocRegistry/delete', deleteBatch = '/xslmes/mesXslBizDocRegistry/deleteBatch', + dbTables = '/xslmes/mesXslBizDocRegistry/dbTables', } export const list = (params) => defHttp.get({ url: Api.list, params }); +/** 当前库物理表(审批注册中心下拉) */ +export const listDbTables = (keyword?: string) => + defHttp.get<{ value: string; label: string; comment?: string }[]>({ + url: Api.dbTables, + params: keyword ? { keyword } : {}, + }); + export const saveOrUpdate = (params, isUpdate) => isUpdate ? defHttp.put({ url: Api.edit, params }) : defHttp.post({ url: Api.save, params }); diff --git a/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts b/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts index c7f7243..7c2ac22 100644 --- a/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts @@ -1,4 +1,5 @@ import { BasicColumn, FormSchema } from '/@/components/Table'; +import { listDbTables } from './MesXslBizDocRegistry.api'; const STAGE_DICT = 'mes_xsl_approval_stage'; @@ -36,9 +37,16 @@ export const formSchema: FormSchema[] = [ { label: '物理表名', field: 'tableName', - component: 'Input', - componentProps: { placeholder: '数据库表名,如 mes_xsl_mixer_ps_compile' }, - dynamicRules: () => [{ required: true, message: '请输入物理表名!' }], + component: 'ApiSelect', + componentProps: { + api: listDbTables, + showSearch: true, + placeholder: '请选择数据库物理表,可输入关键字筛选', + labelField: 'label', + valueField: 'value', + immediate: true, + }, + dynamicRules: () => [{ required: true, message: '请选择物理表名!' }], }, { label: '中文名称', diff --git a/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts b/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts index 942fb16..28245d9 100644 --- a/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts +++ b/jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts @@ -19,6 +19,8 @@ enum Api { registryByTable = '/xslmes/mesXslIntegrationPlan/registryByTable', previewDefaultFromFlow = '/xslmes/mesXslIntegrationPlan/previewDefaultFromFlow', generateDefaultFromFlow = '/xslmes/mesXslIntegrationPlan/generateDefaultFromFlow', + generateForNode = '/xslmes/mesXslIntegrationPlan/generateForNode', + queryById = '/xslmes/mesXslIntegrationPlan/queryById', } export const list = (params) => defHttp.get({ url: Api.list, params }); @@ -49,6 +51,18 @@ export const generateDefaultFromFlow = (params: { nodeBindings?: Array<{ nodeId: string; stage?: string | null }>; }) => defHttp.post({ url: Api.generateDefaultFromFlow, params }); +/** 流程设计器:为单个节点生成集成方案 */ +export const generateForNode = (params: { + sourceTable: string; + flowId: string; + nodeId: string; + stageKey: string; + flowConfig?: string; + overwriteDraft?: boolean; +}) => defHttp.post({ url: Api.generateForNode, params }); + +export const queryPlanById = (id: string) => defHttp.get({ url: Api.queryById, params: { id } }); + // 动作管理 export const listActions = (planId) => defHttp.get({ url: Api.actionList, params: { planId } }); export const saveAction = (params) => defHttp.post({ url: Api.actionAdd, params }); diff --git a/jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue b/jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue index b373ba4..094f757 100644 --- a/jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue +++ b/jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue @@ -105,6 +105,15 @@ return record.actionConfig || '-'; } } + if (record.actionType === 'SQL_UPDATE') { + try { + const cfg = JSON.parse(record.actionConfig || '{}'); + const base = record.sqlTemplate || '-'; + return cfg.syncTrace ? `${base};痕迹同步:是` : base; + } catch { + return record.sqlTemplate || '-'; + } + } return record.sqlTemplate || '-'; } @@ -192,5 +201,14 @@ } } - defineExpose({ open }); + defineExpose({ open, openAndEditFirstAction }); + + async function openAndEditFirstAction(plan: Recordable) { + await open(plan); + if (actions.value.length > 0) { + openVisualEditor(actions.value[0]); + } else { + openVisualEditor(); + } + } diff --git a/jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue b/jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue index 406d235..4d998bc 100644 --- a/jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue +++ b/jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue @@ -177,6 +177,21 @@
+ +
+ 同步主表审批痕迹到目标表 +
+ 开启后:环节通过时将主表当前审批人/时间写入目标表 mes_xsl_approval_trace; + 驳回时将按「新状态」清空对应环节痕迹。 + (驳回清空仅对「状态修改」动作生效) + 目标表须已在审批注册中心启用对应环节。 +
+
+ + + @@ -229,8 +240,19 @@ option-filter-prop="label" allow-clear size="small" + @change="() => onDetailBizFieldChange(record, tf.componentId)" /> + @@ -288,6 +310,13 @@ import { ref, computed, reactive, onMounted } from 'vue'; import { useMessage } from '/@/hooks/web/useMessage'; import * as Api from './dingTplBind.api'; + import { + type FieldTranslateMeta, + type ValueMode, + VALUE_MODE_OPTIONS, + defaultValueMode, + isTranslatableMeta, + } from './dingTplFieldValue'; const { createMessage } = useMessage(); @@ -317,6 +346,7 @@ componentName: string; parentId?: string; bizField?: string; + valueMode?: ValueMode; children?: FieldRow[]; // 仅 TableField 使用 } @@ -349,14 +379,14 @@ const mainFieldRows = ref([]); const tableFieldRows = ref([]); - const bizFields = ref<{ fieldKey: string; label: string }[]>([]); + const bizFields = ref([]); const bizFieldsLoading = ref(false); const detailSlots = ref([]); const detailSlotsLoading = ref(false); // key = tableField's componentId, value = loaded detail field options - const detailFieldsMap = reactive>({}); + const detailFieldsMap = reactive>({}); const detailFieldsLoadingMap = reactive>({}); const saving = ref(false); @@ -404,15 +434,17 @@ // ══ 表格列定义 ══ const mainColumns = [ - { title: '控件类型', key: 'componentType', width: 140 }, - { title: '钉钉字段名', dataIndex: 'componentLabel', width: 160 }, + { title: '控件类型', key: 'componentType', width: 130 }, + { title: '钉钉字段名', dataIndex: 'componentLabel', width: 140 }, { title: '绑定实体字段', key: 'bizField' }, + { title: '取值方式', key: 'valueMode', width: 140 }, ]; const detailColumns = [ - { title: '控件类型', key: 'componentType', width: 140 }, - { title: '字段名', dataIndex: 'componentLabel', width: 160 }, + { title: '控件类型', key: 'componentType', width: 130 }, + { title: '字段名', dataIndex: 'componentLabel', width: 140 }, { title: '绑定明细字段', key: 'bizField' }, + { title: '取值方式', key: 'valueMode', width: 140 }, ]; // ══ 菜单树操作 ══ @@ -482,7 +514,7 @@ bizFieldsLoading.value = true; try { const list = await Api.getBizFields(bizCode); - bizFields.value = (list || []) as { fieldKey: string; label: string }[]; + bizFields.value = (list || []) as FieldTranslateMeta[]; } catch { bizFields.value = []; } finally { @@ -551,6 +583,11 @@ } } + interface SavedMappingEntry { + bizField?: string; + valueMode?: ValueMode; + } + /** 从 dingFields 构建 mainFieldRows / tableFieldRows */ function buildFieldRows(fields: DingField[], savedMappingJson: string | null) { const savedMap = parseSavedMapping(savedMappingJson); @@ -563,30 +600,46 @@ const cid = f.id || f.label; if (f.componentName === 'TableField') { + const saved = savedMap.get(cid); const tfRow: FieldRow = { componentId: cid, componentLabel: f.label, componentName: f.componentName, - bizField: savedMap.get(cid), + bizField: saved?.bizField, children: (f.children || []).map((child) => { const childCid = `${cid}.${child.id || child.label}`; - return { + const childSaved = savedMap.get(childCid); + const row: FieldRow = { componentId: childCid, componentLabel: child.label, componentName: child.componentName, parentId: cid, - bizField: savedMap.get(childCid), - } as FieldRow; + bizField: childSaved?.bizField, + valueMode: childSaved?.valueMode, + }; + if (row.bizField && !row.valueMode) { + row.valueMode = defaultValueMode( + row.componentName, + findDetailFieldMeta(row.bizField, cid), + ); + } + return row; }), }; tables.push(tfRow); } else { - mains.push({ + const saved = savedMap.get(cid); + const row: FieldRow = { componentId: cid, componentLabel: f.label, componentName: f.componentName, - bizField: savedMap.get(cid), - }); + bizField: saved?.bizField, + valueMode: saved?.valueMode, + }; + if (row.bizField && !row.valueMode) { + row.valueMode = defaultValueMode(row.componentName, findMainFieldMeta(row.bizField)); + } + mains.push(row); } } @@ -594,14 +647,23 @@ tableFieldRows.value = tables; } - /** 解析已保存的 fieldMappingJson → Map */ - function parseSavedMapping(json: string | null): Map { - const m = new Map(); + /** 解析已保存的 fieldMappingJson */ + function parseSavedMapping(json: string | null): Map { + const m = new Map(); if (!json) return m; try { - const arr = JSON.parse(json) as { componentId: string; bizField?: string }[]; + const arr = JSON.parse(json) as { + componentId: string; + bizField?: string; + valueMode?: ValueMode; + }[]; for (const item of arr) { - if (item.componentId) m.set(item.componentId, item.bizField || undefined); + if (item.componentId) { + m.set(item.componentId, { + bizField: item.bizField || undefined, + valueMode: item.valueMode, + }); + } } } catch { /* 解析失败忽略 */ @@ -638,7 +700,7 @@ detailFieldsLoadingMap[tableComponentId] = true; try { const list = await Api.getDetailFields(selectedBizCode.value, slotName, kind); - detailFieldsMap[tableComponentId] = (list || []) as { fieldKey: string; label: string }[]; + detailFieldsMap[tableComponentId] = (list || []) as FieldTranslateMeta[]; } catch { detailFieldsMap[tableComponentId] = []; } finally { @@ -653,6 +715,43 @@ })); } + function findMainFieldMeta(fieldKey?: string): FieldTranslateMeta | undefined { + if (!fieldKey) return undefined; + return bizFields.value.find((f) => f.fieldKey === fieldKey); + } + + function findDetailFieldMeta(fieldKey?: string, tableId?: string): FieldTranslateMeta | undefined { + if (!fieldKey || !tableId) return undefined; + return (detailFieldsMap[tableId] || []).find((f) => f.fieldKey === fieldKey); + } + + function isTranslatableMainField(fieldKey?: string): boolean { + return isTranslatableMeta(findMainFieldMeta(fieldKey)); + } + + function isTranslatableDetailField(fieldKey?: string, tableId?: string): boolean { + return isTranslatableMeta(findDetailFieldMeta(fieldKey, tableId)); + } + + function onMainBizFieldChange(record: FieldRow) { + if (!record.bizField) { + record.valueMode = undefined; + return; + } + record.valueMode = defaultValueMode(record.componentName, findMainFieldMeta(record.bizField)); + } + + function onDetailBizFieldChange(record: FieldRow, tableId: string) { + if (!record.bizField) { + record.valueMode = undefined; + return; + } + record.valueMode = defaultValueMode( + record.componentName, + findDetailFieldMeta(record.bizField, tableId), + ); + } + // ══ 自动匹配 ══ function autoMatchFields() { @@ -727,15 +826,20 @@ componentName: string; parentId?: string; bizField?: string; + valueMode?: ValueMode; }[] = []; for (const row of mainFieldRows.value) { - items.push({ + const item: (typeof items)[0] = { componentId: row.componentId, componentLabel: row.componentLabel, componentName: row.componentName, bizField: row.bizField || '', - }); + }; + if (row.bizField && isTranslatableMainField(row.bizField) && row.valueMode) { + item.valueMode = row.valueMode; + } + items.push(item); } for (const tf of tableFieldRows.value) { @@ -746,13 +850,21 @@ bizField: tf.bizField || '', }); for (const child of tf.children || []) { - items.push({ + const childItem: (typeof items)[0] = { componentId: child.componentId, componentLabel: child.componentLabel, componentName: child.componentName, parentId: tf.componentId, bizField: child.bizField || '', - }); + }; + if ( + child.bizField && + isTranslatableDetailField(child.bizField, tf.componentId) && + child.valueMode + ) { + childItem.valueMode = child.valueMode; + } + items.push(childItem); } } @@ -1063,7 +1175,12 @@ } .dtb-bind-table { - margin-bottom: 4px; + margin-bottom: 8px; + } + + .dtb-value-mode-hint { + color: #bbb; + font-size: 12px; } .dtb-bind-table :deep(.ant-table-cell) { diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts index be4ffcd..170023d 100644 --- a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts +++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts @@ -22,6 +22,7 @@ enum Api { bindFlow = '/xslmes/mesXslDingProcessTpl/bindFlow', approvalFlowList = '/xslmes/approvalFlow/list', previewFlowApprovers = '/xslmes/mesXslDingProcessTpl/previewFlowApprovers', + toggleStatus = '/xslmes/mesXslDingProcessTpl/toggleStatus', } export const getExportUrl = Api.exportXls; @@ -45,7 +46,7 @@ export const batchDelete = (params, handleSuccess) => { }; export const saveOrUpdate = (params, isUpdate) => - defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }, { successMessageMode: 'none' }); + defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }, { successMessageMode: isUpdate ? 'message' : 'none' }); /** 新增审批模板草稿(返回含 id 的完整记录) */ export const addNewTemplate = (params) => @@ -81,3 +82,7 @@ export const getApprovalFlowList = (params?) => export const previewFlowApprovers = (flowId: string) => defHttp.get({ url: Api.previewFlowApprovers, params: { flowId } }, { successMessageMode: 'none' }); + +/** 切换模板启用/停用(停用后绑定的业务页不再显示钉钉审批按钮) */ +export const toggleTplStatus = (id: string) => + defHttp.post({ url: Api.toggleStatus, params: { id } }, { joinParamsToUrl: true }); diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue index 7c50711..2ad12f4 100644 --- a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue +++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue @@ -105,6 +105,10 @@ + + + + ({}); @@ -167,6 +174,9 @@ api: list, columns, canResize: true, + //update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压----------- + scroll: { x: 1700 }, + //update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压----------- formConfig: { schemas: searchFormSchema, autoSubmitOnEnter: true, @@ -175,7 +185,9 @@ actionColumn: { title: '操作', dataIndex: 'action', - width: 220, + //update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压----------- + width: 540, + //update-end---author:GHT ---date:2026-06-10 for:【钉钉审批模板】操作列加宽避免按钮挤压----------- fixed: 'right', slots: { customRender: 'action' }, }, @@ -230,27 +242,72 @@ (selectedRowKeys.value = []) && reload(); } + function isTplEnabled(record: Recordable) { + return record.status === '1' || record.status === 1; + } + + async function handleToggleStatus(record: Recordable) { + try { + const msg = await toggleTplStatus(record.id); + createMessage.success(typeof msg === 'string' ? msg : isTplEnabled(record) ? '已停用' : '已启用'); + reload(); + } catch (e: any) { + createMessage.error(e?.message || '操作失败'); + } + } + function getTableAction(record) { + const enabled = isTplEnabled(record); return [ { label: '编辑', onClick: handleEdit.bind(null, record), auth: 'xslmes:mes_xsl_ding_process_tpl:edit' }, - //update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】操作列新增发起审批按钮 + //update-begin---author:GHT ---date:20260610 for:【钉钉审批模板】操作列停用/启用----------- { - label: '发起审批', + label: enabled ? '停用' : '启用', + color: enabled ? 'warning' : 'success', + auth: 'xslmes:mes_xsl_ding_process_tpl:edit', + popConfirm: { + title: enabled + ? '停用后,已绑定该模板的业务页面将不再显示「钉钉审批」按钮,确认停用?' + : '确认启用该审批模板?启用后业务页将恢复显示钉钉审批按钮。', + confirm: handleToggleStatus.bind(null, record), + placement: 'topLeft', + }, + }, + //update-end---author:GHT ---date:20260610 for:【钉钉审批模板】操作列停用/启用----------- + //update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程 + { + label: '绑定审批流程', + icon: 'ant-design:apartment-outlined', + auth: 'xslmes:mes_xsl_ding_process_tpl:edit', + onClick: handleBindApprovalFlow.bind(null, record), + }, + //update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程 + //update-begin---author:GHT ---date:2026-06-10 for:【MESToDing审批配置】设计模板移至操作列、发起审批改名为测试审批 + { + label: '设计模板', + icon: 'ant-design:layout-outlined', + auth: 'xslmes:mes_xsl_ding_process_tpl:edit', + onClick: handleDesignTemplate.bind(null, record), + }, + { + label: '测试审批', icon: 'ant-design:send-outlined', color: 'success', - disabled: !record.processCode, - tooltip: record.processCode ? '手动填表后发起钉钉审批' : '请先配置 processCode', + disabled: !enabled || !record.processCode, + tooltip: !enabled + ? '模板已停用' + : record.processCode + ? '手动填表后测试发起钉钉审批' + : '请先配置 processCode', onClick: handleLaunchApproval.bind(null, record), }, - //update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】操作列新增发起审批按钮 + //update-end---author:GHT ---date:2026-06-10 for:【MESToDing审批配置】设计模板移至操作列、发起审批改名为测试审批 ]; } function getDropDownAction(record) { const actions: any[] = [ { label: '详情', onClick: handleDetail.bind(null, record) }, - //update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】新增设计模板入口 - { label: '设计模板', onClick: handleDesignTemplate.bind(null, record), icon: 'ant-design:layout-outlined' }, ]; if (!record.processCode) { actions.push({ @@ -262,7 +319,6 @@ } actions.push( { label: '查看钉钉字段', onClick: handleShowDingSchema.bind(null, record), icon: 'ant-design:dingtalk-outlined' }, - //update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】新增设计模板入口 { label: '删除', popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record), placement: 'topLeft' }, @@ -272,11 +328,24 @@ return actions; } + // ===== 绑定审批流程 ===== + //update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程 + const bindFlowModalRef = ref(); + + function handleBindApprovalFlow(record: Recordable) { + bindFlowModalRef.value?.open(record); + } + //update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程 + // ===== 手动填表发起钉钉审批 ===== //update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批 const launchModalRef = ref(); function handleLaunchApproval(record: Recordable) { + if (!isTplEnabled(record)) { + createMessage.warning('该模板已停用,请先启用后再发起审批'); + return; + } if (!record.processCode) { createMessage.warning('该模板尚未配置 processCode,请先完成模板配置'); return; diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/BindApprovalFlowModal.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/BindApprovalFlowModal.vue new file mode 100644 index 0000000..75344e0 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/BindApprovalFlowModal.vue @@ -0,0 +1,433 @@ + + + + + + + + diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue index c89dc21..befccf7 100644 --- a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue +++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue @@ -676,6 +676,9 @@ try { const detail = await getTemplateDetail(record.id); tplData.value = detail; + if (detail?.dingNameSynced) { + createMessage.info('已从钉钉同步最新模板名称'); + } if (detail?.dingFields?.length) { // 以钉钉最新 schema 为准(保证结构与钉钉同步) diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts index 90bdf7c..b12c7b2 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts @@ -445,7 +445,14 @@ function sectionTitle(label: string, field: string): FormSchema { } const hasWorkflowInfo = ({ values }) => - !!(values.proofreadBy || values.proofreadTime || values.auditBy || values.auditTime || values.approveBy || values.approveTime); + !!( + values.traceProofreadBy || + values.traceProofreadTime || + values.traceAuditBy || + values.traceAuditTime || + values.traceApproveBy || + values.traceApproveTime + ); export const columns: BasicColumn[] = [ { title: '示方编号', align: 'center', dataIndex: 'specCode', width: 150, fixed: 'left' }, @@ -468,8 +475,7 @@ export const columns: BasicColumn[] = [ width: 100, customRender: ({ record }) => record?.createBy_dictText || record?.createBy || '', }, - { title: '审核人', align: 'center', dataIndex: 'auditBy', width: 100, defaultHidden: true }, - { title: '批准人', align: 'center', dataIndex: 'approveBy', width: 100, defaultHidden: true }, + // 审批痕迹 6 列(校对人/校对时间/审核人/审核时间/批准人/批准时间)由 useListPage 统一从 traceColumns 追加,勿在此手写 trace* 列 { title: '状态', align: 'center', dataIndex: 'status_dictText', width: 120 }, { title: '混合段数', align: 'center', dataIndex: 'mixingStages', width: 90, defaultHidden: true }, { title: 'TOTAL PHR', align: 'center', dataIndex: 'totalPhr', width: 100, defaultHidden: true }, @@ -617,51 +623,51 @@ export const workflowFormSchema: FormSchema[] = [ sectionTitle('审批记录', 'dividerWorkflow'), { label: '校对人', - field: 'proofreadBy', + field: 'traceProofreadBy', component: 'Input', componentProps: { disabled: true, bordered: false }, colProps: colHalf, - ifShow: ({ values }) => !!values.proofreadBy, + ifShow: ({ values }) => !!values.traceProofreadBy, }, { label: '校对时间', - field: 'proofreadTime', + field: 'traceProofreadTime', component: 'Input', componentProps: { disabled: true, bordered: false }, colProps: colHalf, - ifShow: ({ values }) => !!values.proofreadTime, + ifShow: ({ values }) => !!values.traceProofreadTime, }, { label: '审核人', - field: 'auditBy', + field: 'traceAuditBy', component: 'Input', componentProps: { disabled: true, bordered: false }, colProps: colHalf, - ifShow: ({ values }) => !!values.auditBy, + ifShow: ({ values }) => !!values.traceAuditBy, }, { label: '审核时间', - field: 'auditTime', + field: 'traceAuditTime', component: 'Input', componentProps: { disabled: true, bordered: false }, colProps: colHalf, - ifShow: ({ values }) => !!values.auditTime, + ifShow: ({ values }) => !!values.traceAuditTime, }, { label: '批准人', - field: 'approveBy', + field: 'traceApproveBy', component: 'Input', componentProps: { disabled: true, bordered: false }, colProps: colHalf, - ifShow: ({ values }) => !!values.approveBy, + ifShow: ({ values }) => !!values.traceApproveBy, }, { label: '批准时间', - field: 'approveTime', + field: 'traceApproveTime', component: 'Input', componentProps: { disabled: true, bordered: false }, colProps: colHalf, - ifShow: ({ values }) => !!values.approveTime, + ifShow: ({ values }) => !!values.traceApproveTime, }, ]; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/components/MesXslFormulaSpecModal.vue b/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/components/MesXslFormulaSpecModal.vue index c1ab527..a1e55aa 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/components/MesXslFormulaSpecModal.vue +++ b/jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/components/MesXslFormulaSpecModal.vue @@ -450,6 +450,11 @@ workflowFormSchema, } from '../MesXslFormulaSpec.data'; import { saveOrUpdate, queryById, generateRubberCode as generateRubberCodeApi, getRubberContentSetting } from '../MesXslFormulaSpec.api'; + import { + FORMULA_SPEC_BIZ_TABLE, + hasTraceWorkflowInfo, + loadRecordWithTrace, + } from '/@/views/xslmes/approval/integration/traceRecordHelper'; import MesXslFormulaRubberContentSettingModal from './MesXslFormulaRubberContentSettingModal.vue'; import MesXslFormulaGenerateMixingModal from './MesXslFormulaGenerateMixingModal.vue'; import MesXslFormulaLineColumnSetting from './MesXslFormulaLineColumnSetting.vue'; @@ -464,9 +469,9 @@ const CATEGORY_DICT_CODE = 'xslmes_formula_spec_category'; const WORKFLOW_HEADER_DEFS = [ { key: 'compile', label: '编制', operatorField: 'createBy', operatorTextField: 'createBy_dictText' }, - { key: 'proofread', label: '校对', operatorField: 'proofreadBy', operatorTextField: 'proofreadBy_dictText' }, - { key: 'audit', label: '审核', operatorField: 'auditBy', operatorTextField: 'auditBy_dictText' }, - { key: 'approve', label: '批准', operatorField: 'approveBy', operatorTextField: 'approveBy_dictText' }, + { key: 'proofread', label: '校对', operatorField: 'traceProofreadBy', operatorTextField: 'traceProofreadBy_dictText' }, + { key: 'audit', label: '审核', operatorField: 'traceAuditBy', operatorTextField: 'traceAuditBy_dictText' }, + { key: 'approve', label: '批准', operatorField: 'traceApproveBy', operatorTextField: 'traceApproveBy_dictText' }, ] as const; const modalWidth = '96%'; @@ -681,6 +686,7 @@ const userInfo = userStore.getUserInfo || {}; const text = record?.[step.operatorTextField]; const raw = record?.[step.operatorField]; + // 编制:无 createBy 时回退当前登录人;校对/审核/批准仅展示痕迹表数据,无则留空 if (step.key === 'compile') { return resolveFormulaSpecUserDisplayName(raw, text, userInfo); } @@ -688,11 +694,15 @@ return String(text); } if (raw != null && raw !== '') { - return String(raw); + return resolveFormulaSpecUserDisplayName(raw, null, userInfo); } return ''; } + function loadMainRecordWithTrace(id: string, listRecord?: Recordable) { + return loadRecordWithTrace(id, FORMULA_SPEC_BIZ_TABLE, queryById, listRecord); + } + function formatCategoryShortLabel(text?: string) { if (!text) { return ''; @@ -930,14 +940,7 @@ } function hasWorkflowData(record: Recordable) { - return !!( - record?.proofreadBy || - record?.proofreadTime || - record?.auditBy || - record?.auditTime || - record?.approveBy || - record?.approveTime - ); + return hasTraceWorkflowInfo(record); } function resetFooterValues() { @@ -1133,8 +1136,7 @@ if (unref(isUpdate) && data?.record?.id) { lineLoading.value = true; try { - const mainRaw = await queryById({ id: data.record.id }); - const m = (mainRaw as any)?.id != null ? mainRaw : (mainRaw as any)?.result ?? mainRaw; + const m = await loadMainRecordWithTrace(data.record.id, data.record); applyMixingStages(m?.mixingStages); currentStatus.value = m?.status || 'compile'; const lines = m?.lineList?.length ? normalizeLineRows(m.lineList) : createEmptyLineRows(); diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts index 70e34f3..629cb92 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts @@ -694,11 +694,32 @@ export const downStepColumns: JVxeColumn[] = [...stepColumns]; //update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A19】TCU温度条件表列宽可调且表头换行----------- /** TCU 温度条件明细列宽偏好 localStorage 键 */ -export const MIXING_TCU_COLUMN_WIDTH_CACHE_KEY = 'mes_xsl_mixing_spec_tcu_column_widths_v2'; +export const MIXING_TCU_COLUMN_WIDTH_CACHE_KEY = 'mes_xsl_mixing_spec_tcu_column_widths_v3'; /** TCU 温度条件明细列可缩小到的最小宽度 */ export const MIXING_TCU_MIN_COLUMN_WIDTH = 48; +/** TCU 是否附加:否 */ +export const MIXING_TCU_ATTACH_NO = '0'; + +/** TCU 是否附加:是 */ +export const MIXING_TCU_ATTACH_YES = '1'; + +/** 判断 TCU 行是否允许维护附加重量 */ +export function isMixingTcuAttachEnabled(value: unknown): boolean { + return value === 1 || value === '1' || value === true; +} + +/** 规范化 TCU 行是否附加/重量联动 */ +export function normalizeMixingTcuAttachRow(row: Recordable) { + if (row.isAttach == null || row.isAttach === '') { + row.isAttach = MIXING_TCU_ATTACH_NO; + } + if (!isMixingTcuAttachEnabled(row.isAttach)) { + row.attachWeight = undefined; + } +} + export const tcuColumns: JVxeColumn[] = [ //update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A33】TCU区分固定上/下密炼机----------- { title: '区分', key: 'sectionType', type: JVxeTypes.select, dictCode: 'xslmes_mixing_tcu_section', disabled: true, width: 96, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' }, @@ -709,6 +730,29 @@ export const tcuColumns: JVxeColumn[] = [ { title: '后混炼室温度', key: 'rearChamberTemp', type: JVxeTypes.inputNumber, width: 76, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' }, { title: '上下顶栓温度', key: 'topPlugTemp', type: JVxeTypes.inputNumber, width: 76, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' }, { title: '药品称量位置', key: 'drugWeighPos', type: JVxeTypes.select, dictCode: 'xslmes_mixing_drug_weigh_pos', width: 76, minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, align: 'center' }, + //update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量----------- + { + title: '是否附加', + key: 'isAttach', + type: JVxeTypes.select, + dictCode: 'yn', + defaultValue: MIXING_TCU_ATTACH_NO, + width: 76, + minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, + align: 'center', + }, + { + title: '重量', + key: 'attachWeight', + type: JVxeTypes.inputNumber, + width: 76, + minWidth: MIXING_TCU_MIN_COLUMN_WIDTH, + align: 'center', + props: { + isDisabledCell: ({ row }: { row?: Recordable }) => !isMixingTcuAttachEnabled(row?.isAttach), + }, + }, + //update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU温度条件新增是否附加/重量----------- ]; /** 读取已保存的 TCU 温度条件明细列宽 */ @@ -1012,11 +1056,15 @@ export const MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS = 'drug_scale'; export function buildDefaultMixingTcuRows(rows: Recordable[] = []): Recordable[] { const up = rows.find((r) => r.sectionType === 'up_mixer') || - ({ sectionType: 'up_mixer', drugWeighPos: MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS } as Recordable); + ({ sectionType: 'up_mixer', drugWeighPos: MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS, isAttach: MIXING_TCU_ATTACH_NO } as Recordable); if (up.sectionType === 'up_mixer' && !up.drugWeighPos) { up.drugWeighPos = MIXING_TCU_UP_MIXER_DRUG_WEIGH_POS; } - const down = rows.find((r) => r.sectionType === 'down_mixer') || ({ sectionType: 'down_mixer', drugWeighPos: undefined } as Recordable); + const down = + rows.find((r) => r.sectionType === 'down_mixer') || + ({ sectionType: 'down_mixer', drugWeighPos: undefined, isAttach: MIXING_TCU_ATTACH_NO } as Recordable); + normalizeMixingTcuAttachRow(up); + normalizeMixingTcuAttachRow(down); return [up, down]; } //update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A33】TCU默认两行及上密炼机药品称默认值----------- diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue b/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue index 186faa2..e073665 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue @@ -510,6 +510,8 @@ import { DEFAULT_MIXING_STEP_ROW_COUNT, DEFAULT_MIXING_DOWN_STEP_ROW_COUNT, buildDefaultMixingTcuRows, + isMixingTcuAttachEnabled, + MIXING_TCU_ATTACH_NO, applyMixingMaterialFromSelection, fillMixingMaterialAccumWeight, calcMixingMaterialUnitWeightTotal, @@ -534,6 +536,8 @@ import { MIXING_STEP_MIN_COLUMN_WIDTH, } from '../MesXslMixingSpec.data'; import { saveOrUpdate, queryById } from '../MesXslMixingSpec.api'; +import { resolveFormulaSpecUserDisplayName } from '/@/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data'; +import { MIXING_SPEC_BIZ_TABLE, loadRecordWithTrace } from '/@/views/xslmes/approval/integration/traceRecordHelper'; import MesXslMixingMaterialColumnSetting from './MesXslMixingMaterialColumnSetting.vue'; import MesXslMixingTableRowHeightSetting from './MesXslMixingTableRowHeightSetting.vue'; import MesXslMixingStepSelectCell from './MesXslMixingStepSelectCell.vue'; @@ -949,15 +953,23 @@ function formatSignDate(value?: string) { } function refreshSignDisplay(row: Recordable = {}) { - signDisplay.draftBy = row.draftBy || row.createBy_dictText || row.createBy || ''; + const userInfo = userStore.getUserInfo || {}; + //update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】页脚起草人/变更人展示姓名----------- + signDisplay.draftBy = resolveFormulaSpecUserDisplayName( + row.draftBy || row.createBy, + row.draftBy_dictText || row.createBy_dictText, + userInfo, + ); + signDisplay.changeBy = resolveFormulaSpecUserDisplayName(row.updateBy, row.updateBy_dictText, userInfo); + //update-end---author:GHT ---date:2026-06-10 for:【混炼示方】页脚起草人/变更人展示姓名----------- signDisplay.draftTime = formatSignDateTime(row.draftTime || row.createTime); - signDisplay.proofreadBy = row.proofreadBy || row.proofreadBy_dictText || ''; - signDisplay.proofreadTime = formatSignDateTime(row.proofreadTime); - signDisplay.auditBy = row.auditBy || row.auditBy_dictText || ''; - signDisplay.auditTime = formatSignDateTime(row.auditTime); - signDisplay.approveBy = row.approveBy || row.approveBy_dictText || ''; - signDisplay.approveTime = formatSignDateTime(row.approveTime); - signDisplay.changeBy = row.updateBy_dictText || row.updateBy || ''; + // 校对/审核/批准:优先展示痕迹表注入字段 + signDisplay.proofreadBy = row.traceProofreadBy || ''; + signDisplay.proofreadTime = formatSignDateTime(row.traceProofreadTime); + signDisplay.auditBy = row.traceAuditBy || ''; + signDisplay.auditTime = formatSignDateTime(row.traceAuditTime); + signDisplay.approveBy = row.traceApproveBy || ''; + signDisplay.approveTime = formatSignDateTime(row.traceApproveTime); signDisplay.changeDate = formatSignDate(row.changeDate || row.updateTime); } //update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】页脚签章区只读展示----------- @@ -1057,8 +1069,7 @@ async function onSpecPickerEdit(payload: Recordable | null) { return; } try { - const raw = await queryById({ id: payload.mixingSpecId }); - const row = (raw as Recordable)?.specName != null ? raw : (raw as Recordable)?.result; + const row = await loadRecordWithTrace(payload.mixingSpecId, MIXING_SPEC_BIZ_TABLE, queryById); if (!row?.id) { createMessage.warning('未找到混炼示方数据'); return; @@ -1159,17 +1170,25 @@ function ensureTcuDefaultRows(rows: Recordable[] = []) { } function handleTcuValueChange(event) { - //update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】下密炼机禁用药品称量位置----------- const row = event?.row; const key = event?.column?.key; - if (!row || key !== 'drugWeighPos') { + if (!row || !key) { return; } - if (row.sectionType === 'down_mixer') { - row.drugWeighPos = undefined; - createMessage.warning('下密炼机不允许选择药品称量位置'); + //update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】下密炼机禁用药品称量位置----------- + if (key === 'drugWeighPos') { + if (row.sectionType === 'down_mixer') { + row.drugWeighPos = undefined; + createMessage.warning('下密炼机不允许选择药品称量位置'); + } + return; } //update-end---author:cursor ---date:20260522 for:【XSLMES-20260522-A17】下密炼机禁用药品称量位置----------- + //update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量----------- + if (key === 'isAttach' && !isMixingTcuAttachEnabled(row.isAttach)) { + row.attachWeight = undefined; + } + //update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量----------- } function resetSheetForm() { @@ -1231,8 +1250,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data await setProps({ disabled: !showFooter.value }); setModalProps({ showOkBtn: showFooter.value, showCancelBtn: showFooter.value, confirmLoading: false }); if (isUpdate.value && data?.record?.id) { - const raw = await queryById({ id: data.record.id }); - const row = raw?.result || raw; + const row = await loadRecordWithTrace(data.record.id, MIXING_SPEC_BIZ_TABLE, queryById, data.record); await applyMixingSpecPageData(row, 'edit'); } else { const userInfo = userStore.getUserInfo || {}; @@ -1271,7 +1289,9 @@ async function handleSubmit() { downStepList: cleanRows(downStepList), tcuList: tcuList.map((row) => ({ ...row, + isAttach: row.isAttach ?? MIXING_TCU_ATTACH_NO, drugWeighPos: row.sectionType === 'down_mixer' ? undefined : row.drugWeighPos, + attachWeight: isMixingTcuAttachEnabled(row.isAttach) ? row.attachWeight : undefined, })), }; setModalProps({ confirmLoading: true });