钉钉审批功能完善、混炼示方新增是否附加料
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -84,6 +84,9 @@ public class ApprovalTraceResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
}
|
||||
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<Object> {
|
||||
|
||||
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<Object> {
|
||||
|
||||
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<Object> {
|
||||
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) {
|
||||
|
||||
@@ -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<MesXslBizDoc
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService service;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Operation(summary = "审批注册-分页列表")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:list")
|
||||
@@ -81,4 +89,37 @@ public class MesXslBizDocRegistryController extends JeecgController<MesXslBizDoc
|
||||
MesXslBizDocRegistry entity = service.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【审批注册中心】物理表名下拉选择,查询当前库表清单-----------
|
||||
@Operation(summary = "查询当前数据库物理表(供注册中心下拉选择)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:list")
|
||||
@GetMapping("/dbTables")
|
||||
public Result<List<Map<String, String>>> 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<Object> 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<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
|
||||
List<Map<String, String>> options = new ArrayList<>(rows.size());
|
||||
for (Map<String, Object> row : rows) {
|
||||
String tableName = String.valueOf(row.get("tableName"));
|
||||
String comment = String.valueOf(row.get("tableComment"));
|
||||
Map<String, String> 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:【审批注册中心】物理表名下拉选择,查询当前库表清单-----------
|
||||
}
|
||||
|
||||
@@ -160,6 +160,25 @@ public class MesXslIntegrationPlanController extends JeecgController<MesXslInteg
|
||||
IntegrationPlanGenerator.parseStageOverrides(nodeBindings));
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】生成时支持手选识别环节-----------
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案-----------
|
||||
@Operation(summary = "为单个审批节点生成集成方案(流程设计器内使用)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@PostMapping("/generateForNode")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> generateForNode(@RequestBody Map<String, Object> 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】新增表字段元数据查询接口(可视化配置向导用)-----------
|
||||
|
||||
@@ -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 动作是否同步目标表痕迹-----------
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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<String> 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<String, Object> 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<String> listTargetBizIds(String targetTable, String targetField, String linkValue) {
|
||||
String sql = "SELECT id FROM `" + targetTable + "` WHERE `" + targetField + "` = ?";
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, linkValue);
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (Map<String, Object> 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 成功后按动作配置同步目标表痕迹-----------
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Map<String, Object>> 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<String> enabledStages = orderedEnabledStages(registry);
|
||||
List<FlowNode> flowNodes = parseApproverNodes(configJson);
|
||||
boolean nodeFound = flowNodes.stream().anyMatch(n -> nodeId.equals(n.nodeId));
|
||||
if (!nodeFound) {
|
||||
return Result.error("当前节点不在流程配置中,请确认流程设计已包含该节点");
|
||||
}
|
||||
|
||||
List<StatusDictItem> statusChain = loadStatusChain(registry);
|
||||
String initialStatus = resolveInitialStatus(statusChain, enabledStages);
|
||||
Map<String, String> overrides = Map.of(nodeId, stageKey.trim());
|
||||
List<StageBinding> 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<String, Object> 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<String> 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<Map<String, Object>> buildNodeGenerateResult(MesXslIntegrationPlan plan, boolean created,
|
||||
String phase, StageBinding binding) {
|
||||
Map<String, Object> 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<String, Object> buildPreview(String sourceTable, String flowId, Map<String, String> 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();
|
||||
}
|
||||
|
||||
@@ -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<MesXslDingPr
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
|
||||
@Autowired
|
||||
private IMesXslDingTplBindService dingTplBindService;
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】绑定MES审批流(审批人来源)-----------
|
||||
|
||||
@Operation(summary = "钉钉审批模板配置-分页列表查询")
|
||||
@@ -142,10 +147,59 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> 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<String> 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<String> 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<MesXslDingPr
|
||||
}
|
||||
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
Map<String, String> 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<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("processCode", code);
|
||||
row.put("name", name);
|
||||
@@ -263,6 +321,9 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
list.add(row);
|
||||
}
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】从钉钉同步时回写已导入模板名称-----------
|
||||
syncLocalTplNamesFromDingMap(dingNameByCode);
|
||||
//update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】从钉钉同步时回写已导入模板名称-----------
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】同步列表:已导入判定含 processCode 与同名本地草稿-----
|
||||
return Result.OK(list);
|
||||
} catch (Exception e) {
|
||||
@@ -358,6 +419,9 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
detail.put("schemaError", "AccessToken 获取失败,请检查钉钉应用配置");
|
||||
return Result.OK(detail);
|
||||
}
|
||||
if (oConvertUtils.isEmpty(tpl.getProcessCode())) {
|
||||
return Result.OK(detail);
|
||||
}
|
||||
try {
|
||||
String url = "https://api.dingtalk.com/v1.0/workflow/forms/schemas/processCodes"
|
||||
+ "?processCode=" + tpl.getProcessCode();
|
||||
@@ -373,6 +437,7 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
JSONObject ddResp = JSONObject.parseObject(respBody);
|
||||
if (ddResp.containsKey("code")) {
|
||||
detail.put("schemaError", ddResp.getString("message") + " (code=" + ddResp.getString("code") + ")");
|
||||
mergeDingTemplateName(detail, tpl, accessToken, null, ddResp);
|
||||
return Result.OK(detail);
|
||||
}
|
||||
|
||||
@@ -486,9 +551,11 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
}
|
||||
detail.put("dingFields", dingFields);
|
||||
detail.put("dingFieldsCount", dingFields.size());
|
||||
mergeDingTemplateName(detail, tpl, accessToken, root, ddResp);
|
||||
} catch (Exception e) {
|
||||
log.warn("钉钉 Schema 接口异常 processCode={}: {}", tpl.getProcessCode(), e.getMessage());
|
||||
detail.put("schemaError", "接口异常: " + e.getMessage());
|
||||
mergeDingTemplateName(detail, tpl, accessToken, null, null);
|
||||
}
|
||||
return Result.OK(detail);
|
||||
}
|
||||
@@ -571,38 +638,15 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
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<DingFormComponent> 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<String, Object> 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<String> pushResult = pushTemplateMetaToDingtalk(tpl);
|
||||
if (!pushResult.isSuccess()) {
|
||||
return Result.error(pushResult.getMessage());
|
||||
}
|
||||
Map<String, Object> 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<MesXslDingPr
|
||||
return mesXslDingProcessTplService.getOne(qw, false);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】模板名称 MES↔钉钉 双向同步-----------
|
||||
/** 将本地模板名称写入 tpl 表,并同步绑定表 template_name */
|
||||
private void applyLocalTplName(String tplId, String tplName) {
|
||||
if (oConvertUtils.isEmpty(tplId) || oConvertUtils.isEmpty(tplName)) {
|
||||
return;
|
||||
}
|
||||
String name = tplName.trim();
|
||||
MesXslDingProcessTpl update = new MesXslDingProcessTpl();
|
||||
update.setId(tplId);
|
||||
update.setTplName(name);
|
||||
mesXslDingProcessTplService.updateById(update);
|
||||
refreshBindTemplateName(tplId, name);
|
||||
}
|
||||
|
||||
/** 同步更新模板绑定表中的 template_name */
|
||||
private void refreshBindTemplateName(String templateId, String tplName) {
|
||||
if (oConvertUtils.isEmpty(templateId)) {
|
||||
return;
|
||||
}
|
||||
MesXslDingTplBind bindUpdate = new MesXslDingTplBind();
|
||||
bindUpdate.setTemplateName(tplName);
|
||||
dingTplBindService.update(bindUpdate, new QueryWrapper<MesXslDingTplBind>().eq("template_id", templateId));
|
||||
}
|
||||
|
||||
/** 从钉钉拉取模板名称并回写本地(getTemplateDetail / 设计器打开时) */
|
||||
private void mergeDingTemplateName(Map<String, Object> 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<String, String> 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<String, String> nameMap = fetchVisibleProcessNameMap(dtUserId, accessToken);
|
||||
return nameMap.get(processCode);
|
||||
} catch (Exception e) {
|
||||
log.warn("按 processCode 拉取钉钉模板名称失败 processCode={}: {}", processCode, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉取当前用户可见的钉钉审批模板 processCode → name 映射 */
|
||||
private Map<String, String> fetchVisibleProcessNameMap(String dtUserId, String accessToken) throws Exception {
|
||||
Map<String, String> 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<String> 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<DingFormComponent> 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<MesXslDingPr
|
||||
if (tpl == null) {
|
||||
return Result.error("未找到对应模板配置");
|
||||
}
|
||||
if (!"1".equals(tpl.getStatus())) {
|
||||
return Result.error("该审批模板已停用,无法发起钉钉审批");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(tpl.getProcessCode())) {
|
||||
return Result.error("该模板尚无 processCode,请先在钉钉管理后台创建审批模板");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.DingTplBindFieldValueResolver;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingTplBindService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -46,6 +47,8 @@ public class MesXslDingTplBindController {
|
||||
@Autowired(required = false)
|
||||
private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider;
|
||||
|
||||
@Autowired private DingTplBindFieldValueResolver fieldValueResolver;
|
||||
|
||||
// ═══════════════════════ 菜单树 ═══════════════════════
|
||||
|
||||
/**
|
||||
@@ -129,19 +132,7 @@ public class MesXslDingTplBindController {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
String code = bizCode.trim();
|
||||
if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) {
|
||||
return Result.OK(fieldCatalogProvider.listMainFields(code));
|
||||
}
|
||||
PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(code);
|
||||
if (vo == null || StringUtils.isBlank(vo.getDescription())) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
Class<?> 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<PrintBizFieldItemVO> 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<Map<String, Object>> 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<String, PrintBizFieldItemVO> metaMap = buildFieldMetaMap(code);
|
||||
Map<String, Object> 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<PrintBizFieldItemVO> listMainFieldsEnriched(String bizCode) {
|
||||
Class<?> cls = resolveEntityClass(bizCode);
|
||||
List<PrintBizFieldItemVO> 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<String, PrintBizFieldItemVO> buildFieldMetaMap(String bizCode) {
|
||||
Map<String, PrintBizFieldItemVO> 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<String, PrintBizFieldItemVO> map, String bizCode, String prop, String kind) {
|
||||
List<PrintBizFieldItemVO> 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<String, Object> rowData;
|
||||
private List<ResolveFieldItem> items;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ResolveFieldItem {
|
||||
/** 前端映射键,通常为 componentId */
|
||||
private String mapKey;
|
||||
private String bizField;
|
||||
/** raw 或 text */
|
||||
private String valueMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object>) 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<String, Object> map = (Map<String, Object>) 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<MesXslMixingSpecMap
|
||||
|
||||
private static final String TCU_UP = "up_mixer";
|
||||
private static final String TCU_DOWN = "down_mixer";
|
||||
private static final String TCU_ATTACH_YES = "1";
|
||||
private static final String TCU_ATTACH_NO = "0";
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
private static final Pattern GENERATED_B_RUBBER_SPEC_PATTERN = Pattern.compile("^B\\d", Pattern.CASE_INSENSITIVE);
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
@@ -71,6 +75,9 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
@Resource
|
||||
private IMesXslFormulaSpecEditLogService mesXslFormulaSpecEditLogService;
|
||||
|
||||
@Resource
|
||||
private ISysUserService sysUserService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveMain(
|
||||
@@ -235,6 +242,9 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
page.setStepList(queryStepByMainId(id));
|
||||
page.setDownStepList(queryDownStepByMainId(id));
|
||||
page.setTcuList(fillDefaultTcuRows(queryTcuByMainId(id)));
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】queryById补充起草人/变更人姓名-----------
|
||||
fillUserDisplayText(page);
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】queryById补充起草人/变更人姓名-----------
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -282,6 +292,33 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
return options;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】queryById补充起草人/变更人姓名-----------
|
||||
private void fillUserDisplayText(MesXslMixingSpecPage page) {
|
||||
if (page == null) {
|
||||
return;
|
||||
}
|
||||
page.setCreateBy_dictText(resolveUserRealname(page.getCreateBy()));
|
||||
page.setUpdateBy_dictText(resolveUserRealname(page.getUpdateBy()));
|
||||
String draftUsername = StringUtils.isNotBlank(page.getDraftBy()) ? page.getDraftBy() : page.getCreateBy();
|
||||
String draftRealname = resolveUserRealname(draftUsername);
|
||||
if (StringUtils.isBlank(draftRealname)) {
|
||||
draftRealname = page.getCreateBy_dictText();
|
||||
}
|
||||
page.setDraftBy_dictText(draftRealname);
|
||||
}
|
||||
|
||||
private String resolveUserRealname(String username) {
|
||||
if (StringUtils.isBlank(username)) {
|
||||
return null;
|
||||
}
|
||||
SysUser user = sysUserService.getUserByName(username);
|
||||
if (user != null && StringUtils.isNotBlank(user.getRealname())) {
|
||||
return user.getRealname();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】queryById补充起草人/变更人姓名-----------
|
||||
|
||||
private void normalizeMain(MesXslMixingSpec main) {
|
||||
if (main.getDraftTime() == null) {
|
||||
main.setDraftTime(new Date());
|
||||
@@ -484,6 +521,9 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
if (TCU_DOWN.equals(row.getSectionType())) {
|
||||
row.setDrugWeighPos(null);
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量-----------
|
||||
normalizeTcuAttachFields(row);
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量-----------
|
||||
rows.add(row);
|
||||
}
|
||||
if (trace != null) {
|
||||
@@ -663,13 +703,16 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
if (!hasUp) {
|
||||
MesXslMixingSpecTcu up = new MesXslMixingSpecTcu();
|
||||
up.setSectionType(TCU_UP);
|
||||
up.setIsAttach(TCU_ATTACH_NO);
|
||||
rows.add(0, up);
|
||||
}
|
||||
if (!hasDown) {
|
||||
MesXslMixingSpecTcu down = new MesXslMixingSpecTcu();
|
||||
down.setSectionType(TCU_DOWN);
|
||||
down.setIsAttach(TCU_ATTACH_NO);
|
||||
rows.add(down);
|
||||
}
|
||||
rows.forEach(this::normalizeTcuAttachFields);
|
||||
rows.sort((a, b) -> {
|
||||
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<MesXslMixingSpecMap
|
||||
return rows;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量-----------
|
||||
private void normalizeTcuAttachFields(MesXslMixingSpecTcu row) {
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(row.getIsAttach())) {
|
||||
row.setIsAttach(TCU_ATTACH_NO);
|
||||
}
|
||||
if (!TCU_ATTACH_YES.equals(row.getIsAttach())) {
|
||||
row.setAttachWeight(null);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【混炼示方】TCU是否附加为否时清空重量-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
/**
|
||||
* 删除混炼示方后,若该 B/F 段胶示方编号已无其它混炼示方、未被其它示方明细引用、且未被配合示方选作胶料代号,
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
@@ -46,6 +47,7 @@ public final class PrintBizEntityFieldIntrospector {
|
||||
PrintBizFieldItemVO vo =
|
||||
new PrintBizFieldItemVO(name, resolveLabel(f), "");
|
||||
fillJavaJdbcSimple(vo, f.getType());
|
||||
fillDictMeta(vo, f);
|
||||
ordered.putIfAbsent(name, vo);
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
@@ -223,6 +225,69 @@ public final class PrintBizEntityFieldIntrospector {
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【钉钉审批模板绑定】扫描@Dict并补全字典元数据-----------
|
||||
/** 扫描 @Dict 注解,供绑定页选择「原值/显示文本」 */
|
||||
public static void fillDictMeta(PrintBizFieldItemVO vo, Field f) {
|
||||
if (vo == null || f == null) {
|
||||
return;
|
||||
}
|
||||
Dict dict = f.getAnnotation(Dict.class);
|
||||
if (dict == null) {
|
||||
vo.setTranslateKind("NONE");
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isNotBlank(dict.dictTable())) {
|
||||
vo.setTranslateKind("TABLE");
|
||||
vo.setDictTable(dict.dictTable().trim());
|
||||
vo.setDictText(StringUtils.isNotBlank(dict.dicText()) ? dict.dicText().trim() : "");
|
||||
vo.setDictCodeField(StringUtils.isNotBlank(dict.dicCode()) ? dict.dicCode().trim() : "id");
|
||||
} else if (StringUtils.isNotBlank(dict.dicCode())) {
|
||||
vo.setTranslateKind("DICT");
|
||||
vo.setDictCode(dict.dicCode().trim());
|
||||
} else {
|
||||
vo.setTranslateKind("NONE");
|
||||
}
|
||||
}
|
||||
|
||||
/** 按字段名在实体类上补全字典元数据(catalog 缓存字段可能缺 translateKind) */
|
||||
public static void enrichDictMeta(List<PrintBizFieldItemVO> 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<String, Field> 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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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<string, any> = {};
|
||||
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<string, string> = {};
|
||||
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)) {
|
||||
|
||||
@@ -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<StageField[]>([]);
|
||||
@@ -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审批流设计】接收审批注册中心候选环节----- -->
|
||||
|
||||
@@ -133,7 +133,31 @@
|
||||
message="当前为流程最后一个审批节点:「批准」类方案触发时机为「审批通过」,已合并到下方下拉(带 [流程最终通过] 前缀)。"
|
||||
/>
|
||||
<div class="fd-ip-block">
|
||||
<div class="fd-ip-title">{{ isLastApproverNode ? '本节点通过 / 流程最终通过时执行' : '本节点通过时执行' }}</div>
|
||||
<div class="fd-ip-title-row">
|
||||
<div class="fd-ip-title">{{ isLastApproverNode ? '本节点通过 / 流程最终通过时执行' : '本节点通过时执行' }}</div>
|
||||
<div v-if="!readonly" class="fd-ip-title-actions">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
class="fd-ip-gen-btn"
|
||||
:loading="generatingPlan"
|
||||
:disabled="!canGeneratePlan"
|
||||
@click="handleGenerateIntegrationPlan"
|
||||
>
|
||||
生成集成方案
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="selectedPlanId"
|
||||
type="link"
|
||||
size="small"
|
||||
class="fd-ip-gen-btn"
|
||||
:loading="editingPlan"
|
||||
@click="handleEditIntegrationPlan"
|
||||
>
|
||||
编辑方案
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-select
|
||||
:value="primaryPlanValue"
|
||||
:disabled="readonly"
|
||||
@@ -224,6 +248,7 @@
|
||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<MesXslIntegrationActionDrawer ref="actionDrawerRef" />
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
@@ -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<Ref<string>>('approvalBizTable', ref(''));
|
||||
const flowId = inject<Ref<string>>('approvalFlowId', ref(''));
|
||||
const getFlowConfig = inject<(() => string) | null>('approvalFlowConfig', null);
|
||||
const flowRoot = inject<Ref<FlowNode | null>>('approvalFlowRoot', ref(null));
|
||||
const integrationPlansCacheKey = ref('');
|
||||
const generatingPlan = ref(false);
|
||||
const editingPlan = ref(false);
|
||||
const actionDrawerRef = ref();
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = { 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<string, string> = { 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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: '中文名称',
|
||||
|
||||
@@ -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<any>({ url: Api.generateDefaultFromFlow, params });
|
||||
|
||||
/** 流程设计器:为单个节点生成集成方案 */
|
||||
export const generateForNode = (params: {
|
||||
sourceTable: string;
|
||||
flowId: string;
|
||||
nodeId: string;
|
||||
stageKey: string;
|
||||
flowConfig?: string;
|
||||
overwriteDraft?: boolean;
|
||||
}) => defHttp.post<any>({ url: Api.generateForNode, params });
|
||||
|
||||
export const queryPlanById = (id: string) => defHttp.get<any>({ 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 });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -177,6 +177,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-06-10 for:【关联表痕迹同步】关联表动作可选同步主表审批痕迹 -->
|
||||
<div
|
||||
v-if="vc.visualType === 'STATUS_MODIFY' || vc.visualType === 'DATA_SYNC'"
|
||||
style="background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 6px; padding: 12px 14px; margin-bottom: 16px"
|
||||
>
|
||||
<a-checkbox v-model:checked="vc.syncTrace">同步主表审批痕迹到目标表</a-checkbox>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 8px; line-height: 1.6">
|
||||
开启后:环节通过时将主表当前审批人/时间写入目标表 <code>mes_xsl_approval_trace</code>;
|
||||
驳回时将按「新状态」清空对应环节痕迹。
|
||||
<span v-if="vc.visualType === 'DATA_SYNC'" style="color: #fa8c16">(驳回清空仅对「状态修改」动作生效)</span>
|
||||
目标表须已在审批注册中心启用对应环节。
|
||||
</div>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-06-10 for:【关联表痕迹同步】关联表动作可选同步主表审批痕迹 -->
|
||||
|
||||
<!-- ============ 状态修改(全新设计) ============ -->
|
||||
<template v-if="vc.visualType === 'STATUS_MODIFY'">
|
||||
|
||||
@@ -341,6 +356,8 @@
|
||||
statusConfig: StatusConfig;
|
||||
fieldMappings: FieldMapping[];
|
||||
registryStage?: RegistryStageConfig;
|
||||
/** 是否将主表审批痕迹同步到目标表 */
|
||||
syncTrace?: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{ success: [action: any] }>();
|
||||
@@ -431,6 +448,7 @@
|
||||
statusConfig: defaultStatusConfig(),
|
||||
fieldMappings: [],
|
||||
registryStage: defaultRegistryStage(),
|
||||
syncTrace: false,
|
||||
});
|
||||
|
||||
/** 兼容 Flyway 扁平格式(stage/expectedFrom 在顶层)与向导嵌套格式(registryStage 对象) */
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { queryByBiz } from './MesXslApprovalTrace.api';
|
||||
|
||||
/** 配合示方业务表(与审批注册中心 table_name 一致) */
|
||||
export const FORMULA_SPEC_BIZ_TABLE = 'mes_xsl_formula_spec';
|
||||
|
||||
/** 混炼示方业务表 */
|
||||
export const MIXING_SPEC_BIZ_TABLE = 'mes_xsl_mixing_spec';
|
||||
|
||||
const TRACE_FIELD_KEYS = [
|
||||
'traceProofreadBy',
|
||||
'traceProofreadTime',
|
||||
'traceAuditBy',
|
||||
'traceAuditTime',
|
||||
'traceApproveBy',
|
||||
'traceApproveTime',
|
||||
] as const;
|
||||
|
||||
/** 从列表/详情记录中提取已注入的痕迹字段 */
|
||||
export function pickTraceFields(record?: Recordable | null): Recordable {
|
||||
const out: Recordable = {};
|
||||
if (!record) return out;
|
||||
for (const key of TRACE_FIELD_KEYS) {
|
||||
if (record[key] != null && record[key] !== '') {
|
||||
out[key] = record[key];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 将痕迹表实体字段映射为列表注入格式 traceProofreadBy 等 */
|
||||
export function applyTraceEntityToRecord(record: Recordable, trace?: Recordable | null): Recordable {
|
||||
if (!record) return record;
|
||||
const merged = { ...record, ...pickTraceFields(record) };
|
||||
if (!trace) return merged;
|
||||
return {
|
||||
...merged,
|
||||
traceProofreadBy: trace.proofreadBy ?? merged.traceProofreadBy,
|
||||
traceProofreadTime: trace.proofreadTime ?? merged.traceProofreadTime,
|
||||
traceAuditBy: trace.auditBy ?? merged.traceAuditBy,
|
||||
traceAuditTime: trace.auditTime ?? merged.traceAuditTime,
|
||||
traceApproveBy: trace.approveBy ?? merged.traceApproveBy,
|
||||
traceApproveTime: trace.approveTime ?? merged.traceApproveTime,
|
||||
};
|
||||
}
|
||||
|
||||
export function hasTraceWorkflowInfo(record?: Recordable | null): boolean {
|
||||
return !!(
|
||||
record?.traceProofreadBy ||
|
||||
record?.traceProofreadTime ||
|
||||
record?.traceAuditBy ||
|
||||
record?.traceAuditTime ||
|
||||
record?.traceApproveBy ||
|
||||
record?.traceApproveTime
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeApiRecord(mainRaw: unknown): Recordable {
|
||||
const raw = mainRaw as Recordable;
|
||||
if (raw?.id != null) return raw;
|
||||
return (raw as any)?.result ?? raw ?? {};
|
||||
}
|
||||
|
||||
/** 加载业务主表并合并痕迹(列表注入 / queryById 增强 / queryByBiz 兜底) */
|
||||
export async function loadRecordWithTrace(
|
||||
id: string,
|
||||
bizTable: string,
|
||||
fetchById: (params: { id: string }) => Promise<unknown>,
|
||||
listRecord?: Recordable,
|
||||
): Promise<Recordable> {
|
||||
const mainRaw = await fetchById({ id });
|
||||
let record = normalizeApiRecord(mainRaw);
|
||||
record = applyTraceEntityToRecord(record, pickTraceFields(listRecord));
|
||||
if (!hasTraceWorkflowInfo(record)) {
|
||||
try {
|
||||
const trace = await queryByBiz({ bizTable, bizDataId: id });
|
||||
record = applyTraceEntityToRecord(record, trace);
|
||||
} catch {
|
||||
// 无痕迹或无权查询时忽略
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -31,7 +31,14 @@ export const saveBind = (data: {
|
||||
}) => defHttp.post({ url: `${BASE}/save`, data });
|
||||
|
||||
export const deleteBind = (id: string) =>
|
||||
defHttp.delete({ url: `${BASE}/delete`, params: { id } });
|
||||
defHttp.delete({ url: `${BASE}/delete`, params: { id } }, { joinParamsToUrl: true });
|
||||
|
||||
/** 批量解析绑定字段取值(字典/表字典显示文本) */
|
||||
export const resolveFieldValues = (data: {
|
||||
bizCode: string;
|
||||
rowData: Record<string, any>;
|
||||
items: { mapKey: string; bizField: string; valueMode: string }[];
|
||||
}) => defHttp.post<Record<string, any>>({ url: `${BASE}/resolveFieldValues`, data });
|
||||
|
||||
/** 复用现有接口:拉取钉钉模板表单字段(含 dingFields) */
|
||||
export const getTemplateDetail = (id: string) =>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/** 审批模板绑定字段取值:原值 / 显示文本 */
|
||||
|
||||
export type ValueMode = 'raw' | 'text';
|
||||
|
||||
export interface FieldTranslateMeta {
|
||||
fieldKey: string;
|
||||
label?: string;
|
||||
translateKind?: string;
|
||||
dictCode?: string;
|
||||
dictTable?: string;
|
||||
dictText?: string;
|
||||
dictCodeField?: string;
|
||||
}
|
||||
|
||||
export const VALUE_MODE_OPTIONS = [
|
||||
{ label: '原值(ID/Code)', value: 'raw' as ValueMode },
|
||||
{ label: '显示文本', value: 'text' as ValueMode },
|
||||
];
|
||||
|
||||
const DD_SELECT_TYPES = new Set([
|
||||
'DDSelectField',
|
||||
'DDMultiSelectField',
|
||||
'DepartmentField',
|
||||
'InnerContactField',
|
||||
]);
|
||||
|
||||
export function isTranslatableMeta(meta?: FieldTranslateMeta | null): boolean {
|
||||
return !!meta?.translateKind && meta.translateKind !== 'NONE';
|
||||
}
|
||||
|
||||
export function getNestedValue(obj: any, path: string): any {
|
||||
if (!obj || !path) return undefined;
|
||||
return path.split('.').reduce((acc: any, k: string) => acc?.[k], obj);
|
||||
}
|
||||
|
||||
export function getDictTextFromRow(rowData: any, bizField: string): any {
|
||||
if (!rowData || !bizField) return undefined;
|
||||
const parts = bizField.split('.');
|
||||
if (parts.length === 1) {
|
||||
return rowData[`${parts[0]}_dictText`];
|
||||
}
|
||||
const parentPath = parts.slice(0, -1).join('.');
|
||||
const leaf = parts[parts.length - 1];
|
||||
const parent = getNestedValue(rowData, parentPath);
|
||||
if (parent && typeof parent === 'object') {
|
||||
return parent[`${leaf}_dictText`];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 前端本地解析(优先 _dictText) */
|
||||
export function resolveFieldValueLocal(
|
||||
rowData: any,
|
||||
bizField: string,
|
||||
valueMode: ValueMode = 'raw',
|
||||
): any {
|
||||
const raw = getNestedValue(rowData, bizField);
|
||||
if (valueMode !== 'text') return raw;
|
||||
const textVal = getDictTextFromRow(rowData, bizField);
|
||||
if (textVal !== undefined && textVal !== null && textVal !== '') {
|
||||
return textVal;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** 绑定页默认取值方式:下拉类控件用原值,其余字典字段用显示文本 */
|
||||
export function defaultValueMode(
|
||||
componentName: string,
|
||||
meta?: FieldTranslateMeta | null,
|
||||
): ValueMode {
|
||||
if (!isTranslatableMeta(meta)) return 'raw';
|
||||
return DD_SELECT_TYPES.has(componentName) ? 'raw' : 'text';
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom:16px"
|
||||
message="绑定说明:将钉钉审批模板的表单控件与实体字段一一对应,系统自动发起审批时会从业务数据中读取字段值填入表单。TextNote 说明文字类控件无需绑定。明细表(TableField)需先指定对应的业务明细集合,再绑定各列字段。"
|
||||
message="绑定说明:将钉钉审批模板的表单控件与实体字段一一对应。带 @Dict 或表字典的字段可配置「原值/显示文本」:下拉类控件建议原值,文本类控件建议显示文本。明细表需先指定业务明细集合再绑定各列。"
|
||||
/>
|
||||
|
||||
<!-- ① 主表字段 -->
|
||||
@@ -169,8 +169,19 @@
|
||||
option-filter-prop="label"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="() => onMainBizFieldChange(record)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'valueMode'">
|
||||
<a-select
|
||||
v-if="record.bizField && isTranslatableMainField(record.bizField)"
|
||||
v-model:value="record.valueMode"
|
||||
style="width:100%"
|
||||
:options="VALUE_MODE_OPTIONS"
|
||||
size="small"
|
||||
/>
|
||||
<span v-else class="dtb-value-mode-hint">—</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
@@ -229,8 +240,19 @@
|
||||
option-filter-prop="label"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="() => onDetailBizFieldChange(record, tf.componentId)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'valueMode'">
|
||||
<a-select
|
||||
v-if="record.bizField && isTranslatableDetailField(record.bizField, tf.componentId)"
|
||||
v-model:value="record.valueMode"
|
||||
style="width:100%"
|
||||
:options="VALUE_MODE_OPTIONS"
|
||||
size="small"
|
||||
/>
|
||||
<span v-else class="dtb-value-mode-hint">—</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else description="该明细表无子控件" class="dtb-empty-sm" />
|
||||
@@ -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<FieldRow[]>([]);
|
||||
const tableFieldRows = ref<FieldRow[]>([]);
|
||||
|
||||
const bizFields = ref<{ fieldKey: string; label: string }[]>([]);
|
||||
const bizFields = ref<FieldTranslateMeta[]>([]);
|
||||
const bizFieldsLoading = ref(false);
|
||||
|
||||
const detailSlots = ref<DetailSlot[]>([]);
|
||||
const detailSlotsLoading = ref(false);
|
||||
|
||||
// key = tableField's componentId, value = loaded detail field options
|
||||
const detailFieldsMap = reactive<Record<string, { fieldKey: string; label: string }[]>>({});
|
||||
const detailFieldsMap = reactive<Record<string, FieldTranslateMeta[]>>({});
|
||||
const detailFieldsLoadingMap = reactive<Record<string, boolean>>({});
|
||||
|
||||
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<componentId, bizField> */
|
||||
function parseSavedMapping(json: string | null): Map<string, string | undefined> {
|
||||
const m = new Map<string, string | undefined>();
|
||||
/** 解析已保存的 fieldMappingJson */
|
||||
function parseSavedMapping(json: string | null): Map<string, SavedMappingEntry> {
|
||||
const m = new Map<string, SavedMappingEntry>();
|
||||
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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
<DingApprovalLaunchModal ref="launchModalRef" @success="handleLaunchSuccess" />
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程-->
|
||||
<BindApprovalFlowModal ref="bindFlowModalRef" @success="handleSuccess" />
|
||||
<!--update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】钉钉同步结果弹窗-->
|
||||
<a-modal
|
||||
v-model:open="syncVisible"
|
||||
@@ -154,8 +158,11 @@
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
import DingApprovalLaunchModal from './components/DingApprovalLaunchModal.vue';
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
//update-begin---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
import BindApprovalFlowModal from './components/BindApprovalFlowModal.vue';
|
||||
//update-end---author:GHT ---date:20260610 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslDingProcessTpl.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, syncFromDingtalk, batchImport, getTemplateDetail } from './MesXslDingProcessTpl.api';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, syncFromDingtalk, batchImport, getTemplateDetail, toggleTplStatus } from './MesXslDingProcessTpl.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const queryParam = reactive<any>({});
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
<!--
|
||||
钉钉审批模板 - 绑定 MES 审批流弹窗
|
||||
@author GHT
|
||||
@date 2026-06-10 for:【MESToDing审批配置】操作列绑定审批流程
|
||||
-->
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="绑定审批流程"
|
||||
width="660px"
|
||||
wrap-class-name="baf-modal-wrap"
|
||||
:body-style="{ padding: '8px 28px 20px' }"
|
||||
:confirm-loading="submitting"
|
||||
ok-text="确认绑定"
|
||||
cancel-text="取消"
|
||||
destroy-on-close
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="baf-body">
|
||||
<div v-if="tplRecord" class="baf-info-card">
|
||||
<a-descriptions :column="1" bordered size="small" class="baf-descriptions">
|
||||
<a-descriptions-item label="模板名称">{{ tplRecord.tplName || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="processCode">
|
||||
<a-typography-text v-if="tplRecord.processCode" code copyable>{{ tplRecord.processCode }}</a-typography-text>
|
||||
<a-tag v-else color="orange">未创建</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前绑定">
|
||||
<span v-if="currentFlowName" class="baf-bound-name">{{ currentFlowName }}</span>
|
||||
<span v-else class="baf-unbound">未绑定</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="baf-section">
|
||||
<a-form layout="vertical" class="baf-form">
|
||||
<a-form-item label="选择审批流程" required>
|
||||
<div class="baf-select-row">
|
||||
<a-select
|
||||
v-model:value="selectedFlowId"
|
||||
class="baf-flow-select"
|
||||
placeholder="请选择 MES 审批流"
|
||||
:loading="flowLoading"
|
||||
:options="flowSelectOptions"
|
||||
show-search
|
||||
:filter-option="filterFlowOption"
|
||||
allow-clear
|
||||
@change="handleFlowChange"
|
||||
>
|
||||
<template #option="{ label, status, remark, bizTableName }">
|
||||
<div class="baf-opt-item">
|
||||
<span class="baf-opt-name">{{ label }}</span>
|
||||
<span class="baf-opt-meta">
|
||||
<span v-if="bizTableName" class="baf-opt-remark">{{ bizTableName }}</span>
|
||||
<span v-if="remark" class="baf-opt-remark">{{ remark }}</span>
|
||||
<a-tag
|
||||
:color="status === '1' ? 'green' : status === '2' ? 'default' : 'orange'"
|
||||
class="baf-opt-tag"
|
||||
>
|
||||
{{ status === '1' ? '已发布' : status === '2' ? '已停用' : '草稿' }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
<a-button v-if="selectedFlowId" type="link" class="baf-design-btn" @click="handleDesignFlow">设计</a-button>
|
||||
</div>
|
||||
<div class="baf-hint">发起钉钉审批时,将按所选审批流解析各节点审批人</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFlowId" class="baf-preview-card">
|
||||
<div class="baf-preview-title">
|
||||
审批节点预览
|
||||
<a-spin :spinning="previewLoading" size="small" />
|
||||
</div>
|
||||
<div v-if="!previewLoading && approverPreview.length === 0" class="baf-preview-empty">
|
||||
该审批流暂无审批人节点,请先在流程设计器中配置
|
||||
</div>
|
||||
<div v-else class="baf-preview-list">
|
||||
<div v-for="(node, ni) in approverPreview" :key="node.nodeId || ni" class="baf-preview-node">
|
||||
<a-tag :color="node.nodeType === 'cc' ? 'blue' : 'orange'" class="baf-node-tag">
|
||||
{{ node.nodeType === 'cc' ? '抄送' : '审批' }}
|
||||
</a-tag>
|
||||
<span class="baf-preview-name">{{ node.nodeName }}</span>
|
||||
<span v-if="node.users?.length" class="baf-preview-users">
|
||||
{{ node.users.map((u) => u.realname || u.username).join('、') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-alert
|
||||
v-if="selectedFlowStatus && selectedFlowStatus !== '1'"
|
||||
type="warning"
|
||||
show-icon
|
||||
class="baf-warn-alert"
|
||||
message="所选审批流尚未发布,发起审批前请先发布流程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<FlowDesign @register="registerFlowDesign" @success="handleDesignSuccess" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { bindApprovalFlow, getApprovalFlowList, previewFlowApprovers } from '../MesXslDingProcessTpl.api';
|
||||
import FlowDesign from '/@/views/approval/flow/components/FlowDesign.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const tplRecord = ref<Recordable | null>(null);
|
||||
|
||||
const flowLoading = ref(false);
|
||||
const flowList = ref<any[]>([]);
|
||||
const selectedFlowId = ref('');
|
||||
const previewLoading = ref(false);
|
||||
const approverPreview = ref<any[]>([]);
|
||||
|
||||
const [registerFlowDesign, { openModal: openFlowDesign }] = useModal();
|
||||
|
||||
const flowSelectOptions = computed(() =>
|
||||
flowList.value.map((f) => ({
|
||||
value: f.id,
|
||||
label: f.flowName,
|
||||
status: f.status,
|
||||
remark: f.remark || '',
|
||||
bizTableName: f.bizTableName || '',
|
||||
})),
|
||||
);
|
||||
|
||||
const currentFlowName = computed(() => {
|
||||
if (!tplRecord.value?.flowId) return '';
|
||||
const flow = flowList.value.find((f) => f.id === tplRecord.value?.flowId);
|
||||
return flow?.flowName || tplRecord.value?.flowId;
|
||||
});
|
||||
|
||||
const selectedFlowStatus = computed(() => {
|
||||
if (!selectedFlowId.value) return '';
|
||||
return flowList.value.find((f) => f.id === selectedFlowId.value)?.status || '';
|
||||
});
|
||||
|
||||
function filterFlowOption(input: string, option: any) {
|
||||
return (option?.label ?? '').toLowerCase().includes(input.toLowerCase());
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
selectedFlowId.value = '';
|
||||
approverPreview.value = [];
|
||||
tplRecord.value = null;
|
||||
}
|
||||
|
||||
async function open(record: Recordable) {
|
||||
resetState();
|
||||
tplRecord.value = record;
|
||||
visible.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadFlowList();
|
||||
if (record.flowId) {
|
||||
selectedFlowId.value = record.flowId;
|
||||
await loadPreview(record.flowId);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function loadFlowList() {
|
||||
flowLoading.value = true;
|
||||
try {
|
||||
const res = await getApprovalFlowList({ pageSize: 500 });
|
||||
flowList.value = res?.records || res || [];
|
||||
} catch {
|
||||
flowList.value = [];
|
||||
} finally {
|
||||
flowLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFlowChange() {
|
||||
approverPreview.value = [];
|
||||
if (selectedFlowId.value) {
|
||||
await loadPreview(selectedFlowId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreview(flowId: string) {
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const res = await previewFlowApprovers(flowId);
|
||||
approverPreview.value = Array.isArray(res) ? res : [];
|
||||
} catch {
|
||||
approverPreview.value = [];
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDesignFlow() {
|
||||
const flow = flowList.value.find((f) => f.id === selectedFlowId.value);
|
||||
if (flow) {
|
||||
openFlowDesign(true, { record: flow, readonly: false });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDesignSuccess() {
|
||||
if (selectedFlowId.value) {
|
||||
await loadPreview(selectedFlowId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!tplRecord.value?.id) {
|
||||
createMessage.warning('模板信息无效');
|
||||
return;
|
||||
}
|
||||
if (!selectedFlowId.value) {
|
||||
createMessage.warning('请选择要绑定的审批流程');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await bindApprovalFlow({ id: tplRecord.value.id, flowId: selectedFlowId.value });
|
||||
createMessage.success('审批流程绑定成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '绑定失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.baf-body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.baf-info-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.baf-descriptions) {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-descriptions-item-label {
|
||||
width: 110px;
|
||||
background: #fafafa;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ant-descriptions-item-content {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.baf-bound-name {
|
||||
color: #1677ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.baf-unbound {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.baf-section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.baf-form {
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.baf-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.baf-flow-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.baf-design-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.baf-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.baf-opt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.baf-opt-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.baf-opt-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.baf-opt-remark {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.baf-opt-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.baf-preview-card {
|
||||
margin-top: 20px;
|
||||
padding: 14px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.baf-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.baf-preview-empty {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.baf-preview-list {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.baf-preview-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.baf-node-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.baf-preview-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.baf-preview-users {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.baf-warn-alert {
|
||||
margin-top: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.baf-modal-wrap {
|
||||
.ant-modal-footer {
|
||||
padding: 12px 28px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -676,6 +676,9 @@
|
||||
try {
|
||||
const detail = await getTemplateDetail(record.id);
|
||||
tplData.value = detail;
|
||||
if (detail?.dingNameSynced) {
|
||||
createMessage.info('已从钉钉同步最新模板名称');
|
||||
}
|
||||
|
||||
if (detail?.dingFields?.length) {
|
||||
// 以钉钉最新 schema 为准(保证结构与钉钉同步)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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默认两行及上密炼机药品称默认值-----------
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user