新增钉钉审批模板配置功能,包括相关实体、控制器、服务及接口的实现,支持审批模板的增删改查及从钉钉同步模板,增强了系统的审批流管理能力。
This commit is contained in:
@@ -1,14 +1,26 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.jeecg.dingtalk.api.core.response.Response;
|
||||
import com.jeecg.dingtalk.api.user.JdtUserAPI;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
@@ -22,6 +34,7 @@ import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
import org.jeecg.modules.system.service.ISysDepartService;
|
||||
import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixerPsCompile;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService;
|
||||
@@ -44,6 +57,11 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
@Autowired
|
||||
private ISysDepartService sysDepartService;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】注入钉钉服务-----
|
||||
@Autowired
|
||||
private ThirdAppDingtalkServiceImpl dingtalkService;
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】注入钉钉服务-----
|
||||
|
||||
@Operation(summary = "MES密炼PS编制-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslMixerPsCompile>> queryPageList(
|
||||
@@ -283,6 +301,122 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】保存前校验与冗余回填-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试接口-----
|
||||
@Operation(summary = "钉钉审批测试:用当前登录人手机号获取钉钉userId,并尝试PROC-GENERIC发起审批")
|
||||
@PostMapping(value = "/ddApprovalTest")
|
||||
public Result<Map<String, Object>> ddApprovalTest() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
// ① 取当前登录用户
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String username = loginUser.getUsername();
|
||||
String realname = oConvertUtils.isNotEmpty(loginUser.getRealname()) ? loginUser.getRealname() : username;
|
||||
String phone = loginUser.getPhone();
|
||||
result.put("step1_user", realname + "(" + username + ")");
|
||||
result.put("step1_phone", oConvertUtils.isNotEmpty(phone) ? phone : "【未配置手机号】");
|
||||
|
||||
if (oConvertUtils.isEmpty(phone)) {
|
||||
result.put("结论", "当前账号未配置手机号,无法查询钉钉userId");
|
||||
return Result.OK(result);
|
||||
}
|
||||
|
||||
// ② 获取 AccessToken
|
||||
String accessToken;
|
||||
try {
|
||||
accessToken = dingtalkService.getAccessToken();
|
||||
} catch (Exception e) {
|
||||
result.put("step2_accessToken", "获取失败: " + e.getMessage());
|
||||
return Result.OK(result);
|
||||
}
|
||||
if (oConvertUtils.isEmpty(accessToken)) {
|
||||
result.put("step2_accessToken", "获取失败,请在[系统配置-第三方应用]中检查钉钉配置");
|
||||
return Result.OK(result);
|
||||
}
|
||||
result.put("step2_accessToken", accessToken.substring(0, Math.min(10, accessToken.length())) + "...[已截断]");
|
||||
|
||||
// ③ 手机号查钉钉 userId
|
||||
Response<String> userIdResp = JdtUserAPI.getUseridByMobile(phone, accessToken);
|
||||
result.put("step3_getUseridByMobile_errcode", userIdResp.getErrcode());
|
||||
result.put("step3_getUseridByMobile_errmsg", userIdResp.getErrmsg());
|
||||
result.put("step3_dingtalkUserId", userIdResp.isSuccess() ? userIdResp.getResult() : "查询失败");
|
||||
|
||||
if (!userIdResp.isSuccess() || oConvertUtils.isEmpty(userIdResp.getResult())) {
|
||||
result.put("结论", "手机号未在钉钉通讯录中找到对应用户,请确认该手机号已在企业钉钉中注册");
|
||||
return Result.OK(result);
|
||||
}
|
||||
String dtUserId = userIdResp.getResult();
|
||||
|
||||
// ④ 用真实模板 processCode 发起审批实例
|
||||
try {
|
||||
String processCode = "PROC-71957EB4-6F64-4AD7-AA1C-3CD7E797687B"; // MES测试审批流
|
||||
String url = "https://api.dingtalk.com/v1.0/workflow/processInstances";
|
||||
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
|
||||
|
||||
// 表单字段(对应钉钉模板中的控件标题)
|
||||
JSONArray formValues = new JSONArray();
|
||||
for (String[] kv : new String[][]{
|
||||
{"PS编码", "TEST-" + System.currentTimeMillis() % 10000},
|
||||
{"类型", "MES测试"},
|
||||
{"发行日期", new SimpleDateFormat("yyyy-MM-dd").format(new Date())},
|
||||
{"发送部门", "技术部"},
|
||||
{"标题", "MES钉钉审批测试 " + now},
|
||||
}) {
|
||||
JSONObject f = new JSONObject();
|
||||
f.put("name", kv[0]);
|
||||
f.put("value", kv[1]);
|
||||
formValues.add(f);
|
||||
}
|
||||
|
||||
// 审批人:用当前登录人自己做测试审批人(and会签,只有1人)
|
||||
JSONArray approvers = new JSONArray();
|
||||
JSONObject approverNode = new JSONObject();
|
||||
approverNode.put("actionType", "AND");
|
||||
JSONArray approverIds = new JSONArray();
|
||||
approverIds.add(dtUserId);
|
||||
approverNode.put("userIds", approverIds);
|
||||
approvers.add(approverNode);
|
||||
|
||||
JSONObject reqBody = new JSONObject();
|
||||
reqBody.put("processCode", processCode);
|
||||
reqBody.put("originatorUserId", dtUserId);
|
||||
reqBody.put("deptId", -1);
|
||||
reqBody.put("approvers", approvers);
|
||||
reqBody.put("formComponentValues", formValues);
|
||||
|
||||
result.put("step4_请求体", reqBody);
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
HttpRequest httpReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("x-acs-dingtalk-access-token", accessToken)
|
||||
.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);
|
||||
result.put("step4_钉钉响应", ddResp);
|
||||
if (ddResp.containsKey("instanceId") || ddResp.containsKey("processInstanceId")) {
|
||||
String iid = ddResp.containsKey("instanceId")
|
||||
? ddResp.getString("instanceId")
|
||||
: ddResp.getString("processInstanceId");
|
||||
result.put("结论", "✅ 审批实例创建成功!instanceId=" + iid + ",请到钉钉「待我审批」查看");
|
||||
} else {
|
||||
result.put("结论", "❌ 创建失败,code=" + ddResp.getString("code") + " msg=" + ddResp.getString("message"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("钉钉审批实例创建测试异常", e);
|
||||
result.put("step4_钉钉响应", "HTTP请求异常: " + e.getMessage());
|
||||
result.put("结论", "❌ 请求钉钉接口失败");
|
||||
}
|
||||
|
||||
return Result.OK(result);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试接口-----
|
||||
|
||||
//update-begin---author:jiangxh ---date:20260520 for:【密炼PS编制】仅编制状态允许删除-----------
|
||||
private String assertCompileStatusForDelete(MesXslMixerPsCompile entity) {
|
||||
if (entity == null) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉审批表单控件
|
||||
* 对应官方 SDK:FormComponent
|
||||
*
|
||||
* 支持的 componentType:
|
||||
* TextField 单行文本
|
||||
* TextareaField 多行文本
|
||||
* NumberField 数字输入
|
||||
* DDSelectField 单选
|
||||
* DDMultiSelectField 多选
|
||||
* DDDateField 日期
|
||||
* DDDateRangeField 时间区间
|
||||
* DDPhotoField 图片
|
||||
* DDAttachment 附件
|
||||
* DepartmentField 部门
|
||||
* InnerContactField 联系人
|
||||
* TextNote 说明文字
|
||||
* MoneyField 金额
|
||||
* PhoneField 电话
|
||||
* AddressField 省市区
|
||||
* StarRatingField 评分
|
||||
* TableField 明细(含子控件 children)
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class DingFormComponent {
|
||||
|
||||
/** 控件类型 */
|
||||
private String componentType;
|
||||
|
||||
/** 控件属性 */
|
||||
private DingFormComponentProps props;
|
||||
|
||||
/** 子控件列表,仅 TableField 使用 */
|
||||
private List<DingFormComponent> children;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉审批表单控件属性
|
||||
* 对应官方 SDK:FormComponentProps
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class DingFormComponentProps {
|
||||
|
||||
/** 控件唯一标识,建议格式:{componentType}-{bizKey} */
|
||||
private String componentId;
|
||||
|
||||
/** 控件标题(钉钉表单中显示的字段名) */
|
||||
private String label;
|
||||
|
||||
/** 占位提示文字 */
|
||||
private String placeholder;
|
||||
|
||||
/** 是否必填 */
|
||||
private Boolean required;
|
||||
|
||||
/** 日期格式,DDDateField/DDDateRangeField 使用,如 "yyyy-MM-dd" */
|
||||
private String format;
|
||||
|
||||
/** 单位,NumberField/DDDateField/DDDateRangeField 使用 */
|
||||
private String unit;
|
||||
|
||||
/** 说明内容,TextNote 使用 */
|
||||
private String content;
|
||||
|
||||
/** 说明文字超链接,TextNote 使用 */
|
||||
private String link;
|
||||
|
||||
/** 是否参与打印("0"=否),TextNote 使用 */
|
||||
private String print;
|
||||
|
||||
/** 单选/多选选项列表,DDSelectField/DDMultiSelectField 使用 */
|
||||
private List<SelectOption> options;
|
||||
|
||||
/** 业务别名,DDSelectField 使用(如 "staff_type") */
|
||||
private String bizAlias;
|
||||
|
||||
/** 金额大写显示("0"=否 "1"=是),MoneyField 使用 */
|
||||
private String upper;
|
||||
|
||||
/** 电话模式("phone"),PhoneField 使用 */
|
||||
private String mode;
|
||||
|
||||
/** 联系人选择模式("1"=多选),InnerContactField 使用 */
|
||||
private String choice;
|
||||
|
||||
/** 部门是否多选,DepartmentField 使用 */
|
||||
private Boolean multiple;
|
||||
|
||||
/** 省市区精度("city"=市级 "district"=区级),AddressField 使用 */
|
||||
private String addressModel;
|
||||
|
||||
/** 评分最大值,StarRatingField 使用 */
|
||||
private Integer limit;
|
||||
|
||||
/** 明细视图模式("table"/"list"),TableField 使用 */
|
||||
private String tableViewMode;
|
||||
|
||||
/** 明细打印方向(true=纵向 false=横向),TableField 使用 */
|
||||
private Boolean verticalPrint;
|
||||
|
||||
/** 明细汇总字段,TableField 使用 */
|
||||
private List<StatField> statField;
|
||||
|
||||
/** 可关联的审批单列表,RelateField 使用 */
|
||||
private List<AvailableTemplate> availableTemplates;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class SelectOption {
|
||||
private String key;
|
||||
private String value;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class StatField {
|
||||
private String componentId;
|
||||
private String label;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class AvailableTemplate {
|
||||
private String name;
|
||||
private String processCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建钉钉审批模板请求体
|
||||
* POST https://api.dingtalk.com/v1.0/workflow/forms
|
||||
* 对应官方 SDK:FormCreateRequest
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class DingFormCreateRequest {
|
||||
|
||||
/** 模板名称 */
|
||||
private String name;
|
||||
|
||||
/** 模板描述 */
|
||||
private String description;
|
||||
|
||||
/** 表单控件列表 */
|
||||
private List<DingFormComponent> formComponents;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 更新钉钉审批模板请求体
|
||||
* PUT https://api.dingtalk.com/v1.0/workflow/forms
|
||||
* 对应官方 SDK:FormUpdateRequest
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class DingFormUpdateRequest {
|
||||
|
||||
/** 要更新的模板 processCode */
|
||||
private String processCode;
|
||||
|
||||
/** 模板名称 */
|
||||
private String name;
|
||||
|
||||
/** 模板描述 */
|
||||
private String description;
|
||||
|
||||
/** 表单控件列表 */
|
||||
private List<DingFormComponent> formComponents;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板配置
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-03 for:【MESToDing审批配置】钉钉审批模板配置
|
||||
*/
|
||||
@Data
|
||||
@TableName("mes_xsl_ding_process_tpl")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "钉钉审批模板配置")
|
||||
public class MesXslDingProcessTpl extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "模板名称", width = 20)
|
||||
@Schema(description = "模板名称")
|
||||
private String tplName;
|
||||
|
||||
@Excel(name = "钉钉processCode", width = 35)
|
||||
@Schema(description = "钉钉processCode")
|
||||
private String processCode;
|
||||
|
||||
@Excel(name = "业务类型标识", width = 20)
|
||||
@Schema(description = "业务类型标识(供审批流关联使用)")
|
||||
private String bizType;
|
||||
|
||||
@Excel(name = "表单字段映射", width = 30)
|
||||
@Schema(description = "表单字段映射JSON(钉钉字段名→MES字段名)")
|
||||
private String formFields;
|
||||
|
||||
@Excel(name = "状态", width = 10, dicCode = "mes_ding_tpl_status")
|
||||
@Dict(dicCode = "mes_ding_tpl_status")
|
||||
@Schema(description = "状态:0停用 1启用")
|
||||
private String status;
|
||||
|
||||
@Excel(name = "排序", width = 10)
|
||||
@Schema(description = "排序")
|
||||
private Integer sortNo;
|
||||
|
||||
@Excel(name = "备注", width = 30)
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】绑定MES审批流(审批人来源)-----------
|
||||
@Schema(description = "绑定的MES审批流ID(用于发起审批时解析审批人)")
|
||||
private String flowId;
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】绑定MES审批流(审批人来源)-----------
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板配置 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-03
|
||||
*/
|
||||
public interface MesXslDingProcessTplMapper extends BaseMapper<MesXslDingProcessTpl> {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingProcessTplMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板配置 Service 接口
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-03
|
||||
*/
|
||||
public interface IMesXslDingProcessTplService extends IService<MesXslDingProcessTpl> {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
|
||||
import org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingProcessTplMapper;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板配置 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-03
|
||||
*/
|
||||
@Service
|
||||
public class MesXslDingProcessTplServiceImpl extends ServiceImpl<MesXslDingProcessTplMapper, MesXslDingProcessTpl>
|
||||
implements IMesXslDingProcessTplService {
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
-- ============================================================
|
||||
-- MESToDing审批配置 - 钉钉审批模板配置
|
||||
-- author: GHT date: 2026-06-03
|
||||
-- ============================================================
|
||||
|
||||
-- ========== 建表 DDL ==========
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_ding_process_tpl` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`tpl_name` varchar(100) NOT NULL COMMENT '模板名称',
|
||||
`process_code` varchar(100) NOT NULL COMMENT '钉钉processCode',
|
||||
`biz_type` varchar(50) DEFAULT NULL COMMENT '业务类型标识(供审批流关联使用)',
|
||||
`form_fields` text DEFAULT NULL COMMENT '表单字段映射JSON(钉钉字段名→MES字段名)',
|
||||
`status` char(1) NOT NULL DEFAULT '1' COMMENT '状态:0停用 1启用',
|
||||
`sort_no` int NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1已删除',
|
||||
`tenant_id` int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES-钉钉审批模板配置';
|
||||
|
||||
-- ========== 字典 mes_ding_tpl_status ==========
|
||||
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`)
|
||||
VALUES (REPLACE(UUID(),'-',''), '钉钉审批模板状态', 'mes_ding_tpl_status', '钉钉审批模板启用/停用状态', 0, 'admin', NOW(), NULL, NULL, 0, 0);
|
||||
|
||||
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||
SELECT REPLACE(UUID(),'-',''), id, '启用', '1', NULL, 1, 1, 'admin', NOW(), NULL, NULL FROM `sys_dict` WHERE `dict_code` = 'mes_ding_tpl_status' LIMIT 1;
|
||||
|
||||
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||
SELECT REPLACE(UUID(),'-',''), id, '停用', '0', NULL, 2, 1, 'admin', NOW(), NULL, NULL FROM `sys_dict` WHERE `dict_code` = 'mes_ding_tpl_status' LIMIT 1;
|
||||
|
||||
-- ========== 菜单权限 ==========
|
||||
-- 注意:该页面对应的前台目录为 views/xslmes/dingtalk/mesXslDingProcessTpl 文件夹下
|
||||
|
||||
-- 父菜单:MESToDing审批配置(目录级,is_leaf=0)
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
|
||||
VALUES ('178046026420801', NULL, 'MESToDing审批配置', '/mestoding', 'layouts/RouteView', NULL, NULL, 0, NULL, '1', 99.00, 0, 'ant-design:dingtalk-outlined', 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0);
|
||||
|
||||
-- 子菜单:钉钉审批模板配置(is_leaf=0,有按钮子级)
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
|
||||
VALUES ('178046026420802', '178046026420801', '钉钉审批模板配置', '/xslmes/mesXslDingProcessTplList', 'xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList', NULL, NULL, 0, NULL, '1', 1.00, 0, NULL, 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0);
|
||||
|
||||
-- 按钮权限(parent_id = 178046026420802,is_leaf=1)
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('178046026420803', '178046026420802', '添加钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('178046026420804', '178046026420802', '编辑钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('178046026420805', '178046026420802', '删除钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('178046026420806', '178046026420802', '批量删除钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('178046026420807', '178046026420802', '导出excel_钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('178046026420808', '178046026420802', '导入excel_钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
|
||||
|
||||
-- ========== admin 角色授权(role_id = f6817f48af4fb3af11b9e8bf182f618b)==========
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420801', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420802', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420803', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420804', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420805', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420806', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420807', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420808', NULL, '2026-06-03 00:00:00', '127.0.0.1');
|
||||
@@ -0,0 +1,6 @@
|
||||
-- ============================================================
|
||||
-- 钉钉审批模板配置 - 新增绑定MES审批流字段
|
||||
-- author: GHT date: 2026-06-04
|
||||
-- ============================================================
|
||||
ALTER TABLE `mes_xsl_ding_process_tpl`
|
||||
ADD COLUMN `flow_id` varchar(32) DEFAULT NULL COMMENT '绑定的MES审批流ID(用于发起审批时解析审批人)';
|
||||
@@ -53,7 +53,10 @@
|
||||
</template>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人配置----- -->
|
||||
<a-form-item v-if="form.props.approverType === 'user'" label="指定成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" @change="onUserTextChange" />
|
||||
<div v-if="form.props.multiMode === 'none'" style="font-size:12px;color:#888;margin-top:3px">
|
||||
单人审批只能指定一位,已自动保留第一位
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.approverType === 'role'" label="指定角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
@@ -65,12 +68,16 @@
|
||||
<a-select-option :value="3">第3级主管</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="多人审批方式">
|
||||
<a-radio-group v-model:value="form.props.multiMode" :disabled="readonly">
|
||||
<a-radio value="and">会签(需全部同意)</a-radio>
|
||||
<a-form-item label="审批方式">
|
||||
<a-radio-group v-model:value="form.props.multiMode" :disabled="readonly" @change="onMultiModeChange">
|
||||
<a-radio value="none">单人审批</a-radio>
|
||||
<a-radio value="and">会签(全部同意)</a-radio>
|
||||
<a-radio value="or">或签(一人同意)</a-radio>
|
||||
<a-radio value="sequence">依次审批</a-radio>
|
||||
</a-radio-group>
|
||||
<div v-if="form.props.multiMode === 'none'" style="font-size:12px;color:#ff7a00;margin-top:4px">
|
||||
单人审批:仅允许指定一位审批人,对应钉钉 actionType = NONE
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批人为空时">
|
||||
<a-radio-group v-model:value="form.props.emptyStrategy" :disabled="readonly">
|
||||
@@ -193,7 +200,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, inject, watch } from 'vue';
|
||||
import { computed, ref, inject, watch, nextTick } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
@@ -317,12 +324,49 @@
|
||||
list.push({ name: raw.name, method: raw.method || 'POST', url: raw.url });
|
||||
}
|
||||
|
||||
/** 切换审批方式时,若切为单人则自动裁剪 userText 至第一个 */
|
||||
function onMultiModeChange() {
|
||||
if (!form.value) return;
|
||||
if (form.value.props.multiMode === 'none') {
|
||||
trimToSingleUser();
|
||||
}
|
||||
}
|
||||
|
||||
/** userText 变化时,若当前是单人模式则裁剪 */
|
||||
function onUserTextChange() {
|
||||
if (!form.value) return;
|
||||
if (form.value.props.multiMode === 'none') {
|
||||
nextTick(trimToSingleUser);
|
||||
}
|
||||
}
|
||||
|
||||
function trimToSingleUser() {
|
||||
if (!form.value) return;
|
||||
const ut: string = form.value.props.userText || '';
|
||||
const parts = ut.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
if (parts.length > 1) {
|
||||
form.value.props.userText = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
if (node.value && form.value) {
|
||||
// 单人审批最终兜底校验
|
||||
if (
|
||||
node.value.type === 'approver' &&
|
||||
form.value.props.multiMode === 'none' &&
|
||||
form.value.props.approverType === 'user'
|
||||
) {
|
||||
const names = (form.value.props.userText || '').split(',').filter(Boolean);
|
||||
if (names.length > 1) {
|
||||
form.value.props.userText = names[0];
|
||||
createMessage.warning('单人审批已自动保留第一位审批人');
|
||||
}
|
||||
}
|
||||
node.value.name = form.value.name;
|
||||
node.value.props = cloneDeep(form.value.props);
|
||||
emit('confirm', node.value);
|
||||
|
||||
@@ -55,7 +55,7 @@ export function createApproverNode(): FlowNode {
|
||||
userText: '',
|
||||
roleList: [],
|
||||
leaderLevel: 1,
|
||||
// 多人审批方式 and会签(需全部同意) / or或签(一人同意) / sequence依次审批
|
||||
// 多人审批方式 and会签 / or或签 / sequence依次 / none单人审批(仅一人)
|
||||
multiMode: 'and',
|
||||
// 审批人为空时 pass自动通过 / admin转交管理员 / stop终止
|
||||
emptyStrategy: 'admin',
|
||||
@@ -169,13 +169,19 @@ export function nodeSummary(node: FlowNode): string {
|
||||
}
|
||||
if (node.type === 'approver') {
|
||||
const t = node.props.approverType;
|
||||
if (t === 'self') return '发起人自己';
|
||||
if (t === 'leader') return `第${node.props.leaderLevel || 1}级主管`;
|
||||
if (t === 'role') return node.props.roleList?.length ? `角色审批(${node.props.roleList.length})` : '请设置审批角色';
|
||||
const mode = node.props.multiMode;
|
||||
const modeTag = mode === 'none' ? '单人' : mode === 'or' ? '或签' : mode === 'sequence' ? '依次' : '会签';
|
||||
if (t === 'self') return `发起人自己(${modeTag})`;
|
||||
if (t === 'leader') return `第${node.props.leaderLevel || 1}级主管(${modeTag})`;
|
||||
if (t === 'role') return node.props.roleList?.length ? `角色审批(${node.props.roleList.length}人)` : '请设置审批角色';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人摘要-----
|
||||
if (t === 'field') return `取单据字段:${node.props.fieldLabel || node.props.fieldName || '未指定字段'}`;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人摘要-----
|
||||
return node.props.userText ? `指定成员:${node.props.userText}` : '请设置审批人';
|
||||
if (!node.props.userText) return '请设置审批人';
|
||||
const names = node.props.userText.split(',').filter(Boolean);
|
||||
return mode === 'none'
|
||||
? `单人审批:${names[0] || '请选择'}`
|
||||
: `${modeTag}(${names.length}人):${names.slice(0, 2).join('、')}${names.length > 2 ? '...' : ''}`;
|
||||
}
|
||||
if (node.type === 'cc') {
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段抄送人摘要-----
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslDingProcessTpl/list',
|
||||
save = '/xslmes/mesXslDingProcessTpl/add',
|
||||
edit = '/xslmes/mesXslDingProcessTpl/edit',
|
||||
deleteOne = '/xslmes/mesXslDingProcessTpl/delete',
|
||||
deleteBatch = '/xslmes/mesXslDingProcessTpl/deleteBatch',
|
||||
importExcel = '/xslmes/mesXslDingProcessTpl/importExcel',
|
||||
exportXls = '/xslmes/mesXslDingProcessTpl/exportXls',
|
||||
syncFromDingtalk = '/xslmes/mesXslDingProcessTpl/syncFromDingtalk',
|
||||
batchImport = '/xslmes/mesXslDingProcessTpl/batchImport',
|
||||
getTemplateDetail = '/xslmes/mesXslDingProcessTpl/getTemplateDetail',
|
||||
saveFieldMapping = '/xslmes/mesXslDingProcessTpl/saveFieldMapping',
|
||||
createDingTemplate = '/xslmes/mesXslDingProcessTpl/createDingTemplate',
|
||||
updateDingTemplate = '/xslmes/mesXslDingProcessTpl/updateDingTemplate',
|
||||
launchApproval = '/xslmes/mesXslDingProcessTpl/launchApproval',
|
||||
bindFlow = '/xslmes/mesXslDingProcessTpl/bindFlow',
|
||||
approvalFlowList = '/xslmes/approvalFlow/list',
|
||||
previewFlowApprovers = '/xslmes/mesXslDingProcessTpl/previewFlowApprovers',
|
||||
}
|
||||
|
||||
export const getExportUrl = Api.exportXls;
|
||||
export const getImportUrl = Api.importExcel;
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
export const deleteOne = (params, handleSuccess) =>
|
||||
defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
|
||||
|
||||
export const batchDelete = (params, handleSuccess) => {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '确认删除',
|
||||
content: '是否删除选中数据',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () =>
|
||||
defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => handleSuccess()),
|
||||
});
|
||||
};
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) =>
|
||||
defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const syncFromDingtalk = () => defHttp.get({ url: Api.syncFromDingtalk }, { successMessageMode: 'none' });
|
||||
|
||||
export const batchImport = (params) => defHttp.post({ url: Api.batchImport, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const getTemplateDetail = (id: string) =>
|
||||
defHttp.get({ url: Api.getTemplateDetail, params: { id } }, { successMessageMode: 'none' });
|
||||
|
||||
export const queryById = (id: string) =>
|
||||
defHttp.get({ url: '/xslmes/mesXslDingProcessTpl/queryById', params: { id } }, { successMessageMode: 'none' });
|
||||
|
||||
export const saveFieldMapping = (params) =>
|
||||
defHttp.post({ url: Api.saveFieldMapping, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const createDingTemplate = (params) =>
|
||||
defHttp.post({ url: Api.createDingTemplate, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const updateDingTemplate = (params) =>
|
||||
defHttp.post({ url: Api.updateDingTemplate, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const launchApproval = (params) =>
|
||||
defHttp.post({ url: Api.launchApproval, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const bindApprovalFlow = (params: { id: string; flowId: string }) =>
|
||||
defHttp.post({ url: Api.bindFlow, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const getApprovalFlowList = (params?) =>
|
||||
defHttp.get({ url: Api.approvalFlowList, params }, { successMessageMode: 'none' });
|
||||
|
||||
export const previewFlowApprovers = (flowId: string) =>
|
||||
defHttp.get({ url: Api.previewFlowApprovers, params: { flowId } }, { successMessageMode: 'none' });
|
||||
@@ -0,0 +1,102 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '模板名称', align: 'center', dataIndex: 'tplName', width: 180 },
|
||||
{ title: '钉钉processCode', align: 'center', dataIndex: 'processCode', width: 280 },
|
||||
{ title: '业务类型标识', align: 'center', dataIndex: 'bizType', width: 140 },
|
||||
{ title: '状态', align: 'center', dataIndex: 'status_dictText', width: 90 },
|
||||
{ title: '排序', align: 'center', dataIndex: 'sortNo', width: 80 },
|
||||
{ title: '备注', align: 'center', dataIndex: 'remark', width: 200 },
|
||||
{ title: '创建时间', align: 'center', dataIndex: 'createTime', width: 160 },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '模板名称',
|
||||
field: 'tplName',
|
||||
component: 'JInput',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '钉钉processCode',
|
||||
field: 'processCode',
|
||||
component: 'JInput',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '业务类型标识',
|
||||
field: 'bizType',
|
||||
component: 'JInput',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'mes_ding_tpl_status' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{
|
||||
label: '模板名称',
|
||||
field: 'tplName',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '请输入模板名称' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入模板名称!' }],
|
||||
},
|
||||
{
|
||||
label: 'processCode',
|
||||
field: 'processCode',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '请输入钉钉processCode,如 PROC-XXXXXXXX-...' },
|
||||
dynamicRules: () => [{ required: true, message: '请输入processCode!' }],
|
||||
},
|
||||
{
|
||||
label: '业务类型标识',
|
||||
field: 'bizType',
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '供审批流关联使用,如 mixer_ps、formula_spec' },
|
||||
},
|
||||
{
|
||||
label: '表单字段映射',
|
||||
field: 'formFields',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '{"PS编码":"psCode","类型":"type"} —— 钉钉模板字段名→MES字段名',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_ding_tpl_status',
|
||||
placeholder: '请选择状态',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
field: 'sortNo',
|
||||
component: 'InputNumber',
|
||||
componentProps: { placeholder: '请输入排序值', style: 'width:100%' },
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { placeholder: '请输入备注', rows: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
export const superQuerySchema = {
|
||||
tplName: { title: '模板名称', order: 0, view: 'text', type: 'string' },
|
||||
processCode: { title: 'processCode', order: 1, view: 'text', type: 'string' },
|
||||
bizType: { title: '业务类型标识', order: 2, view: 'text', type: 'string' },
|
||||
status: { title: '状态', order: 3, view: 'list', type: 'string', dictCode: 'mes_ding_tpl_status' },
|
||||
sortNo: { title: '排序', order: 4, view: 'number', type: 'number' },
|
||||
};
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_ding_process_tpl:add'" preIcon="ant-design:plus-outlined" @click="handleAdd">
|
||||
新增
|
||||
</a-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_ding_process_tpl:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">
|
||||
导出
|
||||
</a-button>
|
||||
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_ding_process_tpl:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">
|
||||
导入
|
||||
</j-upload-button>
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】从钉钉同步按钮-->
|
||||
<a-button preIcon="ant-design:sync-outlined" :loading="syncLoading" @click="handleSyncFromDingtalk">
|
||||
从钉钉同步
|
||||
</a-button>
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】从钉钉同步按钮-->
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="batchHandleDelete">
|
||||
<Icon icon="ant-design:delete-outlined" />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button v-auth="'xslmes:mes_xsl_ding_process_tpl:deleteBatch'">
|
||||
批量操作
|
||||
<Icon icon="mdi:chevron-down" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<MesXslDingProcessTplModal @register="registerModal" @success="handleSuccess" />
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】钉钉字段详情弹窗(只读)-->
|
||||
<a-modal
|
||||
v-model:open="schemaVisible"
|
||||
title="钉钉模板字段详情"
|
||||
width="720px"
|
||||
:footer="null"
|
||||
destroy-on-close
|
||||
@cancel="schemaVisible = false"
|
||||
>
|
||||
<a-spin :spinning="schemaLoading">
|
||||
<template v-if="schemaData">
|
||||
<a-descriptions :column="2" bordered size="small" style="margin-bottom:14px">
|
||||
<a-descriptions-item label="模板名称">{{ schemaData.tplName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="业务类型">{{ schemaData.bizType || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="processCode" :span="2">
|
||||
<a-typography-text v-if="schemaData.processCode" code copyable>{{ schemaData.processCode }}</a-typography-text>
|
||||
<a-tag v-else color="orange">未创建</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-alert v-if="schemaData.schemaError" type="warning" :message="schemaData.schemaError" show-icon style="margin-bottom:12px" />
|
||||
<template v-if="schemaData.dingFields?.length">
|
||||
<div style="font-weight:600;margin-bottom:8px">
|
||||
钉钉表单字段
|
||||
<a-tag color="blue" style="margin-left:6px;font-weight:400">{{ schemaData.dingFields.length }} 个</a-tag>
|
||||
</div>
|
||||
<a-table
|
||||
:dataSource="schemaData.dingFields"
|
||||
:columns="dingFieldColumns"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:rowKey="(_, i) => i"
|
||||
:scroll="{ y: 300 }"
|
||||
/>
|
||||
</template>
|
||||
<div v-else-if="!schemaData.schemaError" style="color:#999;text-align:center;padding:20px">
|
||||
未从钉钉获取到字段(模板可能无 processCode 或字段为空)
|
||||
</div>
|
||||
<a-collapse style="margin-top:14px" :bordered="false">
|
||||
<a-collapse-panel key="json" header="原始 JSON 数据" style="background:#fafafa">
|
||||
<pre style="font-size:12px;margin:0;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all">{{ JSON.stringify(schemaData, null, 2) }}</pre>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】钉钉字段详情弹窗(只读)-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】表单设计器-->
|
||||
<DingTplDesigner ref="designerRef" @success="handleSuccess" />
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】表单设计器-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批-->
|
||||
<DingApprovalLaunchModal ref="launchModalRef" @success="handleLaunchSuccess" />
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批-->
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】钉钉同步结果弹窗-->
|
||||
<a-modal
|
||||
v-model:open="syncVisible"
|
||||
title="从钉钉同步审批模板"
|
||||
width="780px"
|
||||
:confirmLoading="importLoading"
|
||||
okText="导入选中"
|
||||
cancelText="取消"
|
||||
@ok="handleBatchImport"
|
||||
@cancel="syncVisible = false"
|
||||
>
|
||||
<a-spin :spinning="syncLoading">
|
||||
<a-alert v-if="syncList.length === 0 && !syncLoading" message="未获取到钉钉审批模板,请确认钉钉配置及账号绑定" type="warning" show-icon style="margin-bottom:12px" />
|
||||
<a-table
|
||||
v-if="syncList.length > 0"
|
||||
:dataSource="syncList"
|
||||
:columns="syncColumns"
|
||||
:rowSelection="{ type: 'checkbox', selectedRowKeys: syncSelectedKeys, onChange: onSyncSelectChange }"
|
||||
:rowKey="(r) => r.processCode"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:scroll="{ y: 380 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'imported'">
|
||||
<a-tag :color="record.imported ? 'green' : 'default'">{{ record.imported ? '已导入' : '未导入' }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】钉钉同步结果弹窗-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslDingProcessTpl" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import Icon from '/@/components/Icon';
|
||||
import MesXslDingProcessTplModal from './components/MesXslDingProcessTplModal.vue';
|
||||
import DingTplDesigner from './components/DingTplDesigner.vue';
|
||||
//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审批配置】手动填表发起钉钉审批
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslDingProcessTpl.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, syncFromDingtalk, batchImport, getTemplateDetail } from './MesXslDingProcessTpl.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const queryParam = reactive<any>({});
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
title: '钉钉审批模板配置',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: true,
|
||||
},
|
||||
actionColumn: {
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
beforeFetch: (params) => Object.assign(params, queryParam),
|
||||
},
|
||||
exportConfig: { name: '钉钉审批模板配置', url: getExportUrl, params: queryParam },
|
||||
importConfig: { url: getImportUrl, success: handleSuccess },
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
const superQueryConfig = reactive(superQuerySchema);
|
||||
|
||||
function handleSuperQuery(params) {
|
||||
Object.keys(params).forEach((k) => (queryParam[k] = params[k]));
|
||||
reload();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false, showFooter: true });
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: true });
|
||||
}
|
||||
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: false });
|
||||
}
|
||||
|
||||
async function handleDelete(record) {
|
||||
await deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
async function batchHandleDelete() {
|
||||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
(selectedRowKeys.value = []) && reload();
|
||||
}
|
||||
|
||||
function getTableAction(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审批配置】操作列新增发起审批按钮
|
||||
{
|
||||
label: '发起审批',
|
||||
icon: 'ant-design:send-outlined',
|
||||
color: 'success',
|
||||
disabled: !record.processCode,
|
||||
tooltip: record.processCode ? '手动填表后发起钉钉审批' : '请先配置 processCode',
|
||||
onClick: handleLaunchApproval.bind(null, record),
|
||||
},
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】操作列新增发起审批按钮
|
||||
];
|
||||
}
|
||||
|
||||
function getDropDownAction(record) {
|
||||
return [
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
auth: 'xslmes:mes_xsl_ding_process_tpl:delete',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ===== 手动填表发起钉钉审批 =====
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
const launchModalRef = ref();
|
||||
|
||||
function handleLaunchApproval(record: Recordable) {
|
||||
if (!record.processCode) {
|
||||
createMessage.warning('该模板尚未配置 processCode,请先完成模板配置');
|
||||
return;
|
||||
}
|
||||
launchModalRef.value?.open(record);
|
||||
}
|
||||
|
||||
function handleLaunchSuccess() {
|
||||
// 发起成功后可按需刷新列表(本期无需刷新,审批实例不在此列表)
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批
|
||||
|
||||
// ===== 表单设计器 =====
|
||||
const designerRef = ref();
|
||||
|
||||
function handleDesignTemplate(record: Recordable) {
|
||||
designerRef.value?.open(record);
|
||||
}
|
||||
|
||||
// ===== 钉钉字段详情(只读 schema 查看器)=====
|
||||
const schemaVisible = ref(false);
|
||||
const schemaLoading = ref(false);
|
||||
const schemaData = ref<any>(null);
|
||||
|
||||
const dingFieldColumns = [
|
||||
{ title: '控件标题(钉钉字段名)', dataIndex: 'label' },
|
||||
{ title: '控件类型', dataIndex: 'componentName', width: 160 },
|
||||
{ title: '必填', dataIndex: 'required', width: 70 },
|
||||
];
|
||||
|
||||
async function handleShowDingSchema(record: Recordable) {
|
||||
schemaVisible.value = true;
|
||||
schemaLoading.value = true;
|
||||
schemaData.value = null;
|
||||
try {
|
||||
schemaData.value = await getTemplateDetail(record.id);
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取模板字段失败');
|
||||
schemaVisible.value = false;
|
||||
} finally {
|
||||
schemaLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 从钉钉同步 =====
|
||||
const syncVisible = ref(false);
|
||||
const syncLoading = ref(false);
|
||||
const importLoading = ref(false);
|
||||
const syncList = ref<any[]>([]);
|
||||
const syncSelectedKeys = ref<string[]>([]);
|
||||
|
||||
const syncColumns = [
|
||||
{ title: '模板名称', dataIndex: 'name', width: 200 },
|
||||
{ title: 'processCode', dataIndex: 'processCode', width: 300 },
|
||||
{ title: '描述', dataIndex: 'description', ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'imported', width: 90 },
|
||||
];
|
||||
|
||||
async function handleSyncFromDingtalk() {
|
||||
syncVisible.value = true;
|
||||
syncLoading.value = true;
|
||||
syncList.value = [];
|
||||
syncSelectedKeys.value = [];
|
||||
try {
|
||||
const data = await syncFromDingtalk();
|
||||
syncList.value = data || [];
|
||||
syncSelectedKeys.value = (syncList.value as any[]).filter((r) => !r.imported).map((r) => r.processCode);
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '从钉钉同步失败');
|
||||
syncVisible.value = false;
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSyncSelectChange(keys: string[]) {
|
||||
syncSelectedKeys.value = keys;
|
||||
}
|
||||
|
||||
async function handleBatchImport() {
|
||||
if (syncSelectedKeys.value.length === 0) {
|
||||
createMessage.warning('请勾选要导入的模板');
|
||||
return;
|
||||
}
|
||||
const selected = syncList.value.filter((r) => syncSelectedKeys.value.includes(r.processCode));
|
||||
importLoading.value = true;
|
||||
try {
|
||||
const msg = await batchImport(selected);
|
||||
createMessage.success(typeof msg === 'string' ? msg : '导入成功');
|
||||
syncVisible.value = false;
|
||||
reload();
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '批量导入失败');
|
||||
} finally {
|
||||
importLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-picker-range) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,776 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="`发起审批 · ${tplData?.tplName || ''}`"
|
||||
width="940px"
|
||||
:confirm-loading="submitting"
|
||||
ok-text="发起审批"
|
||||
cancel-text="取消"
|
||||
destroy-on-close
|
||||
:body-style="{ padding: 0 }"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div class="dal-body">
|
||||
<!-- ══════════ 左侧:审批流时间轴 ══════════ -->
|
||||
<div class="dal-timeline-panel">
|
||||
<div class="dal-panel-title">审批流程</div>
|
||||
|
||||
<div v-if="!selectedFlowId" class="dal-timeline-empty">
|
||||
<div class="dal-timeline-empty-icon">🔗</div>
|
||||
<div>请先在「审批流配置」<br>页签中选择审批流</div>
|
||||
</div>
|
||||
|
||||
<a-spin v-else-if="previewLoading" style="display:flex;justify-content:center;padding:32px 0" />
|
||||
|
||||
<div v-else class="dal-timeline">
|
||||
<!-- 发起人节点(固定头) -->
|
||||
<div class="dal-ts-step">
|
||||
<div class="dal-ts-left">
|
||||
<div class="dal-ts-dot dal-ts-dot--start"></div>
|
||||
<div class="dal-ts-line" v-if="approverPreview.length > 0"></div>
|
||||
</div>
|
||||
<div class="dal-ts-content">
|
||||
<div class="dal-ts-name">发起人</div>
|
||||
<div class="dal-ts-sub">所有人可发起</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动态节点 -->
|
||||
<div
|
||||
v-for="(node, ni) in approverPreview"
|
||||
:key="node.nodeId || ni"
|
||||
class="dal-ts-step"
|
||||
>
|
||||
<div class="dal-ts-left">
|
||||
<div
|
||||
class="dal-ts-dot"
|
||||
:class="[
|
||||
node.nodeType === 'cc' ? 'dal-ts-dot--cc' : 'dal-ts-dot--approver',
|
||||
!node.allResolved ? 'dal-ts-dot--warn' : ''
|
||||
]"
|
||||
></div>
|
||||
<div class="dal-ts-line" v-if="ni < approverPreview.length - 1"></div>
|
||||
</div>
|
||||
<div class="dal-ts-content">
|
||||
<div class="dal-ts-tags">
|
||||
<span class="dal-ts-badge" :class="node.nodeType === 'cc' ? 'dal-ts-badge--cc' : 'dal-ts-badge--approver'">
|
||||
{{ node.nodeType === 'cc' ? '抄送' : '审批' }}
|
||||
</span>
|
||||
<span v-if="node.nodeType !== 'cc'" class="dal-ts-mode">{{ modeLabel(node.multiMode) }}</span>
|
||||
</div>
|
||||
<div class="dal-ts-name">{{ node.nodeName }}</div>
|
||||
<div class="dal-ts-users">
|
||||
<template v-for="(u, ui) in node.users" :key="u.username">
|
||||
<span :class="u.resolved ? 'dal-ts-user--ok' : 'dal-ts-user--err'">
|
||||
{{ u.realname }}
|
||||
</span>
|
||||
<span v-if="ui < node.users.length - 1" style="color:#ccc;margin:0 2px">·</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="!node.allResolved" class="dal-ts-unresolved">
|
||||
⚠ 有未解析成员,请补充手机号
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结束节点 -->
|
||||
<div class="dal-ts-step" v-if="approverPreview.length > 0">
|
||||
<div class="dal-ts-left">
|
||||
<div class="dal-ts-dot dal-ts-dot--end"></div>
|
||||
</div>
|
||||
<div class="dal-ts-content">
|
||||
<div class="dal-ts-name" style="color:#888">结束</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════ 分隔线 ══════════ -->
|
||||
<div class="dal-panel-divider"></div>
|
||||
|
||||
<!-- ══════════ 右侧:主内容 ══════════ -->
|
||||
<div class="dal-content-panel">
|
||||
<a-tabs v-model:activeKey="activeTab" size="small" class="dal-tabs">
|
||||
<!-- ── 表单填写 ── -->
|
||||
<a-tab-pane key="form" tab="表单填写">
|
||||
<div class="dal-form-scroll">
|
||||
<a-spin :spinning="loading" tip="加载表单字段中...">
|
||||
<a-alert v-if="loadError" type="error" :message="loadError" show-icon style="margin-bottom:12px" />
|
||||
<template v-else-if="!loading">
|
||||
<div v-if="dingFields.length === 0" class="dal-form-empty">
|
||||
该模板暂无表单字段,可直接发起(仅通知审批人)
|
||||
</div>
|
||||
<template v-for="field in dingFields" :key="field.label">
|
||||
<div v-if="field.componentName === 'TextNote'" class="dal-form-note">{{ field.label }}</div>
|
||||
|
||||
<template v-else-if="field.componentName === 'TableField'">
|
||||
<div class="dal-form-item">
|
||||
<div class="dal-field-label" :class="{ 'dal-field-label--required': field.required }">{{ field.label }}</div>
|
||||
<div class="dal-table-wrap">
|
||||
<table class="dal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;text-align:center">#</th>
|
||||
<th v-for="child in field.children||[]" :key="child.label">{{ child.label }}</th>
|
||||
<th style="width:88px;text-align:center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIdx) in getTableRows(field.label)" :key="rowIdx">
|
||||
<td style="text-align:center;color:#aaa">{{ rowIdx + 1 }}</td>
|
||||
<td v-for="child in field.children||[]" :key="child.label">
|
||||
<a-input v-model:value="row[child.label]" placeholder="请输入" size="small" :bordered="false" />
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<a-space :size="4">
|
||||
<a style="color:#ff4d4f;font-size:12px" @click="deleteTableRow(field.label, rowIdx)">删除</a>
|
||||
<a style="font-size:12px" @click="copyTableRow(field.label, rowIdx)">复制</a>
|
||||
</a-space>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="getTableRows(field.label).length === 0">
|
||||
<td :colspan="(field.children?.length||0)+2" style="text-align:center;color:#bbb;padding:10px 0">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="dal-table-add" @click="addTableRow(field.label, field.children||[])">+ 添加</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="dal-form-item">
|
||||
<div class="dal-field-label" :class="{ 'dal-field-label--required': field.required }">{{ field.label }}</div>
|
||||
<a-range-picker v-if="field.componentName==='DDDateRangeField'" v-model:value="formValues[field.label]" style="width:100%" value-format="YYYY-MM-DD HH:mm" show-time :placeholder="['开始时间','结束时间']" />
|
||||
<a-date-picker v-else-if="field.componentName==='DDDateField'" v-model:value="formValues[field.label]" style="width:100%" value-format="YYYY-MM-DD" placeholder="请选择日期" />
|
||||
<a-input-number v-else-if="['NumberField','MoneyField'].includes(field.componentName)" v-model:value="formValues[field.label]" style="width:100%" placeholder="请输入" />
|
||||
<a-textarea v-else-if="field.componentName==='TextareaField'" v-model:value="formValues[field.label]" :rows="3" placeholder="请输入" />
|
||||
<a-select v-else-if="field.componentName==='DDSelectField'" v-model:value="formValues[field.label]" style="width:100%" placeholder="请选择" allow-clear>
|
||||
<a-select-option v-for="opt in field.options||[]" :key="opt.key" :value="opt.value">{{ opt.value }}</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-else-if="field.componentName==='DDMultiSelectField'" v-model:value="formValues[field.label]" style="width:100%" mode="multiple" placeholder="请选择" allow-clear>
|
||||
<a-select-option v-for="opt in field.options||[]" :key="opt.key" :value="opt.value">{{ opt.value }}</a-select-option>
|
||||
</a-select>
|
||||
<template v-else-if="field.componentName==='InnerContactField'">
|
||||
<a-input v-model:value="formValues[field.label]" placeholder="填写钉钉 userId,多人用英文逗号分隔" allow-clear />
|
||||
<div class="dal-field-hint">多人逗号分隔,系统自动转 JSON 数组格式</div>
|
||||
</template>
|
||||
<template v-else-if="field.componentName==='RelateField'">
|
||||
<a-input v-model:value="formValues[field.label]" placeholder="填写关联审批实例 ID,多个用英文逗号分隔" allow-clear />
|
||||
<div class="dal-field-hint">多个逗号分隔,系统自动转 JSON 数组格式</div>
|
||||
</template>
|
||||
<template v-else-if="field.componentName==='DDPhotoField'">
|
||||
<a-input v-model:value="formValues[field.label]" placeholder="填写图片 URL,多张用英文逗号分隔" allow-clear />
|
||||
<div class="dal-field-hint">多张逗号分隔,系统自动转 JSON 数组格式</div>
|
||||
</template>
|
||||
<template v-else-if="field.componentName==='DepartmentField'">
|
||||
<a-input v-model:value="formValues[field.label]" placeholder="填写钉钉部门 ID,多个用英文逗号分隔" allow-clear />
|
||||
<div class="dal-field-hint">部门 ID 逗号分隔直接传入</div>
|
||||
</template>
|
||||
<template v-else-if="field.componentName==='DDAttachment'">
|
||||
<a-input v-model:value="formValues[field.label]" placeholder='[{"spaceId":"...","fileId":"...","fileName":"...","fileSize":"...","fileType":"..."}]' allow-clear />
|
||||
<div class="dal-field-hint">需先上传到钉钉云盘获取 fileId,直接填写 JSON 数组字符串</div>
|
||||
</template>
|
||||
<a-input v-else v-model:value="formValues[field.label]" placeholder="请输入" allow-clear />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- ── 审批流配置 ── -->
|
||||
<a-tab-pane key="flow">
|
||||
<template #tab>
|
||||
审批流配置
|
||||
<a-badge v-if="hasUnresolved" color="red" style="margin-left:4px" />
|
||||
</template>
|
||||
<div class="dal-form-scroll">
|
||||
<div class="flow-tab-header">
|
||||
<span class="flow-tab-hint">选择审批流,发起时按流程节点指定钉钉审批人</span>
|
||||
<a-button size="small" type="primary" ghost @click="handleNewFlow">+ 新建审批流</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 下拉选择审批流 -->
|
||||
<div class="flow-select-row">
|
||||
<a-select
|
||||
v-model:value="selectedFlowId"
|
||||
style="flex:1;min-width:0"
|
||||
placeholder="请选择审批流"
|
||||
:loading="flowLoading"
|
||||
:options="flowSelectOptions"
|
||||
show-search
|
||||
:filter-option="filterFlowOption"
|
||||
allow-clear
|
||||
@change="handleFlowSelected"
|
||||
>
|
||||
<template #option="{ label, status, remark }">
|
||||
<div class="flow-opt-item">
|
||||
<span class="flow-opt-name">{{ label }}</span>
|
||||
<span style="display:flex;align-items:center;gap:6px;flex-shrink:0">
|
||||
<span v-if="remark" class="flow-opt-remark">{{ remark }}</span>
|
||||
<a-tag
|
||||
:color="status==='1'?'green':status==='2'?'default':'orange'"
|
||||
style="margin:0;font-size:11px;line-height:16px;padding:0 5px"
|
||||
>{{ status==='1'?'已发布':status==='2'?'已停用':'草稿' }}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
<a-button
|
||||
v-if="selectedFlowId"
|
||||
size="small"
|
||||
type="link"
|
||||
style="flex-shrink:0;padding-left:8px"
|
||||
@click="handleDesignSelectedFlow"
|
||||
>设计</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 审批人解析预览 -->
|
||||
<template v-if="selectedFlowId">
|
||||
<a-divider style="margin:14px 0 10px" />
|
||||
<div class="preview-title">
|
||||
审批节点 · 人员解析
|
||||
<a-spin :spinning="previewLoading" size="small" style="margin-left:8px" />
|
||||
</div>
|
||||
<div v-if="!previewLoading && approverPreview.length === 0" style="color:#bbb;font-size:12px;margin-top:6px">
|
||||
该审批流暂无审批人节点
|
||||
</div>
|
||||
<div v-for="(node, ni) in approverPreview" :key="node.nodeId||ni" class="preview-node" :class="{'preview-node--cc': node.nodeType==='cc'}">
|
||||
<div class="preview-node-hd">
|
||||
<a-tag :color="node.nodeType==='cc'?'blue':'orange'" style="margin:0 6px 0 0;font-size:11px">
|
||||
{{ node.nodeType === 'cc' ? '抄送' : '审批' }}
|
||||
</a-tag>
|
||||
<span class="preview-node-name">{{ node.nodeName }}</span>
|
||||
<span class="preview-node-mode">
|
||||
{{ node.nodeType === 'cc' ? '位置自动判断' : modeLabel(node.multiMode) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-for="u in node.users" :key="u.username" class="preview-user">
|
||||
<a-tag v-if="u.resolved" color="success" style="margin:0">{{ u.realname }}({{ u.username }})✓</a-tag>
|
||||
<a-tag v-else-if="u.unsupported" color="default" style="margin:0">{{ u.realname }}(不支持自动解析)</a-tag>
|
||||
<a-tag v-else color="error" style="margin:0">{{ u.realname }}({{ u.username }})未找到钉钉账号</a-tag>
|
||||
</div>
|
||||
<div v-if="!node.allResolved" class="preview-supplement">
|
||||
<a-input
|
||||
v-model:value="supplementPhones[node.nodeId||String(ni)]"
|
||||
:placeholder="node.nodeType==='cc' ? '补充抄送人手机号,多个用逗号分隔' : '补充审批人手机号,多个用逗号分隔'"
|
||||
allow-clear
|
||||
size="small"
|
||||
/>
|
||||
<div class="dal-field-hint" style="margin-top:3px">
|
||||
手机号需在企业钉钉注册,与自动解析的成员合并
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-alert v-if="hasUnresolved" type="warning" show-icon style="margin-top:10px;font-size:12px"
|
||||
message="部分节点有未解析成员,请补充手机号后再发起审批" />
|
||||
</template>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ApprovalFlowModal @register="registerFlowModal" @success="handleFlowCreated" />
|
||||
<FlowDesign @register="registerFlowDesign" @success="loadFlowList" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { getTemplateDetail, launchApproval, getApprovalFlowList, previewFlowApprovers, bindApprovalFlow } from '../MesXslDingProcessTpl.api';
|
||||
import ApprovalFlowModal from '/@/views/approval/flow/ApprovalFlowModal.vue';
|
||||
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 loadError = ref('');
|
||||
const activeTab = ref('form');
|
||||
|
||||
const tplData = ref<any>(null);
|
||||
const dingFields = ref<any[]>([]);
|
||||
const formValues = reactive<Record<string, any>>({});
|
||||
const tableValues = reactive<Record<string, Record<string, string>[]>>({});
|
||||
|
||||
const flowLoading = ref(false);
|
||||
const flowList = ref<any[]>([]);
|
||||
const selectedFlowId = ref('');
|
||||
|
||||
const previewLoading = ref(false);
|
||||
const approverPreview = ref<any[]>([]);
|
||||
const supplementPhones = reactive<Record<string, string>>({});
|
||||
|
||||
const hasUnresolved = computed(() => approverPreview.value.some((n) => !n.allResolved));
|
||||
|
||||
const flowSelectOptions = computed(() =>
|
||||
flowList.value.map((f) => ({
|
||||
value: f.id,
|
||||
label: f.flowName,
|
||||
status: f.status,
|
||||
remark: f.remark || '',
|
||||
})),
|
||||
);
|
||||
|
||||
function filterFlowOption(input: string, option: any) {
|
||||
return (option?.label ?? '').toLowerCase().includes(input.toLowerCase());
|
||||
}
|
||||
|
||||
const [registerFlowModal, { openModal: openFlowModal }] = useModal();
|
||||
const [registerFlowDesign, { openModal: openFlowDesign }] = useModal();
|
||||
|
||||
function modeLabel(mode: string) {
|
||||
if (mode === 'none') return '单人';
|
||||
if (mode === 'or') return '或签';
|
||||
if (mode === 'sequence') return '依次';
|
||||
return '会签';
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.keys(formValues).forEach((k) => delete formValues[k]);
|
||||
Object.keys(tableValues).forEach((k) => delete tableValues[k]);
|
||||
Object.keys(supplementPhones).forEach((k) => delete supplementPhones[k]);
|
||||
loadError.value = '';
|
||||
activeTab.value = 'form';
|
||||
selectedFlowId.value = '';
|
||||
approverPreview.value = [];
|
||||
}
|
||||
|
||||
async function open(record: any) {
|
||||
resetForm();
|
||||
tplData.value = record;
|
||||
dingFields.value = [];
|
||||
visible.value = true;
|
||||
loading.value = true;
|
||||
if (record.flowId) selectedFlowId.value = record.flowId;
|
||||
try {
|
||||
const detail = await getTemplateDetail(record.id);
|
||||
tplData.value = detail;
|
||||
dingFields.value = detail.dingFields || [];
|
||||
if (detail.schemaError) loadError.value = detail.schemaError;
|
||||
for (const f of dingFields.value) {
|
||||
if (f.componentName === 'TableField') tableValues[f.label] = [buildEmptyRow(f.children || [])];
|
||||
}
|
||||
} catch (e: any) {
|
||||
loadError.value = e?.message || '加载模板字段失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
await loadFlowList();
|
||||
if (selectedFlowId.value) loadPreview(selectedFlowId.value);
|
||||
}
|
||||
|
||||
function handleClose() { visible.value = false; }
|
||||
|
||||
async function loadFlowList() {
|
||||
flowLoading.value = true;
|
||||
try {
|
||||
const res = await getApprovalFlowList({ pageSize: 200 });
|
||||
flowList.value = res?.records || res || [];
|
||||
} catch { flowList.value = []; }
|
||||
finally { flowLoading.value = false; }
|
||||
}
|
||||
|
||||
function handleFlowSelected() {
|
||||
// 清空补充手机号和预览(无论选中还是清空)
|
||||
Object.keys(supplementPhones).forEach((k) => delete supplementPhones[k]);
|
||||
approverPreview.value = [];
|
||||
|
||||
// 立刻持久化绑定关系(选中或清空均保存,flowId 为空表示解绑)
|
||||
if (tplData.value?.id) {
|
||||
bindApprovalFlow({ id: tplData.value.id, flowId: selectedFlowId.value || '' }).catch(() => {
|
||||
// 静默失败,不影响主流程;发起时后端会再次兜底保存
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedFlowId.value) 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 handleNewFlow() { openFlowModal(true, { isUpdate: false }); }
|
||||
|
||||
async function handleFlowCreated() {
|
||||
await loadFlowList();
|
||||
if (flowList.value.length > 0) {
|
||||
const last = flowList.value[flowList.value.length - 1];
|
||||
selectedFlowId.value = last.id;
|
||||
loadPreview(last.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDesignFlow(flow: any) { openFlowDesign(true, { record: flow, readonly: false }); }
|
||||
|
||||
function handleDesignSelectedFlow() {
|
||||
const flow = flowList.value.find((f) => f.id === selectedFlowId.value);
|
||||
if (flow) handleDesignFlow(flow);
|
||||
}
|
||||
|
||||
// ─── 表格操作 ───
|
||||
function buildEmptyRow(children: any[]): Record<string, string> {
|
||||
const row: Record<string, string> = {};
|
||||
for (const c of children) row[c.label] = '';
|
||||
return row;
|
||||
}
|
||||
function getTableRows(label: string) {
|
||||
if (!tableValues[label]) tableValues[label] = [];
|
||||
return tableValues[label];
|
||||
}
|
||||
function addTableRow(label: string, children: any[]) { getTableRows(label).push(buildEmptyRow(children)); }
|
||||
function deleteTableRow(label: string, idx: number) { getTableRows(label).splice(idx, 1); }
|
||||
function copyTableRow(label: string, idx: number) {
|
||||
const rows = getTableRows(label);
|
||||
rows.splice(idx + 1, 0, { ...rows[idx] });
|
||||
}
|
||||
|
||||
// ─── 提交 ───
|
||||
async function handleSubmit() {
|
||||
if (!selectedFlowId.value) {
|
||||
activeTab.value = 'flow';
|
||||
createMessage.warning('请在「审批流配置」页签中选择一个审批流');
|
||||
return;
|
||||
}
|
||||
const unresolvedNodes = approverPreview.value.filter((n) => !n.allResolved);
|
||||
for (const node of unresolvedNodes) {
|
||||
const nodeKey = node.nodeId || String(approverPreview.value.indexOf(node));
|
||||
if (!supplementPhones[nodeKey]?.trim()) {
|
||||
activeTab.value = 'flow';
|
||||
createMessage.warning(`${node.nodeType === 'cc' ? '抄送节点' : '审批节点'}「${node.nodeName}」有未解析成员,请补充手机号`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const field of dingFields.value) {
|
||||
if (!field.required || field.componentName === 'TextNote') continue;
|
||||
if (field.componentName === 'TableField') {
|
||||
if (getTableRows(field.label).length === 0) { activeTab.value = 'form'; createMessage.warning(`「${field.label}」至少需要填写一行`); return; }
|
||||
continue;
|
||||
}
|
||||
const val = formValues[field.label];
|
||||
if (val === undefined || val === null || val === '' || (Array.isArray(val) && val.length === 0)) {
|
||||
activeTab.value = 'form'; createMessage.warning(`「${field.label}」为必填项`); return;
|
||||
}
|
||||
}
|
||||
|
||||
const fvList: { name: string; value: string }[] = [];
|
||||
for (const field of dingFields.value) {
|
||||
if (field.componentName === 'TextNote') continue;
|
||||
const label = field.label;
|
||||
if (field.componentName === 'TableField') {
|
||||
const validRows = getTableRows(label).filter((r) => Object.values(r).some((v) => v !== ''));
|
||||
if (validRows.length === 0) continue;
|
||||
fvList.push({ name: label, value: JSON.stringify(validRows.map((row) => Object.entries(row).map(([k, v]) => ({ name: k, value: String(v ?? '') })))) });
|
||||
continue;
|
||||
}
|
||||
let val = formValues[label];
|
||||
if (field.componentName === 'DDDateRangeField' && Array.isArray(val)) {
|
||||
val = val.join('~');
|
||||
} else if (field.componentName === 'DDMultiSelectField' && Array.isArray(val)) {
|
||||
val = val.length > 0 ? JSON.stringify(val) : null;
|
||||
} else if (['InnerContactField', 'RelateField', 'DDPhotoField'].includes(field.componentName)) {
|
||||
const raw = val !== undefined && val !== null ? String(val).trim() : '';
|
||||
val = raw ? JSON.stringify(raw.split(',').map((s) => s.trim()).filter(Boolean)) : null;
|
||||
} else {
|
||||
val = val !== undefined && val !== null ? String(val) : null;
|
||||
}
|
||||
if (val === null || val === '') { if (!field.required) continue; val = ''; }
|
||||
fvList.push({ name: label, value: val as string });
|
||||
}
|
||||
|
||||
const approverOverrides = Object.entries(supplementPhones)
|
||||
.filter(([, phones]) => phones?.trim())
|
||||
.map(([nodeId, phones]) => ({ nodeId, phones: phones.trim() }));
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const result = await launchApproval({ id: tplData.value?.id, formValues: fvList, flowId: selectedFlowId.value, approverOverrides });
|
||||
createMessage.success(typeof result === 'string' ? result : '审批发起成功!审批人将在钉钉「待我审批」中收到任务');
|
||||
visible.value = false;
|
||||
emit('success', result);
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '发起失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// ─── 整体布局 ───
|
||||
.dal-body {
|
||||
display: flex;
|
||||
min-height: 480px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
// ─── 左侧时间轴面板 ───
|
||||
.dal-timeline-panel {
|
||||
width: 210px;
|
||||
flex-shrink: 0;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
padding: 16px 14px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dal-panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
letter-spacing: .5px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dal-timeline-empty {
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
padding-top: 32px;
|
||||
line-height: 1.8;
|
||||
.dal-timeline-empty-icon { font-size: 24px; margin-bottom: 8px; }
|
||||
}
|
||||
|
||||
// ─── 时间轴 ───
|
||||
.dal-timeline { }
|
||||
|
||||
.dal-ts-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dal-ts-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.dal-ts-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 2px solid currentColor;
|
||||
background: #fff;
|
||||
}
|
||||
.dal-ts-dot--start { color: #1677ff; background: #1677ff; border-color: #1677ff; }
|
||||
.dal-ts-dot--end { color: #d9d9d9; background: #d9d9d9; border-color: #d9d9d9; width: 8px; height: 8px; }
|
||||
.dal-ts-dot--approver { color: #fa8c16; }
|
||||
.dal-ts-dot--cc { color: #1677ff; }
|
||||
.dal-ts-dot--warn { color: #ff4d4f !important; }
|
||||
|
||||
.dal-ts-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 22px;
|
||||
background: linear-gradient(to bottom, #e0e0e0 50%, transparent 100%);
|
||||
margin: 3px 0 0;
|
||||
}
|
||||
|
||||
.dal-ts-content {
|
||||
flex: 1;
|
||||
padding-bottom: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dal-ts-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.dal-ts-badge {
|
||||
font-size: 10px;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
&--approver { background: #fff7e6; color: #d46b08; border: 1px solid #ffd591; }
|
||||
&--cc { background: #e6f4ff; color: #0958d9; border: 1px solid #91caff; }
|
||||
}
|
||||
|
||||
.dal-ts-mode {
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
background: #f5f5f5;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.dal-ts-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dal-ts-users {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dal-ts-user--ok { color: #52c41a; }
|
||||
.dal-ts-user--err { color: #ff4d4f; }
|
||||
|
||||
.dal-ts-unresolved {
|
||||
font-size: 10px;
|
||||
color: #ff7a00;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// ─── 分隔线 ───
|
||||
.dal-panel-divider {
|
||||
width: 1px;
|
||||
background: #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// ─── 右侧内容面板 ───
|
||||
.dal-content-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dal-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-tabs-nav) { padding: 0 16px; margin-bottom: 0; }
|
||||
:deep(.ant-tabs-content-holder) { flex: 1; overflow: hidden; }
|
||||
:deep(.ant-tabs-content) { height: 100%; }
|
||||
:deep(.ant-tabs-tabpane) { height: 100%; }
|
||||
}
|
||||
|
||||
.dal-form-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
// ─── 表单元素 ───
|
||||
.dal-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dal-form-empty {
|
||||
color: #bbb;
|
||||
text-align: center;
|
||||
padding: 32px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dal-field-label {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
&--required::before { content: '* '; color: #ff4d4f; }
|
||||
}
|
||||
|
||||
.dal-field-hint { font-size: 11px; color: #aaa; margin-top: 3px; }
|
||||
|
||||
.dal-form-note {
|
||||
background: #f8f8f8;
|
||||
border-left: 3px solid #ddd;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
// ─── 表格 ───
|
||||
.dal-table-wrap { border: 1px solid #ebebeb; border-radius: 6px; overflow: hidden; }
|
||||
|
||||
.dal-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
th { background: #fafafa; padding: 7px 10px; border-bottom: 1px solid #ebebeb; font-weight: 500; color: #666; font-size: 12px; }
|
||||
td { padding: 3px 6px; border-bottom: 1px solid #f5f5f5; vertical-align: middle; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
:deep(.ant-input) { padding: 2px 6px; }
|
||||
}
|
||||
|
||||
.dal-table-add {
|
||||
display: flex; align-items: center; justify-content: center; padding: 7px 0;
|
||||
color: #1677ff; font-size: 13px; cursor: pointer;
|
||||
border-top: 1px dashed #ddd; background: #fafcff;
|
||||
&:hover { background: #e6f0ff; }
|
||||
}
|
||||
|
||||
// ─── 审批流配置页签 ───
|
||||
.flow-tab-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.flow-tab-hint { font-size: 12px; color: #999; }
|
||||
|
||||
.flow-select-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
// 下拉选项内容
|
||||
.flow-opt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.flow-opt-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
.flow-opt-remark {
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preview-title { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 8px; display: flex; align-items: center; }
|
||||
.preview-node { border: 1px solid #f0f0f0; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; &--cc { border-color: #bae0ff; background: #f6fbff; } }
|
||||
.preview-node-hd { display: flex; align-items: center; gap: 6px; margin-bottom: 7px; }
|
||||
.preview-node-name { font-size: 13px; font-weight: 500; color: #333; }
|
||||
.preview-node-mode { font-size: 11px; color: #aaa; background: #f5f5f5; padding: 1px 6px; border-radius: 3px; }
|
||||
.preview-user { display: inline-block; margin: 0 5px 5px 0; }
|
||||
.preview-supplement { margin-top: 8px; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" name="MesXslDingProcessTplForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from '../MesXslDingProcessTpl.data';
|
||||
import { saveOrUpdate } from '../MesXslDingProcessTpl.api';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
const isUpdate = ref(true);
|
||||
const isDetail = ref(false);
|
||||
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
|
||||
labelWidth: 120,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
isDetail.value = !!data?.showFooter;
|
||||
if (unref(isUpdate)) {
|
||||
await setFieldsValue({ ...data.record });
|
||||
}
|
||||
setProps({ disabled: !data?.showFooter });
|
||||
});
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdate(values, isUpdate.value);
|
||||
closeModal();
|
||||
emit('success');
|
||||
} catch ({ errorFields }: any) {
|
||||
if (errorFields) {
|
||||
const firstField = errorFields[0];
|
||||
if (firstField) scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
return Promise.reject(errorFields);
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ enum Api {
|
||||
proofread = '/xslmes/mesXslMixerPsCompile/proofread',
|
||||
audit = '/xslmes/mesXslMixerPsCompile/audit',
|
||||
approve = '/xslmes/mesXslMixerPsCompile/approve',
|
||||
ddApprovalTest = '/xslmes/mesXslMixerPsCompile/ddApprovalTest',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
@@ -37,3 +38,5 @@ export const proofread = (params: { ids: string }) => defHttp.post({ url: Api.pr
|
||||
export const audit = (params: { ids: string }) => defHttp.post({ url: Api.audit, params }, { joinParamsToUrl: true });
|
||||
|
||||
export const approve = (params: { ids: string }) => defHttp.post({ url: Api.approve, params }, { joinParamsToUrl: true });
|
||||
|
||||
export const ddApprovalTest = () => defHttp.post({ url: Api.ddApprovalTest }, { successMessageMode: 'none' });
|
||||
|
||||
@@ -65,6 +65,11 @@
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试按钮-->
|
||||
<a-button :loading="ddTestLoading" preIcon="ant-design:dingtalk-outlined" @click="handleDdApprovalTest">
|
||||
钉钉审批测试
|
||||
</a-button>
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试按钮-->
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction
|
||||
@@ -81,11 +86,19 @@
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslMixerPsCompileModal @register="registerModal" @success="handleSuccess" />
|
||||
|
||||
<!--update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】结果弹窗-->
|
||||
<a-modal v-model:open="ddTestVisible" title="钉钉审批测试结果" width="680px" :footer="null">
|
||||
<a-spin :spinning="ddTestLoading">
|
||||
<pre style="max-height: 520px; overflow: auto; font-size: 12px; background: #f6f8fa; padding: 14px; border-radius: 6px; white-space: pre-wrap; word-break: break-all">{{ ddTestResult }}</pre>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
<!--update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】结果弹窗-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslMixerPsCompile" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
@@ -103,6 +116,7 @@
|
||||
proofread,
|
||||
audit,
|
||||
approve,
|
||||
ddApprovalTest,
|
||||
} from './MesXslMixerPsCompile.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
@@ -228,6 +242,26 @@
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试逻辑
|
||||
const ddTestVisible = ref(false);
|
||||
const ddTestLoading = ref(false);
|
||||
const ddTestResult = ref('');
|
||||
|
||||
async function handleDdApprovalTest() {
|
||||
ddTestVisible.value = true;
|
||||
ddTestLoading.value = true;
|
||||
ddTestResult.value = '正在请求,请稍候...';
|
||||
try {
|
||||
const res = await ddApprovalTest();
|
||||
ddTestResult.value = JSON.stringify(res, null, 2);
|
||||
} catch (e: any) {
|
||||
ddTestResult.value = '请求失败:\n' + (e?.message || JSON.stringify(e));
|
||||
} finally {
|
||||
ddTestLoading.value = false;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试逻辑
|
||||
|
||||
function getDropDownAction(record: Recordable) {
|
||||
return [
|
||||
{ label: '详情', onClick: handleDetail.bind(null, record) },
|
||||
|
||||
Reference in New Issue
Block a user