Merge remote-tracking branch 'origin/20260519-3.9.2版本-葛昊天分支'
This commit is contained in:
@@ -466,3 +466,58 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTes
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
|
||||
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】我的租户下新增审批流设计,钉钉式可视化拖拽设计(先选单据再设计流程),本期实现设计器 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_111__mes_xsl_approval_flow.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalFlowMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalFlowService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalFlowServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/components/FlowNode.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/flow.less
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批运行时:全局悬浮按钮(选单据类型->选单据->发起),本期仅发起(生成审批实例+解析首节点处理人),不办理/不回写业务表 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_112__mes_xsl_approval_instance.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalInstanceMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalInstanceService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalInstanceServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
jeecgboot-vue3/src/layouts/default/index.vue
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批悬浮按钮仅在配置了审批流的功能页显示:审批流定义增加route_path(功能页路由),前端按当前路由匹配后才显示按钮 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_113__mes_xsl_approval_flow_route.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】取消手填功能页路由:publishedList按单据表名自动反查sys_permission菜单url填入routePath,设计表单去掉路由字段 ---
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批支持列表多选联动:useListPage自动同步选中行到全局上下文,悬浮按钮发起弹窗自动带入选中单据并批量发起 ---
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/useApprovalSelection.ts
|
||||
jeecgboot-vue3/src/hooks/system/useListPage.ts
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批与IM聊天结合:IM新增系统单聊消息(绕过同部门校验),发起后把审批消息发给当前节点处理人 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】审批IM消息升级为可跳转业务卡片(biz_record):点击可定位到对应单据,无法定位功能页时退回纯文本 ---
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* 审批动作领域事件。
|
||||
* 与 {@link IApprovalBizCallback} 等价的另一种接入方式:
|
||||
* 偏好松耦合的业务可使用 {@code @EventListener} 监听本事件(同步、同事务)。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
public class ApprovalActionEvent extends ApplicationEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final ApprovalCallbackContext context;
|
||||
|
||||
public ApprovalActionEvent(Object source, ApprovalCallbackContext context) {
|
||||
super(source);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ApprovalCallbackContext getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 审批节点「回调接口」HTTP 执行器。
|
||||
*
|
||||
* 审批到对应时机时,读取节点配置中录制好的业务接口(url+method),
|
||||
* 以「当前审批处理人」的登录态(透传当前请求的 X-Access-Token)内部调用该接口,
|
||||
* 自动带上单据ID(覆盖 id 参数),从而真实执行业务页面按钮背后的逻辑。
|
||||
*
|
||||
* 限制:自动流转 / 无人值守节点(无登录态请求上下文)时降级跳过并记录日志。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】节点回调接口内部调用执行
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalActionHttpExecutor {
|
||||
|
||||
@Value("${server.port:8080}")
|
||||
private int serverPort;
|
||||
|
||||
@Value("${server.servlet.context-path:}")
|
||||
private String contextPath;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 执行某节点在指定时机配置的所有回调接口。
|
||||
*
|
||||
* @param node 流程节点 JSON(含 props.callbackActions)
|
||||
* @param phase 时机:onNodeApprove / onApprove / onReject
|
||||
* @param inst 审批实例
|
||||
*/
|
||||
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
|
||||
if (node == null || inst == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject propsObj = node.getJSONObject("props");
|
||||
if (propsObj == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
|
||||
if (callbackActions == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray actions = callbackActions.getJSONArray(phase);
|
||||
if (actions == null || actions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String token = currentToken();
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
JSONObject action = actions.getJSONObject(i);
|
||||
if (action == null) {
|
||||
continue;
|
||||
}
|
||||
String url = action.getString("url");
|
||||
if (oConvertUtils.isEmpty(url)) {
|
||||
continue;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
// 无登录态(自动流转/无人值守) -> 降级跳过
|
||||
log.warn("[审批回调] 无当前处理人登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, inst.getBizDataId());
|
||||
continue;
|
||||
}
|
||||
String method = oConvertUtils.getString(action.getString("method"), "POST").toUpperCase();
|
||||
invoke(method, url, action.getJSONObject("body"), inst.getBizDataId(), token);
|
||||
}
|
||||
}
|
||||
|
||||
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
|
||||
String fullUrl = buildFullUrl(url);
|
||||
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
|
||||
headers.add(HttpHeaders.AUTHORIZATION, token);
|
||||
|
||||
Object bodyToSend = null;
|
||||
if ("GET".equals(method) || "DELETE".equals(method)) {
|
||||
// 查询/删除:单据ID拼到 url
|
||||
String sep = fullUrl.contains("?") ? "&" : "?";
|
||||
fullUrl = fullUrl + sep + "id=" + bizDataId + "&dataId=" + bizDataId;
|
||||
} else {
|
||||
// 写操作:合并录制的 body,并用单据ID覆盖 id
|
||||
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
|
||||
body.put("id", bizDataId);
|
||||
bodyToSend = body;
|
||||
}
|
||||
|
||||
try {
|
||||
HttpEntity<Object> entity = new HttpEntity<>(bodyToSend, headers);
|
||||
ResponseEntity<String> resp = restTemplate.exchange(fullUrl, httpMethod, entity, String.class);
|
||||
String respBody = resp.getBody();
|
||||
if (!resp.getStatusCode().is2xxSuccessful()) {
|
||||
throw new RuntimeException("回调接口返回非2xx:" + resp.getStatusCode());
|
||||
}
|
||||
// Jeecg 统一返回 {success:false,...} 视为业务失败
|
||||
if (oConvertUtils.isNotEmpty(respBody)) {
|
||||
try {
|
||||
JSONObject r = JSONObject.parseObject(respBody);
|
||||
if (r != null && r.containsKey("success") && Boolean.FALSE.equals(r.getBoolean("success"))) {
|
||||
throw new RuntimeException("回调接口业务失败:" + oConvertUtils.getString(r.getString("message"), "未知错误"));
|
||||
}
|
||||
} catch (RuntimeException re) {
|
||||
throw re;
|
||||
} catch (Exception ignore) {
|
||||
// 非JSON响应不强校验
|
||||
}
|
||||
}
|
||||
log.info("[审批回调] 已调用业务接口成功 {} {} bizId={}", method, fullUrl, bizDataId);
|
||||
} catch (RuntimeException e) {
|
||||
log.error("[审批回调] 调用业务接口失败 {} {} bizId={}", method, fullUrl, bizDataId, e);
|
||||
// 抛出以回滚整个审批动作,保证审批与业务一致
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建内部调用绝对地址:http://127.0.0.1:port + context-path + url */
|
||||
private String buildFullUrl(String url) {
|
||||
String ctx = oConvertUtils.getString(contextPath, "");
|
||||
if (oConvertUtils.isNotEmpty(ctx) && !ctx.startsWith("/")) {
|
||||
ctx = "/" + ctx;
|
||||
}
|
||||
String path = url.startsWith("/") ? url : "/" + url;
|
||||
// 录制到的路径已含 context-path 时不重复拼接
|
||||
if (oConvertUtils.isNotEmpty(ctx) && (path.equals(ctx) || path.startsWith(ctx + "/"))) {
|
||||
ctx = "";
|
||||
}
|
||||
return "http://127.0.0.1:" + serverPort + ctx + path;
|
||||
}
|
||||
|
||||
/** 取当前请求的登录 token(处理人身份) */
|
||||
private String currentToken() {
|
||||
try {
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = attrs.getRequest();
|
||||
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
}
|
||||
return token;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批回调上下文。
|
||||
* 由审批引擎在「节点通过 / 最终通过 / 驳回」时构建并传给业务回调,
|
||||
* 业务模块据此调用自身已有的审核/回写接口,实现审批与业务功能的统一联动。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ApprovalCallbackContext implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 回调动作类型 */
|
||||
public enum Action {
|
||||
/** 单个审批节点通过(中间态,每个节点都会触发一次) */
|
||||
NODE_APPROVED,
|
||||
/** 整个流程最终通过 */
|
||||
APPROVED,
|
||||
/** 被驳回(任一节点驳回即终止) */
|
||||
REJECTED
|
||||
}
|
||||
|
||||
/** 回调动作 */
|
||||
private Action action;
|
||||
|
||||
/** 审批实例ID */
|
||||
private String instanceId;
|
||||
|
||||
/** 审批流定义ID */
|
||||
private String flowId;
|
||||
|
||||
/** 审批流名称 */
|
||||
private String flowName;
|
||||
|
||||
/** 业务单据表名 */
|
||||
private String bizTable;
|
||||
|
||||
/** 业务单据中文名 */
|
||||
private String bizTableName;
|
||||
|
||||
/** 业务单据记录ID(业务表主键) */
|
||||
private String bizDataId;
|
||||
|
||||
/** 业务单据展示标题 */
|
||||
private String bizTitle;
|
||||
|
||||
/** 当前/刚处理的节点ID */
|
||||
private String nodeId;
|
||||
|
||||
/** 当前/刚处理的节点名称 */
|
||||
private String nodeName;
|
||||
|
||||
/** 操作人 username(系统自动处理时为 null/system) */
|
||||
private String operatorUsername;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
/** 审批意见 / 驳回理由 */
|
||||
private String comment;
|
||||
|
||||
/** 发起人 username */
|
||||
private String applyUser;
|
||||
|
||||
/** 是否为流程最终结束(APPROVED/REJECTED 时为 true) */
|
||||
private boolean finalResult;
|
||||
|
||||
/** 完整审批实例(供业务读取租户、发起信息等) */
|
||||
private transient MesXslApprovalInstance instance;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批业务回调分发器。
|
||||
* 统一在审批引擎流转关键点调用:
|
||||
* <ol>
|
||||
* <li>按业务表名路由到对应的 {@link IApprovalBizCallback} 实现(强类型,业务直接调用自身接口);</li>
|
||||
* <li>同时发布 {@link ApprovalActionEvent} 供 {@code @EventListener} 松耦合监听。</li>
|
||||
* </ol>
|
||||
* 回调与审批状态变更同事务执行,回调异常将向上抛出以回滚整个审批动作。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalCallbackDispatcher {
|
||||
|
||||
/** 监听所有业务表的通配符 */
|
||||
private static final String ANY_TABLE = "*";
|
||||
|
||||
private final ObjectProvider<List<IApprovalBizCallback>> callbacksProvider;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public ApprovalCallbackDispatcher(ObjectProvider<List<IApprovalBizCallback>> callbacksProvider,
|
||||
ApplicationEventPublisher eventPublisher) {
|
||||
this.callbacksProvider = callbacksProvider;
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
/** 节点通过(中间态) */
|
||||
public void fireNodeApproved(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.NODE_APPROVED);
|
||||
ctx.setFinalResult(false);
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
/** 流程最终通过 */
|
||||
public void fireApproved(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.APPROVED);
|
||||
ctx.setFinalResult(true);
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
/** 驳回 */
|
||||
public void fireRejected(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.REJECTED);
|
||||
ctx.setFinalResult(true);
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
private void dispatch(ApprovalCallbackContext ctx) {
|
||||
if (ctx == null || oConvertUtils.isEmpty(ctx.getBizTable())) {
|
||||
return;
|
||||
}
|
||||
// 1) 强类型回调:按表路由 + 通配
|
||||
for (IApprovalBizCallback cb : matchedCallbacks(ctx.getBizTable())) {
|
||||
invoke(cb, ctx);
|
||||
}
|
||||
// 2) 领域事件:松耦合监听(同步、同事务)
|
||||
try {
|
||||
eventPublisher.publishEvent(new ApprovalActionEvent(this, ctx));
|
||||
} catch (RuntimeException e) {
|
||||
log.error("审批领域事件处理失败 table={}, bizId={}, action={}", ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private List<IApprovalBizCallback> matchedCallbacks(String bizTable) {
|
||||
List<IApprovalBizCallback> all = callbacksProvider.getIfAvailable();
|
||||
List<IApprovalBizCallback> matched = new ArrayList<>();
|
||||
if (all == null) {
|
||||
return matched;
|
||||
}
|
||||
for (IApprovalBizCallback cb : all) {
|
||||
String support = cb.supportTable();
|
||||
if (ANY_TABLE.equals(support) || (support != null && support.equalsIgnoreCase(bizTable))) {
|
||||
matched.add(cb);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private void invoke(IApprovalBizCallback cb, ApprovalCallbackContext ctx) {
|
||||
try {
|
||||
switch (ctx.getAction()) {
|
||||
case NODE_APPROVED:
|
||||
cb.onNodeApproved(ctx);
|
||||
break;
|
||||
case APPROVED:
|
||||
cb.onApproved(ctx);
|
||||
break;
|
||||
case REJECTED:
|
||||
cb.onRejected(ctx);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.error("审批业务回调执行失败 callback={}, table={}, bizId={}, action={}",
|
||||
cb.getClass().getSimpleName(), ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
// 抛出以回滚整个审批动作,保证审批与业务数据一致
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
/**
|
||||
* 业务审批回调扩展点(SPI)。
|
||||
*
|
||||
* 各业务模块按需实现本接口并声明 {@link #supportTable()}(绑定的业务表名),
|
||||
* 审批引擎会在审批流转的关键节点自动回调对应实现,业务模块在回调里
|
||||
* 调用自己「已有的」审核/回写接口(如更新审核状态、扣减库存、生成下游单据等),
|
||||
* 从而把审批流程与业务单据的功能统一串联起来。
|
||||
*
|
||||
* <p>事务说明:回调与审批状态变更处于同一事务内,
|
||||
* 若回调抛出异常,整个审批动作回滚(审批失败),保证审批与业务数据一致。</p>
|
||||
*
|
||||
* <p>使用方式:实现类标注为 Spring Bean(@Component / @Service)即可被自动收集。</p>
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Component
|
||||
* public class RubberStdApprovalCallback implements IApprovalBizCallback {
|
||||
* @Override public String supportTable() { return "mes_xsl_rubber_quick_test_std"; }
|
||||
* @Override public void onApproved(ApprovalCallbackContext ctx) {
|
||||
* // 调用业务自身已有接口完成回写
|
||||
* stdService.lambdaUpdate()
|
||||
* .eq(MesXslRubberQuickTestStd::getId, ctx.getBizDataId())
|
||||
* .set(MesXslRubberQuickTestStd::getAuditStatus, "1").update();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
public interface IApprovalBizCallback {
|
||||
|
||||
/**
|
||||
* 绑定的业务表名(与审批流定义 bizTable 一致)。
|
||||
* 返回 "*" 表示监听所有业务表。
|
||||
*/
|
||||
String supportTable();
|
||||
|
||||
/**
|
||||
* 单个审批节点通过(中间态)。每经过一个审批节点通过都会触发一次。
|
||||
* 适合更新中间状态(如「审核中」「已校对」等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onNodeApproved(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 整个审批流程最终通过。适合执行终态业务(如置为「已批准」、生效、扣库存等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onApproved(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批被驳回(流程终止)。适合回退业务状态(如置回「草稿」、释放占用等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onRejected(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback.impl;
|
||||
|
||||
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.callback.IApprovalBizCallback;
|
||||
import org.jeecg.modules.xslmes.common.XslMesBizConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 胶料快检实验标准 审批回调示例。
|
||||
* 演示如何把审批流转结果联动到业务单据已有功能:
|
||||
* 审批最终通过 -> 审核状态置「已批准」;驳回 -> 回退「草稿」。
|
||||
*
|
||||
* 业务在回调里直接调用自身的 service(此处用 lambdaUpdate 更新审核状态字段),
|
||||
* 与原有「批准/反审核」逻辑保持统一。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调-示例
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RubberQuickTestStdApprovalCallback implements IApprovalBizCallback {
|
||||
|
||||
private final IMesXslRubberQuickTestStdService stdService;
|
||||
|
||||
public RubberQuickTestStdApprovalCallback(IMesXslRubberQuickTestStdService stdService) {
|
||||
this.stdService = stdService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportTable() {
|
||||
return "mes_xsl_rubber_quick_test_std";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext ctx) {
|
||||
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_APPROVED);
|
||||
log.info("[审批联动] 实验标准 {} 审批通过,审核状态置为已批准", ctx.getBizDataId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext ctx) {
|
||||
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT);
|
||||
log.info("[审批联动] 实验标准 {} 被驳回,审核状态回退为草稿", ctx.getBizDataId());
|
||||
}
|
||||
|
||||
private void updateAuditStatus(String bizDataId, String auditStatus) {
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
return;
|
||||
}
|
||||
stdService.lambdaUpdate()
|
||||
.eq(MesXslRubberQuickTestStd::getId, bizDataId)
|
||||
.set(MesXslRubberQuickTestStd::getAuditStatus, auditStatus)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.DictModel;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
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.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* MES 审批流设计
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Tag(name = "MES审批流设计")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalFlow")
|
||||
@Slf4j
|
||||
public class MesXslApprovalFlowController extends JeecgController<MesXslApprovalFlow, IMesXslApprovalFlowService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService mesXslApprovalFlowService;
|
||||
|
||||
@Autowired
|
||||
private ISysBaseAPI sysBaseAPI;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
||||
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
|
||||
/**
|
||||
* 审批阶段关键字配置(有序):key=阶段标识,name=阶段中文,nodeType=对应节点类型,keywords=列注释匹配关键字。
|
||||
* 解析顺序即默认流程顺序:校对 -> 审核 -> 审批 -> 分发 -> 抄送。
|
||||
*/
|
||||
private static final String[][] STAGE_DEFS = new String[][]{
|
||||
{"proofread", "校对", "approver", "校对"},
|
||||
{"review", "审核", "approver", "审核|审查"},
|
||||
{"approve", "审批", "approver", "审批|批准|核准"},
|
||||
{"distribute", "分发", "approver", "分发|发放"},
|
||||
{"cc", "抄送", "cc", "抄送"},
|
||||
};
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
|
||||
/**
|
||||
* 根据所选单据表名翻译字典,回填单据中文名
|
||||
*/
|
||||
private void fillBizTableName(MesXslApprovalFlow flow) {
|
||||
if (oConvertUtils.isEmpty(flow.getBizTable())) {
|
||||
return;
|
||||
}
|
||||
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
|
||||
if (items != null) {
|
||||
for (DictModel item : items) {
|
||||
if (flow.getBizTable().equals(item.getValue())) {
|
||||
flow.setBizTableName(item.getText());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "审批流设计-分页列表查询")
|
||||
@RequiresPermissions("approval:flow:list")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslApprovalFlow>> queryPageList(
|
||||
MesXslApprovalFlow mesXslApprovalFlow,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslApprovalFlow> queryWrapper = QueryGenerator.initQueryWrapper(mesXslApprovalFlow, req.getParameterMap());
|
||||
// 列表查询不返回大字段 flowConfig,避免传输冗余
|
||||
queryWrapper.select(MesXslApprovalFlow.class, info -> !"flow_config".equals(info.getColumn()));
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
Page<MesXslApprovalFlow> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslApprovalFlow> pageList = mesXslApprovalFlowService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-添加")
|
||||
@Operation(summary = "审批流设计-添加")
|
||||
@RequiresPermissions("approval:flow:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getFlowName())) {
|
||||
return Result.error("审批流名称不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getBizTable())) {
|
||||
return Result.error("请先选择绑定单据");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getStatus())) {
|
||||
mesXslApprovalFlow.setStatus("0");
|
||||
}
|
||||
fillBizTableName(mesXslApprovalFlow);
|
||||
mesXslApprovalFlowService.save(mesXslApprovalFlow);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-编辑")
|
||||
@Operation(summary = "审批流设计-编辑")
|
||||
@RequiresPermissions("approval:flow:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
fillBizTableName(mesXslApprovalFlow);
|
||||
mesXslApprovalFlowService.updateById(mesXslApprovalFlow);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-保存流程设计")
|
||||
@Operation(summary = "审批流设计-保存流程设计")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@PostMapping(value = "/saveDesign")
|
||||
public Result<String> saveDesign(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
MesXslApprovalFlow update = new MesXslApprovalFlow();
|
||||
update.setId(mesXslApprovalFlow.getId());
|
||||
update.setFlowConfig(mesXslApprovalFlow.getFlowConfig());
|
||||
// 设计保存时若仍为草稿则置为已发布
|
||||
if (oConvertUtils.isNotEmpty(mesXslApprovalFlow.getStatus())) {
|
||||
update.setStatus(mesXslApprovalFlow.getStatus());
|
||||
}
|
||||
mesXslApprovalFlowService.updateById(update);
|
||||
return Result.OK("流程设计已保存!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-发布/停用")
|
||||
@Operation(summary = "审批流设计-发布/停用")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@PostMapping(value = "/updateStatus")
|
||||
public Result<String> updateStatus(
|
||||
@RequestParam(name = "id") String id,
|
||||
@RequestParam(name = "status") String status) {
|
||||
if (!"0".equals(status) && !"1".equals(status) && !"2".equals(status)) {
|
||||
return Result.error("状态参数非法");
|
||||
}
|
||||
boolean ok = mesXslApprovalFlowService.lambdaUpdate()
|
||||
.eq(MesXslApprovalFlow::getId, id)
|
||||
.set(MesXslApprovalFlow::getStatus, status)
|
||||
.update();
|
||||
return ok ? Result.OK("操作成功") : Result.error("操作失败");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-删除")
|
||||
@Operation(summary = "审批流设计-删除")
|
||||
@RequiresPermissions("approval:flow:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
mesXslApprovalFlowService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-批量删除")
|
||||
@Operation(summary = "审批流设计-批量删除")
|
||||
@RequiresPermissions("approval:flow:delete")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids") String ids) {
|
||||
mesXslApprovalFlowService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "审批流设计-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslApprovalFlow> queryById(@RequestParam(name = "id") String id) {
|
||||
MesXslApprovalFlow entity = mesXslApprovalFlowService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
|
||||
/**
|
||||
* 设计上下文:供全局"审批流程设计"悬浮按钮调用。
|
||||
* 1) 根据当前功能页路由反查绑定的业务表;
|
||||
* 2) 解析该表的字段,识别"校对/审核/审批/分发/抄送"等阶段字段(不存在不报错,存在即解析);
|
||||
* 3) 取/建该业务表的草稿审批流,返回流程记录(含id)供直接进入设计器。
|
||||
*
|
||||
* @param routePath 当前功能页前端路由(如 /xslmes/mesXslFormulaSpec/MesXslFormulaSpecList)
|
||||
*/
|
||||
@Operation(summary = "审批流设计-当前页设计上下文")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@GetMapping(value = "/designContext")
|
||||
public Result<Map<String, Object>> designContext(@RequestParam(name = "routePath") String routePath) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("routePath", routePath);
|
||||
data.put("bizTable", null);
|
||||
data.put("bizTableName", null);
|
||||
data.put("stages", new ArrayList<>());
|
||||
data.put("flow", null);
|
||||
|
||||
// 1) 路由 -> 业务表名(无法反查时不报错,返回空上下文,前端提示)
|
||||
String table = resolveTableByRoutePath(routePath);
|
||||
if (oConvertUtils.isEmpty(table) || !tableExists(table)) {
|
||||
return Result.OK(data);
|
||||
}
|
||||
data.put("bizTable", table);
|
||||
|
||||
// 2) 业务表中文名:优先字典,其次表注释
|
||||
String bizTableName = resolveBizTableName(table);
|
||||
data.put("bizTableName", bizTableName);
|
||||
|
||||
// 3) 解析阶段字段
|
||||
data.put("stages", parseStageFields(table));
|
||||
|
||||
// 4) 取/建草稿审批流
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
MesXslApprovalFlow flow = findFlowByTable(table, tenantId);
|
||||
if (flow == null) {
|
||||
flow = new MesXslApprovalFlow();
|
||||
flow.setFlowName((oConvertUtils.isNotEmpty(bizTableName) ? bizTableName : table) + "审批流");
|
||||
flow.setBizTable(table);
|
||||
flow.setBizTableName(bizTableName);
|
||||
flow.setRoutePath(routePath);
|
||||
flow.setStatus("0");
|
||||
if (tenantId != null) {
|
||||
flow.setTenantId(tenantId);
|
||||
}
|
||||
mesXslApprovalFlowService.save(flow);
|
||||
} else if (oConvertUtils.isEmpty(flow.getRoutePath())) {
|
||||
// 历史数据未记录路由,回填一次便于后续发起按钮匹配
|
||||
mesXslApprovalFlowService.lambdaUpdate()
|
||||
.eq(MesXslApprovalFlow::getId, flow.getId())
|
||||
.set(MesXslApprovalFlow::getRoutePath, routePath)
|
||||
.update();
|
||||
flow.setRoutePath(routePath);
|
||||
}
|
||||
data.put("flow", flow);
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据前端路由反查业务表名。
|
||||
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List,sys_permission.component 形如
|
||||
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,url 即路由。
|
||||
* 反查:url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
|
||||
*/
|
||||
private String resolveTableByRoutePath(String routePath) {
|
||||
if (oConvertUtils.isEmpty(routePath)) {
|
||||
return null;
|
||||
}
|
||||
String path = routePath.trim().replaceAll("/+$", "");
|
||||
String sql = "SELECT component FROM sys_permission WHERE menu_type IN (0,1) "
|
||||
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
|
||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||
String component;
|
||||
try {
|
||||
List<String> list = jdbcTemplate.queryForList(sql, String.class, path);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
component = list.get(0);
|
||||
} catch (Exception e) {
|
||||
log.warn("反查菜单组件失败 routePath={}", routePath, e);
|
||||
return null;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(component)) {
|
||||
return null;
|
||||
}
|
||||
// 取组件路径末段:xslmes/mesXslFormulaSpec/MesXslFormulaSpecList -> MesXslFormulaSpecList
|
||||
String comp = component.contains("/") ? component.substring(component.lastIndexOf('/') + 1) : component;
|
||||
if (comp.endsWith("List")) {
|
||||
comp = comp.substring(0, comp.length() - "List".length());
|
||||
}
|
||||
return camelToUnderline(comp);
|
||||
}
|
||||
|
||||
/** 驼峰转下划线小写:MesXslFormulaSpec -> mes_xsl_formula_spec */
|
||||
private String camelToUnderline(String camel) {
|
||||
if (oConvertUtils.isEmpty(camel)) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < camel.length(); i++) {
|
||||
char c = camel.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) {
|
||||
sb.append('_');
|
||||
}
|
||||
sb.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 校验表是否存在于当前库 */
|
||||
private boolean tableExists(String table) {
|
||||
if (!IDENTIFIER.matcher(table).matches()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
|
||||
Integer.class, table);
|
||||
return cnt != null && cnt > 0;
|
||||
} catch (Exception e) {
|
||||
log.warn("校验表存在失败 table={}", table, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 业务表中文名:优先字典 mes_xsl_approval_biz_doc,其次表注释 */
|
||||
private String resolveBizTableName(String table) {
|
||||
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
|
||||
if (items != null) {
|
||||
for (DictModel item : items) {
|
||||
if (table.equals(item.getValue())) {
|
||||
return item.getText();
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
List<String> comments = jdbcTemplate.queryForList(
|
||||
"SELECT table_comment FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
|
||||
String.class, table);
|
||||
if (!comments.isEmpty() && oConvertUtils.isNotEmpty(comments.get(0))) {
|
||||
return comments.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询表注释失败 table={}", table, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析表字段,识别审批阶段字段。每个阶段最多取一个字段(优先列注释含"人/员"的人员字段)。
|
||||
* 返回有序列表:[{stageKey, stageName, nodeType, field, fieldComment}]
|
||||
*/
|
||||
private List<Map<String, Object>> parseStageFields(String table) {
|
||||
List<Map<String, Object>> stages = new ArrayList<>();
|
||||
List<Map<String, Object>> columns;
|
||||
try {
|
||||
columns = jdbcTemplate.queryForList(
|
||||
"SELECT column_name AS name, column_comment AS comment FROM information_schema.columns "
|
||||
+ "WHERE table_schema = (SELECT DATABASE()) AND table_name = ? ORDER BY ordinal_position",
|
||||
table);
|
||||
} catch (Exception e) {
|
||||
log.warn("查询表字段失败 table={}", table, e);
|
||||
return stages;
|
||||
}
|
||||
for (String[] def : STAGE_DEFS) {
|
||||
String stageKey = def[0];
|
||||
String stageName = def[1];
|
||||
String nodeType = def[2];
|
||||
String[] keywords = def[3].split("\\|");
|
||||
Map<String, Object> hit = matchStageColumn(columns, keywords);
|
||||
if (hit != null) {
|
||||
Map<String, Object> stage = new LinkedHashMap<>();
|
||||
stage.put("stageKey", stageKey);
|
||||
stage.put("stageName", stageName);
|
||||
stage.put("nodeType", nodeType);
|
||||
stage.put("field", hit.get("name"));
|
||||
stage.put("fieldComment", hit.get("comment"));
|
||||
stages.add(stage);
|
||||
}
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
|
||||
/** 在列集合中按关键字匹配阶段字段,优先返回注释含"人/员"的人员字段 */
|
||||
private Map<String, Object> matchStageColumn(List<Map<String, Object>> columns, String[] keywords) {
|
||||
Map<String, Object> firstMatch = null;
|
||||
for (Map<String, Object> col : columns) {
|
||||
String comment = col.get("comment") == null ? "" : String.valueOf(col.get("comment"));
|
||||
if (oConvertUtils.isEmpty(comment)) {
|
||||
continue;
|
||||
}
|
||||
boolean matched = false;
|
||||
for (String kw : keywords) {
|
||||
if (comment.contains(kw)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
continue;
|
||||
}
|
||||
if (firstMatch == null) {
|
||||
firstMatch = col;
|
||||
}
|
||||
// 人员字段优先(如"校对人""审核员")
|
||||
if (comment.contains("人") || comment.contains("员")) {
|
||||
return col;
|
||||
}
|
||||
}
|
||||
return firstMatch;
|
||||
}
|
||||
|
||||
/** 按业务表+租户查找审批流(取最近一条) */
|
||||
private MesXslApprovalFlow findFlowByTable(String table, Integer tenantId) {
|
||||
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
|
||||
qw.eq("biz_table", table);
|
||||
if (tenantId != null) {
|
||||
qw.eq("tenant_id", tenantId);
|
||||
}
|
||||
qw.orderByDesc("create_time");
|
||||
qw.last("LIMIT 1");
|
||||
return mesXslApprovalFlowService.getOne(qw, false);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES 审批办理(运行时-流转)。
|
||||
* 供 IM 审批卡片按钮调用:查看详情 / 审批通过 / 驳回。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
@Tag(name = "MES审批办理")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalHandle")
|
||||
@Slf4j
|
||||
public class MesXslApprovalHandleController {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
|
||||
@Operation(summary = "审批办理-单据详情")
|
||||
@GetMapping("/detail")
|
||||
public Result<Map<String, Object>> detail(@RequestParam("instanceId") String instanceId) {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
Map<String, Object> data = approvalHandleService.detail(instanceId, user);
|
||||
if (data == null || data.isEmpty()) {
|
||||
return Result.error("审批实例不存在");
|
||||
}
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "审批办理-实时状态(卡片置灰判断)")
|
||||
@GetMapping("/status")
|
||||
public Result<Map<String, Object>> status(@RequestParam("instanceId") String instanceId) {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
Map<String, Object> data = approvalHandleService.statusInfo(instanceId, user);
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "审批办理-通过")
|
||||
@PostMapping("/approve")
|
||||
public Result<String> approve(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
String comment = body.get("comment") == null ? null : String.valueOf(body.get("comment"));
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.approve(instanceId, comment, user);
|
||||
}
|
||||
|
||||
@Operation(summary = "审批办理-驳回")
|
||||
@PostMapping("/reject")
|
||||
public Result<String> reject(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.reject(instanceId, reason, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
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.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* MES 审批发起(运行时-本期仅发起)
|
||||
* 供全局"发起审批"悬浮按钮调用:选单据类型 -> 选单据记录 -> 发起。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Tag(name = "MES审批发起")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalLaunch")
|
||||
@Slf4j
|
||||
public class MesXslApprovalLaunchController {
|
||||
|
||||
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
||||
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService approvalFlowService;
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalInstanceService approvalInstanceService;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起改用流转引擎进入首节点-----
|
||||
@Autowired
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起改用流转引擎进入首节点-----
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(按租户隔离),即"可发起的单据类型"。
|
||||
* 同时按"功能模块(单据表)"自动反查其菜单路由填入 routePath,供前端控制悬浮按钮仅在该功能页显示,无需手工配置。
|
||||
*/
|
||||
@Operation(summary = "发起审批-已发布审批流列表")
|
||||
@GetMapping("/publishedList")
|
||||
public Result<List<MesXslApprovalFlow>> publishedList() {
|
||||
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
|
||||
qw.eq("status", "1");
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
if (tenantId != null) {
|
||||
qw.eq("tenant_id", tenantId);
|
||||
}
|
||||
qw.orderByDesc("create_time");
|
||||
List<MesXslApprovalFlow> list = approvalFlowService.list(qw);
|
||||
// 未手工指定 route_path 时,按单据表名自动反查菜单路由
|
||||
for (MesXslApprovalFlow flow : list) {
|
||||
if (oConvertUtils.isEmpty(flow.getRoutePath())) {
|
||||
flow.setRoutePath(resolveRoutePathByTable(flow.getBizTable()));
|
||||
}
|
||||
}
|
||||
return Result.OK(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据单据表名反查对应功能菜单的前端路由。
|
||||
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List(如 mes_xsl_formula_spec -> MesXslFormulaSpecList),
|
||||
* 对应 sys_permission.component 形如 xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,取其 url 即路由。
|
||||
*/
|
||||
private String resolveRoutePathByTable(String table) {
|
||||
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder comp = new StringBuilder();
|
||||
for (String p : table.split("_")) {
|
||||
if (oConvertUtils.isEmpty(p)) {
|
||||
continue;
|
||||
}
|
||||
comp.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
|
||||
}
|
||||
comp.append("List");
|
||||
String sql = "SELECT url FROM sys_permission WHERE menu_type IN (0,1) "
|
||||
+ "AND (del_flag = 0 OR del_flag IS NULL) "
|
||||
+ "AND (component LIKE ? OR component LIKE ?) "
|
||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||
try {
|
||||
List<String> urls = jdbcTemplate.queryForList(sql, String.class, "%/" + comp, "%" + comp);
|
||||
return urls.isEmpty() ? null : urls.get(0);
|
||||
} catch (Exception e) {
|
||||
log.warn("反查菜单路由失败 table={}", table, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据审批流绑定的单据表,查询业务单据记录(id + 标题),供发起时选择
|
||||
*/
|
||||
@Operation(summary = "发起审批-业务单据记录列表")
|
||||
@GetMapping("/bizRecords")
|
||||
public Result<List<Map<String, Object>>> bizRecords(
|
||||
@RequestParam("flowId") String flowId,
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
String table = flow.getBizTable();
|
||||
String titleField = flow.getTitleField();
|
||||
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return Result.error("单据表名非法");
|
||||
}
|
||||
boolean hasTitle = oConvertUtils.isNotEmpty(titleField) && IDENTIFIER.matcher(titleField).matches();
|
||||
|
||||
StringBuilder sql = new StringBuilder("SELECT id, ");
|
||||
sql.append(hasTitle ? titleField : "id").append(" AS title FROM ").append(table);
|
||||
List<Object> args = new ArrayList<>();
|
||||
if (hasTitle && oConvertUtils.isNotEmpty(keyword)) {
|
||||
sql.append(" WHERE ").append(titleField).append(" LIKE CONCAT('%', ?, '%')");
|
||||
args.add(keyword);
|
||||
}
|
||||
// 主键一定存在,按 id 倒序近似最新;限制条数防全表
|
||||
sql.append(" ORDER BY id DESC LIMIT 100");
|
||||
try {
|
||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), args.toArray());
|
||||
return Result.OK(list);
|
||||
} catch (Exception e) {
|
||||
log.error("查询业务单据失败 table={}, field={}", table, titleField, e);
|
||||
return Result.error("查询业务单据失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起审批:根据审批流定义创建审批实例,解析首个审批节点处理人
|
||||
*/
|
||||
@Operation(summary = "发起审批-发起")
|
||||
@PostMapping("/launch")
|
||||
public Result<String> launch(@RequestBody Map<String, Object> body) {
|
||||
String flowId = (String) body.get("flowId");
|
||||
String bizDataId = (String) body.get("bizDataId");
|
||||
String bizTitle = (String) body.get("bizTitle");
|
||||
if (oConvertUtils.isEmpty(flowId) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("请选择审批流和单据");
|
||||
}
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
if (!"1".equals(flow.getStatus())) {
|
||||
return Result.error("该审批流未发布,无法发起");
|
||||
}
|
||||
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
return Result.OK("发起成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发起审批:用于列表多选后一次性发起
|
||||
*/
|
||||
@Operation(summary = "发起审批-批量发起")
|
||||
@PostMapping("/launchBatch")
|
||||
public Result<String> launchBatch(@RequestBody Map<String, Object> body) {
|
||||
String flowId = (String) body.get("flowId");
|
||||
Object itemsObj = body.get("items");
|
||||
if (oConvertUtils.isEmpty(flowId) || !(itemsObj instanceof List) || ((List<?>) itemsObj).isEmpty()) {
|
||||
return Result.error("请选择审批流和单据");
|
||||
}
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
if (!"1".equals(flow.getStatus())) {
|
||||
return Result.error("该审批流未发布,无法发起");
|
||||
}
|
||||
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
int count = 0;
|
||||
for (Object o : (List<?>) itemsObj) {
|
||||
if (!(o instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> item = (Map<?, ?>) o;
|
||||
String bizDataId = item.get("bizDataId") == null ? null : String.valueOf(item.get("bizDataId"));
|
||||
String bizTitle = item.get("bizTitle") == null ? null : String.valueOf(item.get("bizTitle"));
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
continue;
|
||||
}
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
return Result.error("没有有效的单据数据");
|
||||
}
|
||||
return Result.OK("已发起 " + count + " 条审批!");
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一条审批实例(基础字段;处理人解析/卡片由流转引擎完成)
|
||||
*/
|
||||
private MesXslApprovalInstance buildInstance(MesXslApprovalFlow flow, String bizDataId, String bizTitle, LoginUser loginUser) {
|
||||
MesXslApprovalInstance inst = new MesXslApprovalInstance();
|
||||
inst.setFlowId(flow.getId());
|
||||
inst.setFlowName(flow.getFlowName());
|
||||
inst.setBizTable(flow.getBizTable());
|
||||
inst.setBizTableName(flow.getBizTableName());
|
||||
inst.setBizDataId(bizDataId);
|
||||
inst.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
|
||||
inst.setStatus("0");
|
||||
if (loginUser != null) {
|
||||
inst.setApplyUser(loginUser.getUsername());
|
||||
inst.setApplyUserName(loginUser.getRealname());
|
||||
}
|
||||
inst.setApplyTime(new Date());
|
||||
inst.setTenantId(MesXslTenantUtils.resolveTenantId(flow.getTenantId()));
|
||||
// 处理人解析、节点进度初始化、卡片发送统一由 IMesXslApprovalHandleService.enterFirstNode 完成
|
||||
return inst;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.jeecg.modules.xslmes.approval.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 java.io.Serializable;
|
||||
|
||||
/**
|
||||
* MES 审批流定义
|
||||
* 钉钉式可视化审批流设计,绑定 MES 业务单据。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_flow")
|
||||
@Schema(description = "MES审批流定义")
|
||||
public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "审批流名称")
|
||||
private String flowName;
|
||||
|
||||
@Schema(description = "绑定单据表名")
|
||||
@Dict(dicCode = "mes_xsl_approval_biz_doc")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "绑定单据中文名(冗余展示)")
|
||||
private String bizTableName;
|
||||
|
||||
@Schema(description = "单据标题字段名(发起选单据时展示)")
|
||||
private String titleField;
|
||||
|
||||
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
|
||||
private String routePath;
|
||||
|
||||
@Schema(description = "流程设计JSON(钉钉式节点树)")
|
||||
private String flowConfig;
|
||||
|
||||
@Schema(description = "状态:0草稿 1已发布 2已停用")
|
||||
@Dict(dicCode = "mes_xsl_approval_flow_status")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Double sortNo;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.jeecg.modules.xslmes.approval.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.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MES 审批实例(本期仅记录发起,不含办理)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_instance")
|
||||
@Schema(description = "MES审批实例")
|
||||
public class MesXslApprovalInstance extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "审批流定义ID")
|
||||
private String flowId;
|
||||
|
||||
@Schema(description = "审批流名称")
|
||||
private String flowName;
|
||||
|
||||
@Schema(description = "业务单据表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据中文名")
|
||||
private String bizTableName;
|
||||
|
||||
@Schema(description = "业务单据记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "业务单据展示标题")
|
||||
private String bizTitle;
|
||||
|
||||
@Schema(description = "当前节点ID")
|
||||
private String currentNodeId;
|
||||
|
||||
@Schema(description = "当前节点名称")
|
||||
private String currentNodeName;
|
||||
|
||||
@Schema(description = "当前处理人(username逗号分隔)")
|
||||
private String currentHandlers;
|
||||
|
||||
@Schema(description = "当前处理人展示文本")
|
||||
private String currentHandlersText;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
|
||||
@Schema(description = "当前节点处理进度JSON(nodeId/mode/tasks)")
|
||||
private String nodeProgress;
|
||||
|
||||
@Schema(description = "审批历史JSON数组")
|
||||
private String history;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
|
||||
|
||||
@Schema(description = "状态:0审批中 1已通过 2已驳回 3已撤销")
|
||||
@Dict(dicCode = "mes_xsl_approval_instance_status")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "发起人username")
|
||||
private String applyUser;
|
||||
|
||||
@Schema(description = "发起人姓名")
|
||||
private String applyUserName;
|
||||
|
||||
@Schema(description = "发起时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date applyTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
|
||||
/**
|
||||
* MES 审批流定义 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Mapper
|
||||
public interface MesXslApprovalFlowMapper extends BaseMapper<MesXslApprovalFlow> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
/**
|
||||
* MES 审批实例 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Mapper
|
||||
public interface MesXslApprovalInstanceMapper extends BaseMapper<MesXslApprovalInstance> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
|
||||
/**
|
||||
* MES 审批流定义 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
public interface IMesXslApprovalFlowService extends IService<MesXslApprovalFlow> {
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES 审批办理/流转引擎。
|
||||
* 负责:发起后进入首节点、审批通过/驳回的流转推进(支持会签/或签/依次)、查看单据全字段详情。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
public interface IMesXslApprovalHandleService {
|
||||
|
||||
/**
|
||||
* 发起后进入首个审批节点:解析处理人、初始化节点进度、发送审批卡片。
|
||||
* 实例需已先行 save(带 id)。
|
||||
*/
|
||||
void enterFirstNode(MesXslApprovalInstance inst, LoginUser applyUser);
|
||||
|
||||
/**
|
||||
* 审批通过:标记当前处理人任务完成,按节点 multiMode 判断是否流转到下一节点。
|
||||
*/
|
||||
Result<String> approve(String instanceId, String comment, LoginUser user);
|
||||
|
||||
/**
|
||||
* 驳回:任一处理人驳回即终止流程,通知发起人。
|
||||
*/
|
||||
Result<String> reject(String instanceId, String reason, LoginUser user);
|
||||
|
||||
/**
|
||||
* 查看单据全部字段 + 审批进度/历史,供 IM 卡片"查看详情"弹窗使用。
|
||||
*/
|
||||
Map<String, Object> detail(String instanceId, LoginUser user);
|
||||
|
||||
/**
|
||||
* 轻量状态查询:供 IM 卡片实时判断是否仍可办理(旧节点卡片置灰)。
|
||||
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
|
||||
*/
|
||||
Map<String, Object> statusInfo(String instanceId, LoginUser user);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
/**
|
||||
* MES 审批实例 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
public interface IMesXslApprovalInstanceService extends IService<MesXslApprovalInstance> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalFlowMapper;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 审批流定义 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Service
|
||||
public class MesXslApprovalFlowServiceImpl extends ServiceImpl<MesXslApprovalFlowMapper, MesXslApprovalFlow> implements IMesXslApprovalFlowService {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalInstanceMapper;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 审批实例 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Service
|
||||
public class MesXslApprovalInstanceServiceImpl extends ServiceImpl<MesXslApprovalInstanceMapper, MesXslApprovalInstance> implements IMesXslApprovalInstanceService {
|
||||
}
|
||||
@@ -48,6 +48,21 @@ public interface ISysImChatService {
|
||||
|
||||
SysImMessageVO sendMessage(String userId, Integer tenantId, SysImSendMessageDTO dto);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
|
||||
/**
|
||||
* 系统消息:以 fromUser 身份给 toUser 发送单聊消息。
|
||||
* 与 sendMessage 区别:自动获取/创建单聊会话,且不做"同部门/同租户聊天"校验,专供审批通知等系统场景。
|
||||
*
|
||||
* @param fromUserId 发送人用户ID(一般为业务发起人)
|
||||
* @param toUserId 接收人用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param content 消息内容
|
||||
* @param msgType 消息类型 text/biz_record 等,空则按 text
|
||||
* @return 消息VO,发送失败返回 null
|
||||
*/
|
||||
SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
|
||||
|
||||
|
||||
|
||||
void markRead(String userId, String conversationId);
|
||||
|
||||
@@ -473,6 +473,57 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
}
|
||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】发送消息-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType) {
|
||||
if (oConvertUtils.isEmpty(fromUserId) || oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) {
|
||||
return null;
|
||||
}
|
||||
// 不给自己发送
|
||||
if (fromUserId.equals(toUserId)) {
|
||||
return null;
|
||||
}
|
||||
Integer tenant = tenantId == null ? 0 : tenantId;
|
||||
Date now = new Date();
|
||||
// 获取或创建单聊会话(系统通知场景,不做同部门校验)
|
||||
String pairKey = buildPairKey(fromUserId, toUserId);
|
||||
SysImConversation conversation = conversationMapper.selectOne(new LambdaQueryWrapper<SysImConversation>()
|
||||
.eq(SysImConversation::getTenantId, tenant)
|
||||
.eq(SysImConversation::getUserPairKey, pairKey));
|
||||
if (conversation == null) {
|
||||
conversation = new SysImConversation();
|
||||
conversation.setConvType(CONV_TYPE_SINGLE);
|
||||
conversation.setUserPairKey(pairKey);
|
||||
conversation.setTenantId(tenant);
|
||||
conversation.setCreateBy(fromUserId);
|
||||
conversation.setCreateTime(now);
|
||||
conversation.setUpdateTime(now);
|
||||
conversationMapper.insert(conversation);
|
||||
createMember(conversation.getId(), fromUserId, now);
|
||||
createMember(conversation.getId(), toUserId, now);
|
||||
}
|
||||
// 写入消息
|
||||
SysImMessage message = new SysImMessage();
|
||||
message.setConversationId(conversation.getId());
|
||||
message.setSenderId(fromUserId);
|
||||
message.setContent(content.trim());
|
||||
message.setMsgType(oConvertUtils.isEmpty(msgType) ? MSG_TYPE_TEXT : msgType);
|
||||
message.setTenantId(tenant);
|
||||
message.setCreateTime(now);
|
||||
messageMapper.insert(message);
|
||||
// 更新会话摘要与未读、推送
|
||||
conversation.setLastContent(truncate(resolveLastContent(message.getMsgType(), message.getContent()), 200));
|
||||
conversation.setLastTime(now);
|
||||
conversation.setUpdateTime(now);
|
||||
conversationMapper.updateById(conversation);
|
||||
memberMapper.incrementUnreadExceptSender(conversation.getId(), fromUserId);
|
||||
SysImMessageVO messageVo = toMessageVo(message, fromUserId);
|
||||
pushChatMessage(conversation.getId(), fromUserId, messageVo, conversation.getConvType());
|
||||
return messageVo;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
|
||||
|
||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】标记已读-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
-- 【QH-MES审批流设计】审批流定义表 + 可审批单据字典 + 我的租户菜单与授权
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 1) 审批流定义表
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_approval_flow` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`flow_name` varchar(100) NOT NULL COMMENT '审批流名称',
|
||||
`biz_table` varchar(100) NOT NULL COMMENT '绑定单据表名',
|
||||
`biz_table_name` varchar(200) DEFAULT NULL COMMENT '绑定单据中文名(冗余展示)',
|
||||
`flow_config` longtext COMMENT '流程设计JSON(钉钉式节点树)',
|
||||
`status` varchar(1) DEFAULT '0' COMMENT '状态 0草稿 1已发布 2已停用',
|
||||
`sort_no` double DEFAULT '0' COMMENT '排序',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`del_flag` int DEFAULT '0' COMMENT '逻辑删除 0正常 1已删除',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
|
||||
`sys_org_code` varchar(64) 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 '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_appr_flow_tenant_biz` (`tenant_id`, `biz_table`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批流定义表';
|
||||
|
||||
-- 2) 可审批单据字典(单据来源:MES现有业务表) item_value=表名 item_text=单据中文名
|
||||
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
VALUES ('1995000000000000310', '可审批单据', 'mes_xsl_approval_biz_doc', 'MES审批流可绑定的业务单据', 0, 'admin', NOW(), 0, 0);
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
('1995000000000000311', '1995000000000000310', '配合示方', 'mes_xsl_formula_spec', '配合示方主表', 1, 1, 'admin', NOW()),
|
||||
('1995000000000000312', '1995000000000000310', '混炼示方', 'mes_xsl_mixing_spec', '混炼示方主表', 2, 1, 'admin', NOW()),
|
||||
('1995000000000000313', '1995000000000000310', '密炼PS编制', 'mes_xsl_mixer_ps_compile', '密炼PS编制', 3, 1, 'admin', NOW()),
|
||||
('1995000000000000314', '1995000000000000310', '胶料快检标准', 'mes_xsl_rubber_quick_test_std', '胶料快检实验标准', 4, 1, 'admin', NOW()),
|
||||
('1995000000000000315', '1995000000000000310', '原料入场记录', 'mes_xsl_raw_material_entry', '原料入场记录', 5, 1, 'admin', NOW());
|
||||
|
||||
-- 2.1) 审批流状态字典
|
||||
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
VALUES ('1995000000000000320', '审批流状态', 'mes_xsl_approval_flow_status', 'MES审批流定义状态', 0, 'admin', NOW(), 0, 0);
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
('1995000000000000321', '1995000000000000320', '草稿', '0', '草稿', 1, 1, 'admin', NOW()),
|
||||
('1995000000000000322', '1995000000000000320', '已发布', '1', '已发布', 2, 1, 'admin', NOW()),
|
||||
('1995000000000000323', '1995000000000000320', '已停用', '2', '已停用', 3, 1, 'admin', NOW());
|
||||
|
||||
-- 3) 我的租户 -> 审批流设计 菜单(parent_id=我的租户 1674708136602542082)
|
||||
INSERT IGNORE 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 (
|
||||
'1995000000000000301', '1674708136602542082', '审批流设计', '/approval/ApprovalFlowList',
|
||||
'approval/flow/ApprovalFlowList', 1, 'ApprovalFlowList', NULL,
|
||||
1, NULL, '0', 4.00, 0, 'ant-design:partition-outlined', 0, 1,
|
||||
0, 0, '租户审批流可视化设计', 'admin', NOW(), 'admin', NOW(),
|
||||
0, 0, '1', 0
|
||||
);
|
||||
|
||||
-- 按钮权限
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000302', '1995000000000000301', '查询', 2, 'approval:flow:list', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000303', '1995000000000000301', '新增', 2, 'approval:flow:add', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000304', '1995000000000000301', '编辑', 2, 'approval:flow:edit', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000305', '1995000000000000301', '删除', 2, 'approval:flow:delete', '1', 4.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000306', '1995000000000000301', '设计/发布', 2, 'approval:flow:design', '1', 5.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
-- 4) 授权给超级管理员 admin
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`role_code` = 'admin'
|
||||
AND p.`id` IN ('1995000000000000301','1995000000000000302','1995000000000000303','1995000000000000304','1995000000000000305','1995000000000000306')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
|
||||
-- 5) 授权给租户管理员 zuhuadmin(挂在"我的租户"下必须授权,否则租户管理员看不到)
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`role_code` = 'zuhuadmin'
|
||||
AND p.`id` IN ('1995000000000000301','1995000000000000302','1995000000000000303','1995000000000000304','1995000000000000305','1995000000000000306')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
-- 【QH-MES审批流设计】审批实例表 + 审批流定义增加单据标题字段
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 1) 审批流定义表增加"单据标题字段名",发起时用于展示具体单据
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `title_field` varchar(100) DEFAULT NULL COMMENT '单据标题字段名(发起选单据时展示)' AFTER `biz_table_name`;
|
||||
|
||||
-- 2) 审批实例表(本期仅发起,记录实例与当前处理人)
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_approval_instance` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`flow_id` varchar(32) NOT NULL COMMENT '审批流定义ID',
|
||||
`flow_name` varchar(100) DEFAULT NULL COMMENT '审批流名称',
|
||||
`biz_table` varchar(100) DEFAULT NULL COMMENT '业务单据表名',
|
||||
`biz_table_name` varchar(200) DEFAULT NULL COMMENT '业务单据中文名',
|
||||
`biz_data_id` varchar(64) DEFAULT NULL COMMENT '业务单据记录ID',
|
||||
`biz_title` varchar(300) DEFAULT NULL COMMENT '业务单据展示标题',
|
||||
`current_node_id` varchar(64) DEFAULT NULL COMMENT '当前节点ID',
|
||||
`current_node_name` varchar(100) DEFAULT NULL COMMENT '当前节点名称',
|
||||
`current_handlers` varchar(1000) DEFAULT NULL COMMENT '当前处理人(username逗号分隔)',
|
||||
`current_handlers_text` varchar(1000) DEFAULT NULL COMMENT '当前处理人展示文本',
|
||||
`status` varchar(2) DEFAULT '0' COMMENT '状态 0审批中 1已通过 2已驳回 3已撤销',
|
||||
`apply_user` varchar(50) DEFAULT NULL COMMENT '发起人username',
|
||||
`apply_user_name` varchar(100) DEFAULT NULL COMMENT '发起人姓名',
|
||||
`apply_time` datetime DEFAULT NULL COMMENT '发起时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`del_flag` int DEFAULT '0' COMMENT '逻辑删除 0正常 1已删除',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
|
||||
`sys_org_code` varchar(64) 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 '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_appr_inst_tenant` (`tenant_id`, `flow_id`),
|
||||
KEY `idx_appr_inst_apply` (`apply_user`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批实例表';
|
||||
|
||||
-- 3) 审批实例状态字典
|
||||
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
VALUES ('1995000000000000330', '审批实例状态', 'mes_xsl_approval_instance_status', 'MES审批实例状态', 0, 'admin', NOW(), 0, 0);
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
('1995000000000000331', '1995000000000000330', '审批中', '0', '审批中', 1, 1, 'admin', NOW()),
|
||||
('1995000000000000332', '1995000000000000330', '已通过', '1', '已通过', 2, 1, 'admin', NOW()),
|
||||
('1995000000000000333', '1995000000000000330', '已驳回', '2', '已驳回', 3, 1, 'admin', NOW()),
|
||||
('1995000000000000334', '1995000000000000330', '已撤销', '3', '已撤销', 4, 1, 'admin', NOW());
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 【QH-MES审批流设计】审批流定义增加"功能页面路由",用于控制发起审批悬浮按钮仅在对应功能页显示
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `route_path` varchar(255) DEFAULT NULL COMMENT '对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)' AFTER `title_field`;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 【QH-MES审批流设计】审批办理/流转:实例表增加当前节点处理进度与历史(支持会签/或签/依次)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_instance`
|
||||
ADD COLUMN `node_progress` longtext COMMENT '当前节点处理进度JSON(nodeId/mode/tasks)' AFTER `current_handlers_text`,
|
||||
ADD COLUMN `history` longtext COMMENT '审批历史JSON数组' AFTER `node_progress`;
|
||||
113
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
Normal file
113
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
全局「审批流程设计」悬浮按钮
|
||||
拥有 approval:flow:design 权限的用户,在任意功能页点击即可:
|
||||
1)后端按当前页路由反查绑定的业务表;
|
||||
2)解析该表字段,识别「校对/审核/审批/分发/抄送」等阶段字段(不存在不报错);
|
||||
3)进入可视化设计器,可点选识别到的阶段字段按顺序生成审批流程并保存发布。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮
|
||||
-->
|
||||
<template>
|
||||
<div v-if="show" class="approval-design-float" :style="floatStyle">
|
||||
<div class="approval-design-btn" :class="{ 'is-loading': loading }" title="审批流程设计" @click="openDesigner">
|
||||
<Icon :icon="loading ? 'ant-design:loading-outlined' : 'ant-design:partition-outlined'" :size="20" :spin="loading" />
|
||||
<span class="approval-design-text">流程设计</span>
|
||||
</div>
|
||||
<!-- 流程设计器(全屏) -->
|
||||
<FlowDesign @register="registerDesign" @success="onSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { getApprovalDesignContext } from '/@/views/approval/flow/approvalFlow.api';
|
||||
import FlowDesign from '/@/views/approval/flow/components/FlowDesign.vue';
|
||||
|
||||
defineOptions({ name: 'ApprovalDesignFloat' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { currentRoute } = useRouter();
|
||||
const { hasPermission } = usePermission();
|
||||
const [registerDesign, { openModal: openDesign }] = useModal();
|
||||
|
||||
const loading = ref(false);
|
||||
// 悬浮位置(位于「发起审批」按钮上方)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '190px' });
|
||||
|
||||
// 仅拥有设计权限的用户可见
|
||||
const show = computed(() => hasPermission('approval:flow:design'));
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
async function openDesigner() {
|
||||
if (loading.value) return;
|
||||
const path = normalizePath(currentRoute.value?.path);
|
||||
if (!path) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const ctx: any = await getApprovalDesignContext(path);
|
||||
if (!ctx || !ctx.bizTable || !ctx.flow) {
|
||||
createMessage.info('当前页面未能识别到可绑定的业务单据,无法设计审批流程');
|
||||
return;
|
||||
}
|
||||
openDesign(true, {
|
||||
record: ctx.flow,
|
||||
readonly: false,
|
||||
paletteStages: ctx.stages || [],
|
||||
});
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取设计上下文失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess() {
|
||||
createMessage.success('审批流程已保存');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-design-float {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
.approval-design-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #15bca3, #0e9e88);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(21, 188, 163, 0.45);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
user-select: none;
|
||||
|
||||
.approval-design-text {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(21, 188, 163, 0.6);
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
283
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
Normal file
283
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<!--
|
||||
全局「发起审批」悬浮按钮
|
||||
仅在「设计并发布了审批流、且能匹配到对应功能页路由」的页面显示。
|
||||
支持两种发起方式:
|
||||
1)列表多选联动:在列表勾选数据后点击,弹窗自动带入选中单据并可批量发起;
|
||||
2)手动选择:未勾选时,在弹窗内搜索选择单条单据发起。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
-->
|
||||
<template>
|
||||
<div v-if="show" class="approval-float" :style="floatStyle">
|
||||
<div class="approval-float-btn" title="发起审批" @click="openModal">
|
||||
<Icon icon="ant-design:audit-outlined" :size="20" />
|
||||
<span class="approval-float-text">发起审批</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="visible" title="发起审批" :width="540" :confirmLoading="loading" okText="发起审批" @ok="handleLaunch">
|
||||
<a-form layout="vertical" style="margin-top: 8px">
|
||||
<a-form-item label="单据类型(审批流)" required>
|
||||
<a-select v-model:value="flowId" placeholder="请选择审批流" :options="flowOptions" @change="onFlowChange" allowClear />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 批量模式:直接展示列表勾选的单据 -->
|
||||
<a-form-item v-if="isBatch" :label="`已选单据(共 ${batchItems.length} 条)`" required>
|
||||
<div class="approval-float-batch">
|
||||
<div v-for="it in batchItems" :key="it.bizDataId" class="approval-float-batch-item">
|
||||
<Icon icon="ant-design:file-text-outlined" :size="14" />
|
||||
<span class="approval-float-batch-title">{{ it.bizTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 手动模式:搜索选择单条单据 -->
|
||||
<a-form-item v-else label="选择单据" required>
|
||||
<a-select
|
||||
v-model:value="bizDataId"
|
||||
show-search
|
||||
placeholder="请选择需要发起审批的单据"
|
||||
:filter-option="false"
|
||||
:options="recordOptions"
|
||||
:disabled="!flowId"
|
||||
@search="onSearch"
|
||||
@change="onRecordChange"
|
||||
>
|
||||
<template #notFoundContent>
|
||||
<a-spin v-if="recordLoading" size="small" />
|
||||
<span v-else>无单据数据</span>
|
||||
</template>
|
||||
</a-select>
|
||||
<div v-if="flowId && !recordOptions.length && !recordLoading" class="approval-float-tip">
|
||||
该单据暂无数据,或审批流未配置“单据标题字段”
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getPublishedFlows, getBizRecords, launchApproval, launchApprovalBatch } from '/@/views/approval/flow/launch.api';
|
||||
import { useApprovalSelection } from './useApprovalSelection';
|
||||
|
||||
defineOptions({ name: 'ApprovalLaunchFloat' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { currentRoute } = useRouter();
|
||||
const approvalSelection = useApprovalSelection();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const recordLoading = ref(false);
|
||||
|
||||
const flowId = ref<string>();
|
||||
const bizDataId = ref<string>();
|
||||
const bizTitle = ref<string>('');
|
||||
|
||||
const flowList = ref<any[]>([]);
|
||||
const recordList = ref<any[]>([]);
|
||||
// 打开弹窗时快照的列表勾选行(批量模式数据源)
|
||||
const batchRows = ref<any[]>([]);
|
||||
|
||||
// 悬浮位置(右下角)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '120px' });
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// 当前路由匹配到的已发布审批流
|
||||
const matchedFlows = computed(() => {
|
||||
const cur = normalizePath(currentRoute.value?.path);
|
||||
if (!cur) return [];
|
||||
return flowList.value.filter((f) => f.routePath && normalizePath(f.routePath) === cur);
|
||||
});
|
||||
|
||||
const show = computed(() => matchedFlows.value.length > 0);
|
||||
|
||||
const flowOptions = computed(() =>
|
||||
matchedFlows.value.map((f) => ({
|
||||
label: `${f.flowName}(${f.bizTableName || f.bizTable})`,
|
||||
value: f.id,
|
||||
}))
|
||||
);
|
||||
|
||||
// 当前选中的审批流对象
|
||||
const currentFlow = computed(() => matchedFlows.value.find((f) => f.id === flowId.value));
|
||||
|
||||
// 是否批量模式(列表有勾选)
|
||||
const isBatch = computed(() => batchRows.value.length > 0);
|
||||
|
||||
// 批量模式下的单据项(用审批流的标题字段取展示标题)
|
||||
const batchItems = computed(() => {
|
||||
const titleField = currentFlow.value?.titleField;
|
||||
return batchRows.value
|
||||
.filter((r) => r && r.id != null)
|
||||
.map((r) => ({
|
||||
bizDataId: String(r.id),
|
||||
bizTitle: titleField && r[titleField] != null ? String(r[titleField]) : String(r.id),
|
||||
}));
|
||||
});
|
||||
|
||||
const recordOptions = computed(() =>
|
||||
recordList.value.map((r) => ({
|
||||
label: r.title ?? r.id,
|
||||
value: r.id,
|
||||
}))
|
||||
);
|
||||
|
||||
onMounted(loadFlows);
|
||||
|
||||
async function loadFlows() {
|
||||
try {
|
||||
flowList.value = (await getPublishedFlows()) || [];
|
||||
} catch {
|
||||
flowList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
flowId.value = undefined;
|
||||
bizDataId.value = undefined;
|
||||
bizTitle.value = '';
|
||||
recordList.value = [];
|
||||
batchRows.value = [];
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
visible.value = true;
|
||||
resetSelection();
|
||||
// 读取当前列表页的勾选行
|
||||
batchRows.value = approvalSelection.getRowsByPath(currentRoute.value?.path || '');
|
||||
// 当前页只匹配到一个审批流时自动选中
|
||||
if (matchedFlows.value.length === 1) {
|
||||
flowId.value = matchedFlows.value[0].id;
|
||||
}
|
||||
// 手动模式且已确定审批流时预加载单据列表
|
||||
if (!isBatch.value && flowId.value) {
|
||||
await loadRecords();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(keyword?: string) {
|
||||
if (!flowId.value) return;
|
||||
try {
|
||||
recordLoading.value = true;
|
||||
recordList.value = (await getBizRecords({ flowId: flowId.value, keyword })) || [];
|
||||
} finally {
|
||||
recordLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onFlowChange() {
|
||||
bizDataId.value = undefined;
|
||||
bizTitle.value = '';
|
||||
recordList.value = [];
|
||||
if (!isBatch.value && flowId.value) loadRecords();
|
||||
}
|
||||
|
||||
const onSearch = debounce((val: string) => {
|
||||
loadRecords(val);
|
||||
}, 350);
|
||||
|
||||
function onRecordChange() {
|
||||
const hit = recordList.value.find((r) => r.id === bizDataId.value);
|
||||
bizTitle.value = hit ? hit.title ?? hit.id : '';
|
||||
}
|
||||
|
||||
async function handleLaunch() {
|
||||
if (!flowId.value) {
|
||||
createMessage.warning('请选择审批流');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
if (isBatch.value) {
|
||||
await launchApprovalBatch({ flowId: flowId.value, items: batchItems.value });
|
||||
createMessage.success(`已发起 ${batchItems.value.length} 条审批!`);
|
||||
} else {
|
||||
if (!bizDataId.value) {
|
||||
createMessage.warning('请选择需要发起审批的单据');
|
||||
return;
|
||||
}
|
||||
await launchApproval({ flowId: flowId.value, bizDataId: bizDataId.value, bizTitle: bizTitle.value });
|
||||
createMessage.success('发起成功!');
|
||||
}
|
||||
visible.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-float {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
.approval-float-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3296fa, #1668dc);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(50, 150, 250, 0.45);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
user-select: none;
|
||||
|
||||
.approval-float-text {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(50, 150, 250, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-float-batch {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
|
||||
.approval-float-batch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 2px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.approval-float-batch-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-float-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #faad14;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 审批发起-列表选中上下文(全局单例)
|
||||
* 由 useListPage 自动把当前列表页的选中行同步进来,
|
||||
* 全局「发起审批」悬浮按钮发起时直接读取,实现"列表多选 -> 发起弹窗"联动。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批支持列表多选联动
|
||||
*/
|
||||
// 当前选中的行记录
|
||||
const rows = ref<any[]>([]);
|
||||
// 选中来源页面路由,用于校验与当前页是否一致,避免跨页串数据
|
||||
const sourcePath = ref<string>('');
|
||||
|
||||
export function useApprovalSelection() {
|
||||
function setSelection(list: any[], path: string) {
|
||||
rows.value = Array.isArray(list) ? [...list] : [];
|
||||
sourcePath.value = path || '';
|
||||
}
|
||||
|
||||
function clear() {
|
||||
rows.value = [];
|
||||
sourcePath.value = '';
|
||||
}
|
||||
|
||||
/** 获取与指定路由匹配的选中行(不匹配返回空) */
|
||||
function getRowsByPath(path: string): any[] {
|
||||
return sourcePath.value === path ? rows.value : [];
|
||||
}
|
||||
|
||||
return { rows, sourcePath, setSelection, clear, getRowsByPath };
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { filterObj } from '/@/utils/common/compUtils';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { registerImPageListProvider } from '/@/views/system/im/imPageListRegistry';
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文-----
|
||||
import { useApprovalSelection } from '/@/components/ApprovalLaunch/useApprovalSelection';
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文-----
|
||||
import { buildImPageListSnapshot } from '/@/views/system/im/imPageListUtil';
|
||||
import { IM_RECORD_QUERY_KEY } from '/@/views/system/im/imBizRecordMessage';
|
||||
import {
|
||||
@@ -71,7 +74,7 @@ export function useListPage(options: ListPageOptions) {
|
||||
const tableContext = useListTable(options.tableProps);
|
||||
|
||||
const route = useRoute();
|
||||
const [, tableMethods, { selectedRowKeys }] = tableContext;
|
||||
const [, tableMethods, { selectedRowKeys, selectedRows }] = tableContext;
|
||||
const { getForm, reload, setLoading, getColumns } = tableMethods;
|
||||
const imHighlightRecordId = ref('');
|
||||
let clearHighlightTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -84,6 +87,16 @@ export function useListPage(options: ListPageOptions) {
|
||||
}
|
||||
});
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文,供全局"发起审批"悬浮按钮读取-----
|
||||
const approvalSelection = useApprovalSelection();
|
||||
watch(
|
||||
selectedRows,
|
||||
(rows) => approvalSelection.setSelection((rows as any[]) || [], route.path),
|
||||
{ deep: true },
|
||||
);
|
||||
onUnmounted(() => approvalSelection.clear());
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文,供全局"发起审批"悬浮按钮读取-----
|
||||
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】列表页注册 IM 明细快照提供器-----------
|
||||
onUnmounted(
|
||||
registerImPageListProvider(() => {
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
<LayoutFooter />
|
||||
</Layout>
|
||||
</Layout>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮----- -->
|
||||
<ApprovalLaunchFloat />
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮----- -->
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮----- -->
|
||||
<ApprovalDesignFloat />
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮----- -->
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +42,12 @@
|
||||
components: {
|
||||
LayoutFeatures: createAsyncComponent(() => import('/@/layouts/default/feature/index.vue')),
|
||||
LayoutFooter: createAsyncComponent(() => import('/@/layouts/default/footer/index.vue')),
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮-----
|
||||
ApprovalLaunchFloat: createAsyncComponent(() => import('/@/components/ApprovalLaunch/index.vue')),
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮-----
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-----
|
||||
ApprovalDesignFloat: createAsyncComponent(() => import('/@/components/ApprovalDesign/index.vue')),
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-----
|
||||
LayoutHeader,
|
||||
LayoutContent,
|
||||
LayoutSideBar,
|
||||
|
||||
94
jeecgboot-vue3/src/utils/flowApiRecorder.ts
Normal file
94
jeecgboot-vue3/src/utils/flowApiRecorder.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 审批流「回调接口」录制器。
|
||||
*
|
||||
* 审批流设计器是覆盖在业务页面之上的(同一前端应用、同一个 defHttp 实例),
|
||||
* 录制时全局拦截器会把每次业务请求的 url/method/参数 上报到这里,
|
||||
* 捕获用户点击目标按钮所触发的真实请求,自动回填到节点的回调接口配置。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】可视化录制业务按钮接口
|
||||
*/
|
||||
|
||||
export interface FlowCapturedApi {
|
||||
/** 请求路径(servlet 内路径,如 /xslmes/xxx/approve) */
|
||||
url: string;
|
||||
/** 请求方法 GET/POST/PUT/DELETE */
|
||||
method: string;
|
||||
/** 请求体(POST/PUT 时) */
|
||||
data?: any;
|
||||
/** 查询参数 */
|
||||
params?: any;
|
||||
}
|
||||
|
||||
/** 录制中状态(供设计器隐藏遮罩、显示录制条) */
|
||||
export const flowApiRecording = ref(false);
|
||||
|
||||
let resolver: ((c: FlowCapturedApi | null) => void) | null = null;
|
||||
|
||||
/** 开始录制,返回 Promise;捕获到请求或取消时 resolve(取消为 null) */
|
||||
export function startFlowApiRecord(): Promise<FlowCapturedApi | null> {
|
||||
// 若已有未完成的录制,先取消
|
||||
cancelFlowApiRecord();
|
||||
flowApiRecording.value = true;
|
||||
return new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消录制 */
|
||||
export function cancelFlowApiRecord() {
|
||||
if (resolver) {
|
||||
const r = resolver;
|
||||
resolver = null;
|
||||
flowApiRecording.value = false;
|
||||
r(null);
|
||||
} else {
|
||||
flowApiRecording.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 由全局请求拦截器调用,上报每次请求。
|
||||
* 仅在录制中生效,默认只捕获「非 GET」业务请求(按钮动作通常是写操作),
|
||||
* 并跳过审批流自身接口,避免误捕获。
|
||||
*/
|
||||
export function notifyFlowApiRecorder(config: any) {
|
||||
if (!flowApiRecording.value || !resolver) {
|
||||
return;
|
||||
}
|
||||
const method = String(config?.method || 'get').toUpperCase();
|
||||
if (method === 'GET') {
|
||||
return;
|
||||
}
|
||||
const url = config?.url;
|
||||
if (!url || typeof url !== 'string') {
|
||||
return;
|
||||
}
|
||||
// 跳过审批流/审批办理自身接口
|
||||
if (url.includes('/approvalFlow') || url.includes('/approvalHandle') || url.includes('/sys/dict')) {
|
||||
return;
|
||||
}
|
||||
const captured: FlowCapturedApi = {
|
||||
url,
|
||||
method,
|
||||
data: safeClone(config?.data),
|
||||
params: safeClone(config?.params),
|
||||
};
|
||||
const r = resolver;
|
||||
resolver = null;
|
||||
flowApiRecording.value = false;
|
||||
r(captured);
|
||||
}
|
||||
|
||||
function safeClone(v: any) {
|
||||
if (v == null || typeof v !== 'object') {
|
||||
return v;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(v));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { joinTimestamp, formatRequestDate } from './helper';
|
||||
import { useUserStoreWithOut } from '/@/store/modules/user';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】回调接口录制-----
|
||||
import { notifyFlowApiRecorder } from '/@/utils/flowApiRecorder';
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】回调接口录制-----
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix;
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
@@ -90,6 +93,9 @@ const transform: AxiosTransform = {
|
||||
|
||||
// 请求之前处理config
|
||||
beforeRequestHook: (config, options) => {
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制时捕获原始servlet路径(未加前缀/上下文)-----
|
||||
notifyFlowApiRecorder({ url: config.url, method: config.method, data: config.data, params: config.params });
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制时捕获原始servlet路径(未加前缀/上下文)-----
|
||||
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
|
||||
|
||||
// http开头的请求url,不加前缀
|
||||
|
||||
128
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
Normal file
128
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
审批流设计 列表页
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd" v-auth="'approval:flow:add'">新增审批流</a-button>
|
||||
<a-button
|
||||
v-if="selectedRowKeys.length > 0"
|
||||
danger
|
||||
preIcon="ant-design:delete-outlined"
|
||||
@click="handleBatchDelete"
|
||||
v-auth="'approval:flow:delete'"
|
||||
style="margin-left: 8px"
|
||||
>批量删除</a-button
|
||||
>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getActions(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 基本信息弹窗 -->
|
||||
<ApprovalFlowModal @register="registerModal" @success="reload" />
|
||||
<!-- 流程设计器 -->
|
||||
<FlowDesign @register="registerDesign" @success="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema } from './approvalFlow.data';
|
||||
import { getApprovalFlowList, deleteApprovalFlow, batchDeleteApprovalFlow, updateApprovalFlowStatus } from './approvalFlow.api';
|
||||
import ApprovalFlowModal from './ApprovalFlowModal.vue';
|
||||
import FlowDesign from './components/FlowDesign.vue';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowList' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerDesign, { openModal: openDesign }] = useModal();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '审批流列表',
|
||||
api: getApprovalFlowList,
|
||||
columns,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
labelWidth: 90,
|
||||
},
|
||||
actionColumn: {
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
},
|
||||
showIndexColumn: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false });
|
||||
}
|
||||
|
||||
function handleEdit(record) {
|
||||
openModal(true, { isUpdate: true, record });
|
||||
}
|
||||
|
||||
// 打开可视化设计器
|
||||
function handleDesign(record, readonly = false) {
|
||||
openDesign(true, { record, readonly });
|
||||
}
|
||||
|
||||
function handleDelete(record) {
|
||||
deleteApprovalFlow({ id: record.id }, reload);
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
batchDeleteApprovalFlow({ ids: selectedRowKeys.value.join(',') }, reload);
|
||||
}
|
||||
|
||||
// 发布 / 停用
|
||||
async function handleToggleStatus(record) {
|
||||
const target = record.status === '1' ? '2' : '1';
|
||||
await updateApprovalFlowStatus({ id: record.id, status: target });
|
||||
createMessage.success(target === '1' ? '已发布' : '已停用');
|
||||
reload();
|
||||
}
|
||||
|
||||
function getActions(record) {
|
||||
return [
|
||||
{
|
||||
label: '设计',
|
||||
auth: 'approval:flow:design',
|
||||
onClick: handleDesign.bind(null, record, false),
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
auth: 'approval:flow:edit',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: record.status === '1' ? '停用' : '发布',
|
||||
auth: 'approval:flow:design',
|
||||
popConfirm: {
|
||||
title: `确认${record.status === '1' ? '停用' : '发布'}该审批流?`,
|
||||
confirm: handleToggleStatus.bind(null, record),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
color: 'error',
|
||||
auth: 'approval:flow:delete',
|
||||
popConfirm: {
|
||||
title: '确认删除该审批流?',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
51
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
Normal file
51
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
审批流 基本信息 新增/编辑弹窗(先选单据)
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="560" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from './approvalFlow.data';
|
||||
import { saveOrUpdateApprovalFlow, getApprovalFlowById } from './approvalFlow.api';
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
|
||||
const isUpdate = ref(true);
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑审批流' : '新增审批流'));
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
labelWidth: 100,
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate) && data?.record?.id) {
|
||||
const record = await getApprovalFlowById({ id: data.record.id });
|
||||
await setFieldsValue({ ...record });
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdateApprovalFlow(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
75
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
Normal file
75
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 审批流设计 API
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
enum Api {
|
||||
list = '/xslmes/approvalFlow/list',
|
||||
save = '/xslmes/approvalFlow/add',
|
||||
edit = '/xslmes/approvalFlow/edit',
|
||||
get = '/xslmes/approvalFlow/queryById',
|
||||
saveDesign = '/xslmes/approvalFlow/saveDesign',
|
||||
updateStatus = '/xslmes/approvalFlow/updateStatus',
|
||||
delete = '/xslmes/approvalFlow/delete',
|
||||
deleteBatch = '/xslmes/approvalFlow/deleteBatch',
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
designContext = '/xslmes/approvalFlow/designContext',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*/
|
||||
export const getApprovalFlowList = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
/**
|
||||
* 新增/编辑基本信息
|
||||
*/
|
||||
export const saveOrUpdateApprovalFlow = (params, isUpdate) => {
|
||||
const url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过 id 查询(含流程设计 JSON)
|
||||
*/
|
||||
export const getApprovalFlowById = (params) => defHttp.get({ url: Api.get, params });
|
||||
|
||||
/**
|
||||
* 保存流程设计(节点树 JSON)
|
||||
*/
|
||||
export const saveApprovalFlowDesign = (params) => defHttp.post({ url: Api.saveDesign, params });
|
||||
|
||||
/**
|
||||
* 发布 / 停用
|
||||
*/
|
||||
export const updateApprovalFlowStatus = (params) => defHttp.post({ url: Api.updateStatus, params }, { joinParamsToUrl: true });
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const deleteApprovalFlow = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
export const batchDeleteApprovalFlow = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
/**
|
||||
* 获取当前功能页的审批流设计上下文:
|
||||
* 返回 { routePath, bizTable, bizTableName, stages[], flow },
|
||||
* stages 为识别到的阶段字段(校对/审核/审批/分发/抄送),flow 为可直接进入设计器的流程记录。
|
||||
*/
|
||||
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
112
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
Normal file
112
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
/**
|
||||
* 审批流设计 列表列 / 表单 schema
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '审批流名称',
|
||||
dataIndex: 'flowName',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '绑定单据',
|
||||
dataIndex: 'bizTableName',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status_dictText',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'flowName',
|
||||
label: '审批流名称',
|
||||
component: 'Input',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'bizTable',
|
||||
label: '绑定单据',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_biz_doc',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_flow_status',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: '主键',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
field: 'flowName',
|
||||
label: '审批流名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入审批流名称',
|
||||
maxlength: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'bizTable',
|
||||
label: '绑定单据',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_biz_doc',
|
||||
placeholder: '请先选择需要审批的单据',
|
||||
},
|
||||
// 编辑时禁止修改绑定单据,避免已设计的节点条件与单据字段错配
|
||||
dynamicDisabled: ({ values }) => !!values.id,
|
||||
},
|
||||
{
|
||||
field: 'titleField',
|
||||
label: '单据标题字段',
|
||||
component: 'Input',
|
||||
helpMessage: '发起审批时用于展示具体单据的字段名(如 spec_name、code),不填则只显示单据ID',
|
||||
componentProps: {
|
||||
placeholder: '选填,业务表中用于展示的字段名,如 spec_name',
|
||||
maxlength: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
maxlength: 500,
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
25
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
Normal file
25
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 审批办理/流转 API(供 IM 审批卡片按钮调用)
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
enum Api {
|
||||
detail = '/xslmes/approvalHandle/detail',
|
||||
status = '/xslmes/approvalHandle/status',
|
||||
approve = '/xslmes/approvalHandle/approve',
|
||||
reject = '/xslmes/approvalHandle/reject',
|
||||
}
|
||||
|
||||
/** 查看单据全部字段 + 审批进度/历史 */
|
||||
export const getApprovalDetail = (instanceId: string) => defHttp.get({ url: Api.detail, params: { instanceId } });
|
||||
|
||||
/** 轻量实时状态:用于卡片判断是否仍可办理(旧节点卡片置灰) */
|
||||
export const getApprovalStatus = (instanceId: string) => defHttp.get({ url: Api.status, params: { instanceId } });
|
||||
|
||||
/** 审批通过(按节点 multiMode 流转到下一处理人/节点) */
|
||||
export const approveApproval = (params: { instanceId: string; comment?: string }) => defHttp.post({ url: Api.approve, data: params });
|
||||
|
||||
/** 驳回(需填写理由) */
|
||||
export const rejectApproval = (params: { instanceId: string; reason: string }) => defHttp.post({ url: Api.reject, data: params });
|
||||
189
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
Normal file
189
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<!--
|
||||
钉钉式审批流 可视化设计器(全屏)
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave" :wrapClassName="flowApiRecording ? 'flow-design-recording-hide' : ''">
|
||||
<div class="fd-design">
|
||||
<div class="fd-toolbar">
|
||||
<span class="fd-tb-item">绑定单据:<b>{{ record.bizTableName || record.bizTable }}</b></span>
|
||||
<span class="fd-tb-item">审批流:<b>{{ record.flowName }}</b></span>
|
||||
<span class="fd-tb-tip">点击节点可配置,点击节点间的「+」可插入审批人 / 抄送人 / 条件分支</span>
|
||||
</div>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
<div class="fd-body">
|
||||
<div class="fd-palette" v-if="!readonly && paletteStages.length">
|
||||
<div class="fd-palette-title">当前页识别到的审批阶段</div>
|
||||
<div class="fd-palette-tip">点击下方阶段,按顺序追加到流程末尾;处理人将取自单据对应字段。</div>
|
||||
<div class="fd-palette-list">
|
||||
<div v-for="s in paletteStages" :key="s.stageKey" class="fd-palette-item" :class="'fd-palette-' + s.nodeType" @click="appendStageNode(s)">
|
||||
<div class="fd-palette-item-name">{{ s.stageName }}</div>
|
||||
<div class="fd-palette-item-field">{{ s.fieldComment || s.field }}</div>
|
||||
<Icon icon="ant-design:plus-circle-outlined" class="fd-palette-item-add" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-canvas">
|
||||
<div class="fd-flow" v-if="root">
|
||||
<FlowNode :node="root" />
|
||||
<div class="fd-end">
|
||||
<div class="fd-end-dot"></div>
|
||||
<span>流程结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
</div>
|
||||
<NodeConfigDrawer ref="drawerRef" :readonly="readonly" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, reactive, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import FlowNode from './FlowNode.vue';
|
||||
import NodeConfigDrawer from './NodeConfigDrawer.vue';
|
||||
import {
|
||||
createStartNode,
|
||||
createApproverNode,
|
||||
createCcNode,
|
||||
createConditionNode,
|
||||
createStageNode,
|
||||
insertAfter,
|
||||
removeNode,
|
||||
addBranch,
|
||||
} from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType, StageField } from './flowTypes';
|
||||
import { saveApprovalFlowDesign, getApprovalFlowById } from '../approvalFlow.api';
|
||||
import { flowApiRecording } from '/@/utils/flowApiRecorder';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowDesign' });
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const root = ref<FlowNodeType | null>(null);
|
||||
const readonly = ref(false);
|
||||
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
|
||||
const drawerRef = ref();
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
const paletteStages = ref<StageField[]>([]);
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
|
||||
const modalTitle = computed(() => (readonly.value ? '查看审批流' : '设计审批流'));
|
||||
|
||||
// 设计器上下文,提供给递归 FlowNode 使用
|
||||
const flowCtx = reactive({
|
||||
readonly: false,
|
||||
onSelect: (node: FlowNodeType) => {
|
||||
drawerRef.value?.openDrawer(node);
|
||||
},
|
||||
onInsert: (prevId: string, type: NodeType) => {
|
||||
if (!root.value) return;
|
||||
const factory: Record<string, () => FlowNodeType> = {
|
||||
approver: createApproverNode,
|
||||
cc: createCcNode,
|
||||
condition: createConditionNode,
|
||||
};
|
||||
const node = factory[type]?.();
|
||||
if (node) insertAfter(root.value, prevId, node);
|
||||
},
|
||||
onDelete: (id: string) => {
|
||||
if (root.value) removeNode(root.value, id);
|
||||
},
|
||||
addBranch: (conditionId: string) => {
|
||||
if (root.value) addBranch(root.value, conditionId);
|
||||
},
|
||||
});
|
||||
|
||||
// 通过 provide 注入,供递归 FlowNode 调用(避免逐层透传)
|
||||
provide('flowCtx', flowCtx);
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
readonly.value = !!data?.readonly;
|
||||
flowCtx.readonly = readonly.value;
|
||||
// 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审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
const id = data?.record?.id || '';
|
||||
Object.assign(record, {
|
||||
id,
|
||||
flowName: data?.record?.flowName || '',
|
||||
bizTable: data?.record?.bizTable || '',
|
||||
bizTableName: data?.record?.bizTableName || '',
|
||||
});
|
||||
// 列表接口未返回大字段 flow_config,需按 id 重新查询完整记录
|
||||
let cfg = data?.record?.flowConfig;
|
||||
if (id) {
|
||||
try {
|
||||
setModalProps({ loading: true });
|
||||
const full = await getApprovalFlowById({ id });
|
||||
if (full) {
|
||||
cfg = full.flowConfig;
|
||||
Object.assign(record, {
|
||||
flowName: full.flowName || record.flowName,
|
||||
bizTable: full.bizTable || record.bizTable,
|
||||
bizTableName: full.bizTableName || record.bizTableName,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setModalProps({ loading: false });
|
||||
}
|
||||
}
|
||||
// 解析已有流程设计,无则初始化一个发起人节点
|
||||
root.value = parseConfig(cfg);
|
||||
});
|
||||
|
||||
function parseConfig(cfg?: string): FlowNodeType {
|
||||
if (cfg) {
|
||||
try {
|
||||
const obj = JSON.parse(cfg);
|
||||
if (obj && obj.type) return obj;
|
||||
} catch (e) {
|
||||
console.warn('[审批流设计] flowConfig 解析失败,重置为默认', e);
|
||||
}
|
||||
}
|
||||
return createStartNode();
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】点选候选阶段,追加为流程末尾节点----- -->
|
||||
/** 将候选阶段追加到主流程链末尾(沿 childNode 找到尾节点后插入) */
|
||||
function appendStageNode(stage: StageField) {
|
||||
if (!root.value || readonly.value) return;
|
||||
let tail: FlowNodeType = root.value;
|
||||
while (tail.childNode) {
|
||||
tail = tail.childNode;
|
||||
}
|
||||
const node = createStageNode(stage);
|
||||
insertAfter(root.value, tail.id, node);
|
||||
createMessage.success(`已添加「${stage.stageName}」节点`);
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】点选候选阶段,追加为流程末尾节点----- -->
|
||||
|
||||
async function handleSave() {
|
||||
if (!record.id) {
|
||||
createMessage.error('缺少审批流ID,无法保存');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveApprovalFlowDesign({
|
||||
id: record.id,
|
||||
flowConfig: JSON.stringify(root.value),
|
||||
status: '1',
|
||||
});
|
||||
createMessage.success('流程设计已保存并发布');
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './flow.less';
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!--
|
||||
钉钉式审批流 递归节点组件
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<div class="fd-node">
|
||||
<!-- 条件分支节点:渲染多列分支 -->
|
||||
<div v-if="node.type === 'condition'" class="fd-branches">
|
||||
<div class="fd-add-branch-wrap">
|
||||
<a-button size="small" type="primary" ghost class="fd-add-branch" @click="ctx.addBranch(node.id)">+ 条件</a-button>
|
||||
</div>
|
||||
<div class="fd-branch-cols">
|
||||
<div class="fd-branch-col" v-for="(b, idx) in node.conditionNodes" :key="b.id">
|
||||
<!-- 首末列外侧连线遮罩,形成分支汇聚效果 -->
|
||||
<div v-if="idx === 0" class="fd-cover fd-cover-tl"></div>
|
||||
<div v-if="idx === 0" class="fd-cover fd-cover-bl"></div>
|
||||
<div v-if="idx === lastBranchIndex" class="fd-cover fd-cover-tr"></div>
|
||||
<div v-if="idx === lastBranchIndex" class="fd-cover fd-cover-br"></div>
|
||||
<div class="fd-branch-inner">
|
||||
<FlowNode :node="b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 普通卡片:start / approver / cc / branch -->
|
||||
<div v-else class="fd-card-wrap">
|
||||
<div class="fd-card" :class="['fd-' + node.type, { 'fd-error': isPlaceholder }]" @click="ctx.onSelect(node)">
|
||||
<div class="fd-card-header">
|
||||
<span class="fd-title">{{ node.name }}</span>
|
||||
<span v-if="node.type === 'branch' && !node.props.isDefault" class="fd-priority">优先级 {{ node.props.priorityLevel }}</span>
|
||||
<Icon v-if="canDelete && !ctx.readonly" icon="ant-design:close-outlined" class="fd-del" @click.stop="ctx.onDelete(node.id)" />
|
||||
</div>
|
||||
<div class="fd-card-body" :class="{ 'fd-placeholder': isPlaceholder }">{{ summary }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加号:在当前节点之后插入新节点 -->
|
||||
<div v-if="!ctx.readonly" class="fd-add">
|
||||
<a-popover trigger="click" placement="rightTop" v-model:open="addOpen" :overlayClassName="'fd-add-pop'">
|
||||
<template #content>
|
||||
<div class="fd-add-menu">
|
||||
<div class="fd-add-item fd-add-approver" @click="add('approver')">
|
||||
<Icon icon="ant-design:audit-outlined" /> <span>审批人</span>
|
||||
</div>
|
||||
<div class="fd-add-item fd-add-cc" @click="add('cc')">
|
||||
<Icon icon="ant-design:mail-outlined" /> <span>抄送人</span>
|
||||
</div>
|
||||
<div class="fd-add-item fd-add-condition" @click="add('condition')">
|
||||
<Icon icon="ant-design:share-alt-outlined" /> <span>条件分支</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button class="fd-add-btn" title="添加节点"><Icon icon="ant-design:plus-outlined" /></button>
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<!-- 后续链(递归) -->
|
||||
<FlowNode v-if="node.childNode" :node="node.childNode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { nodeSummary } from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType } from './flowTypes';
|
||||
|
||||
defineOptions({ name: 'FlowNode' });
|
||||
|
||||
const props = defineProps<{ node: FlowNodeType }>();
|
||||
|
||||
// 注入设计器上下文
|
||||
const ctx = inject<any>('flowCtx', {
|
||||
readonly: false,
|
||||
onSelect: () => {},
|
||||
onInsert: () => {},
|
||||
onDelete: () => {},
|
||||
addBranch: () => {},
|
||||
});
|
||||
|
||||
const addOpen = ref(false);
|
||||
|
||||
const summary = computed(() => nodeSummary(props.node));
|
||||
|
||||
// 条件分支最后一列索引(用于首末列连线遮罩)
|
||||
const lastBranchIndex = computed(() => (props.node.conditionNodes?.length || 0) - 1);
|
||||
|
||||
// 摘要为"请设置..."视为未配置(红色提示)
|
||||
const isPlaceholder = computed(() => summary.value.startsWith('请'));
|
||||
|
||||
// 发起人节点不可删除
|
||||
const canDelete = computed(() => props.node.type !== 'start');
|
||||
|
||||
function add(type: NodeType) {
|
||||
ctx.onInsert(props.node.id, type);
|
||||
addOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,359 @@
|
||||
<!--
|
||||
审批流节点配置抽屉
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<a-drawer :title="title" :width="480" :open="open && !flowApiRecording" @close="onClose" :maskClosable="!readonly">
|
||||
<template v-if="form">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="节点名称">
|
||||
<a-input v-model:value="form.name" :disabled="readonly || node?.type === 'start'" placeholder="请输入节点名称" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 发起人 -->
|
||||
<template v-if="node?.type === 'start'">
|
||||
<a-form-item label="可发起人员">
|
||||
<a-radio-group v-model:value="form.props.initiatorType" :disabled="readonly">
|
||||
<a-radio value="all">所有人</a-radio>
|
||||
<a-radio value="user">指定成员</a-radio>
|
||||
<a-radio value="role">指定角色</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.initiatorType === 'user'" label="指定成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.initiatorType === '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="请选择角色" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 审批人 -->
|
||||
<template v-else-if="node?.type === 'approver'">
|
||||
<a-form-item label="审批人类型">
|
||||
<a-radio-group v-model:value="form.props.approverType" :disabled="readonly">
|
||||
<a-radio value="user">指定成员</a-radio>
|
||||
<a-radio value="role">指定角色</a-radio>
|
||||
<a-radio value="leader">主管</a-radio>
|
||||
<a-radio value="self">发起人自己</a-radio>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人----- -->
|
||||
<a-radio value="field">取单据字段</a-radio>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人----- -->
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人配置----- -->
|
||||
<template v-if="form.props.approverType === 'field'">
|
||||
<a-alert type="info" show-icon style="margin-bottom: 12px" message="发起审批时,处理人将取自单据该字段的值(通常为人员账号)。" />
|
||||
<a-form-item label="字段中文名">
|
||||
<a-input v-model:value="form.props.fieldLabel" :disabled="readonly" placeholder="如:校对人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字段名">
|
||||
<a-input v-model:value="form.props.fieldName" :disabled="readonly" placeholder="如:proofreader" />
|
||||
</a-form-item>
|
||||
</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" />
|
||||
</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="请选择角色" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.approverType === 'leader'" label="主管层级">
|
||||
<a-select v-model:value="form.props.leaderLevel" :disabled="readonly" style="width: 160px">
|
||||
<a-select-option :value="1">直接主管</a-select-option>
|
||||
<a-select-option :value="2">第2级主管</a-select-option>
|
||||
<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-radio value="or">或签(一人同意)</a-radio>
|
||||
<a-radio value="sequence">依次审批</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批人为空时">
|
||||
<a-radio-group v-model:value="form.props.emptyStrategy" :disabled="readonly">
|
||||
<a-radio value="admin">转交管理员</a-radio>
|
||||
<a-radio value="pass">自动通过</a-radio>
|
||||
<a-radio value="stop">终止流程</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(录制业务按钮接口)----- -->
|
||||
<a-divider style="margin: 16px 0 12px">回调接口(审批联动业务)</a-divider>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
message="审批到对应时机时,系统会以「当前处理人」身份调用所选业务接口(自动带上单据ID)。点击「录制」后,设计器会临时隐藏,请到页面上点击目标按钮(如“批准”),系统自动识别其接口并回填。"
|
||||
/>
|
||||
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
|
||||
<div class="fd-cb-title">{{ phase.label }}</div>
|
||||
<div v-for="(a, i) in form.props.callbackActions[phase.key]" :key="i" class="fd-cb-row">
|
||||
<a-input v-model:value="a.name" placeholder="动作名" :disabled="readonly" style="width: 88px" />
|
||||
<a-select v-model:value="a.method" :disabled="readonly" style="width: 82px" :options="methodOptions" />
|
||||
<a-input v-model:value="a.url" placeholder="接口路径 /xxx" :disabled="readonly" style="flex: 1; min-width: 120px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
||||
</div>
|
||||
<a-space v-if="!readonly" style="margin-top: 4px">
|
||||
<a-button size="small" @click="recordInto(phase.key)">
|
||||
<Icon icon="ant-design:aim-outlined" />
|
||||
<span>录制接口</span>
|
||||
</a-button>
|
||||
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(录制业务按钮接口)----- -->
|
||||
</template>
|
||||
|
||||
<!-- 抄送人 -->
|
||||
<template v-else-if="node?.type === 'cc'">
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送人来源支持取单据字段----- -->
|
||||
<a-form-item label="抄送人来源">
|
||||
<a-radio-group v-model:value="form.props.ccType" :disabled="readonly">
|
||||
<a-radio value="user">指定成员/角色</a-radio>
|
||||
<a-radio value="field">取单据字段</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="form.props.ccType === 'field'">
|
||||
<a-alert type="info" show-icon style="margin-bottom: 12px" message="发起审批时,抄送人将取自单据该字段的值(通常为人员账号)。" />
|
||||
<a-form-item label="字段中文名">
|
||||
<a-input v-model:value="form.props.fieldLabel" :disabled="readonly" placeholder="如:分发人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字段名">
|
||||
<a-input v-model:value="form.props.fieldName" :disabled="readonly" placeholder="如:distributor" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="抄送成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item label="抄送角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="form.props.allowEditCc" :disabled="readonly">允许审批人自行添加抄送人</a-checkbox>
|
||||
</a-form-item>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送人来源支持取单据字段----- -->
|
||||
</template>
|
||||
|
||||
<!-- 条件分支 -->
|
||||
<template v-else-if="node?.type === 'branch'">
|
||||
<template v-if="form.props.isDefault">
|
||||
<a-alert type="info" show-icon message="“其它情况”分支:当以上条件均不满足时进入此分支,无需配置条件。" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="条件关系">
|
||||
<a-radio-group v-model:value="form.props.logic" :disabled="readonly">
|
||||
<a-radio value="and">且(同时满足)</a-radio>
|
||||
<a-radio value="or">或(满足其一)</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="条件设置">
|
||||
<div v-for="(c, i) in form.props.conditions" :key="i" class="fd-cond-row">
|
||||
<a-input v-model:value="c.label" placeholder="字段中文名" :disabled="readonly" style="width: 110px" />
|
||||
<a-input v-model:value="c.field" placeholder="字段名" :disabled="readonly" style="width: 110px" />
|
||||
<a-select v-model:value="c.operator" :disabled="readonly" style="width: 100px" :options="operatorOptions" />
|
||||
<a-input v-if="!['empty', 'notEmpty'].includes(c.operator)" v-model:value="c.value" placeholder="值" :disabled="readonly" style="width: 90px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cond-del" @click="removeCond(i)" />
|
||||
</div>
|
||||
<a-button v-if="!readonly" type="dashed" block @click="addCond" style="margin-top: 8px">+ 添加条件</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<template #footer v-if="!readonly">
|
||||
<a-space>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制中悬浮提示条----- -->
|
||||
<Teleport to="body">
|
||||
<div v-if="flowApiRecording" class="flow-record-banner">
|
||||
<Icon icon="ant-design:aim-outlined" class="flow-record-banner-icon" />
|
||||
<span class="flow-record-banner-text">录制中:请点击页面上要绑定的业务按钮(如“批准/反审核”),系统会自动识别其接口</span>
|
||||
<a-button size="small" danger @click="cancelRecord">取消录制</a-button>
|
||||
</div>
|
||||
</Teleport>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制中悬浮提示条----- -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { ApiSelect } from '/@/components/Form';
|
||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import { OPERATOR_OPTIONS } from './flowTypes';
|
||||
import type { FlowNode } from './flowTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { flowApiRecording, startFlowApiRecord, cancelFlowApiRecord } from '/@/utils/flowApiRecorder';
|
||||
|
||||
const props = defineProps<{ readonly?: boolean }>();
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const open = ref(false);
|
||||
const node = ref<FlowNode | null>(null);
|
||||
const form = ref<any>(null);
|
||||
|
||||
const operatorOptions = OPERATOR_OPTIONS;
|
||||
const readonly = computed(() => !!props.readonly);
|
||||
|
||||
// 回调接口配置:三个触发时机
|
||||
const callbackPhases = [
|
||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||
{ key: 'onReject', label: '驳回时执行' },
|
||||
];
|
||||
const methodOptions = [
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
];
|
||||
|
||||
const title = computed(() => {
|
||||
const map: Record<string, string> = { start: '发起人设置', approver: '审批人设置', cc: '抄送人设置', branch: '条件设置' };
|
||||
return map[node.value?.type || ''] || '节点设置';
|
||||
});
|
||||
|
||||
const roleApi = (params) => defHttp.get({ url: '/sys/role/list', params });
|
||||
|
||||
function openDrawer(n: FlowNode) {
|
||||
node.value = n;
|
||||
// 编辑副本,确定时回写,避免取消后脏数据
|
||||
form.value = { name: n.name, props: cloneDeep(n.props) };
|
||||
// 审批人节点确保回调接口配置结构存在
|
||||
if (n.type === 'approver') {
|
||||
const cb = form.value.props.callbackActions || {};
|
||||
form.value.props.callbackActions = {
|
||||
onNodeApprove: Array.isArray(cb.onNodeApprove) ? cb.onNodeApprove : [],
|
||||
onApprove: Array.isArray(cb.onApprove) ? cb.onApprove : [],
|
||||
onReject: Array.isArray(cb.onReject) ? cb.onReject : [],
|
||||
};
|
||||
}
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function addAction(phaseKey: string) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: 'POST', url: '' });
|
||||
}
|
||||
|
||||
function removeAction(phaseKey: string, i: number) {
|
||||
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
||||
}
|
||||
|
||||
/** 录制:临时隐藏设计器,捕获用户点击业务按钮的请求并回填 */
|
||||
async function recordInto(phaseKey: string) {
|
||||
const cap = await startFlowApiRecord();
|
||||
if (cap) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: cap.method, url: cap.url, body: cap.data });
|
||||
createMessage.success(`已录制接口:${cap.method} ${cap.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRecord() {
|
||||
cancelFlowApiRecord();
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
if (node.value && form.value) {
|
||||
node.value.name = form.value.name;
|
||||
node.value.props = cloneDeep(form.value.props);
|
||||
emit('confirm', node.value);
|
||||
}
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function addCond() {
|
||||
form.value.props.conditions.push({ label: '', field: '', operator: 'eq', value: '' });
|
||||
}
|
||||
|
||||
function removeCond(i: number) {
|
||||
form.value.props.conditions.splice(i, 1);
|
||||
}
|
||||
|
||||
defineExpose({ openDrawer });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.fd-cond-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cond-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 回调接口配置 */
|
||||
.fd-cb-block {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.fd-cb-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cb-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fd-cb-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 录制提示条 Teleport 到 body,需非 scoped 全局样式 -->
|
||||
<style lang="less">
|
||||
.flow-record-banner {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 92vw;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
color: #614700;
|
||||
font-size: 13px;
|
||||
|
||||
.flow-record-banner-icon {
|
||||
color: #fa8c16;
|
||||
font-size: 18px;
|
||||
}
|
||||
.flow-record-banner-text {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
428
jeecgboot-vue3/src/views/approval/flow/components/flow.less
Normal file
428
jeecgboot-vue3/src/views/approval/flow/components/flow.less
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 钉钉式审批流设计器 样式
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@line-color: #cacaca;
|
||||
|
||||
/* 录制回调接口时,隐藏整个设计器弹窗(含遮罩),让用户能点击业务页面上的真实按钮 */
|
||||
.flow-design-recording-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fd-design {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fd-toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.fd-tb-item {
|
||||
margin-right: 24px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.fd-tb-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】候选阶段侧边栏布局----- */
|
||||
.fd-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fd-palette {
|
||||
flex-shrink: 0;
|
||||
width: 220px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
padding: 14px 12px;
|
||||
|
||||
.fd-palette-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fd-palette-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fd-palette-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fd-palette-item {
|
||||
position: relative;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
padding: 8px 30px 8px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border-left: 3px solid #ff943e;
|
||||
|
||||
&.fd-palette-cc {
|
||||
border-left-color: #3296fa;
|
||||
}
|
||||
|
||||
.fd-palette-item-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fd-palette-item-field {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fd-palette-item-add {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #3296fa;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #3296fa;
|
||||
background: #f0f7ff;
|
||||
box-shadow: 0 2px 8px rgba(50, 150, 250, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】候选阶段侧边栏布局----- */
|
||||
|
||||
.fd-canvas {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: #f0f2f5;
|
||||
padding: 40px 20px 80px;
|
||||
}
|
||||
|
||||
.fd-flow {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- 节点列 ---------- */
|
||||
.fd-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-card-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-card {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.fd-del {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.fd-error {
|
||||
box-shadow: 0 0 0 1px #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
.fd-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fd-priority {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.fd-del {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fd-card-body {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
min-height: 22px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
|
||||
&.fd-placeholder {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
/* 各类型节点头部配色 */
|
||||
.fd-start .fd-card-header {
|
||||
background: #576a95;
|
||||
}
|
||||
.fd-approver .fd-card-header {
|
||||
background: #ff943e;
|
||||
}
|
||||
.fd-cc .fd-card-header {
|
||||
background: #3296fa;
|
||||
}
|
||||
.fd-branch {
|
||||
.fd-card {
|
||||
width: 220px;
|
||||
}
|
||||
.fd-card-header {
|
||||
background: #fff;
|
||||
color: #15bca3;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.fd-priority {
|
||||
color: #999;
|
||||
}
|
||||
.fd-del {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 节点之间的连线 + 加号 ---------- */
|
||||
.fd-add {
|
||||
position: relative;
|
||||
width: 2px;
|
||||
min-height: 70px;
|
||||
background: @line-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fd-add-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #3296fa;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(50, 150, 250, 0.35);
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加号弹出菜单 */
|
||||
.fd-add-menu {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
.fd-add-item {
|
||||
width: 76px;
|
||||
height: 70px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
transition: all 0.15s;
|
||||
|
||||
.anticon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #3296fa;
|
||||
color: #3296fa;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
&.fd-add-approver .anticon {
|
||||
color: #ff943e;
|
||||
}
|
||||
&.fd-add-cc .anticon {
|
||||
color: #3296fa;
|
||||
}
|
||||
&.fd-add-condition .anticon {
|
||||
color: #15bca3;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 条件分支 ---------- */
|
||||
.fd-branches {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* +条件 按钮(带进入竖线) */
|
||||
.fd-add-branch-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: @line-color;
|
||||
}
|
||||
|
||||
.fd-add-branch {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-branch-cols {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-branch-col {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #f0f2f5;
|
||||
border-top: 2px solid @line-color;
|
||||
border-bottom: 2px solid @line-color;
|
||||
padding: 0 24px;
|
||||
|
||||
/* 列内顶部进入竖线 + 底部汇出竖线 */
|
||||
.fd-branch-inner {
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: @line-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 首末列外侧白块,遮挡多余横线,形成包裹效果 */
|
||||
.fd-cover {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 4px;
|
||||
background: #f0f2f5;
|
||||
z-index: 2;
|
||||
}
|
||||
.fd-cover-tl {
|
||||
top: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
.fd-cover-bl {
|
||||
bottom: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
.fd-cover-tr {
|
||||
top: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
.fd-cover-br {
|
||||
bottom: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
/* ---------- 结束节点 ---------- */
|
||||
.fd-end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
|
||||
.fd-end-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #dedede;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
267
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
Normal file
267
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 钉钉式审批流 节点数据模型 + 工厂函数
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
|
||||
// 节点类型:start发起人 approver审批人 cc抄送人 condition条件路由 branch条件分支
|
||||
export type NodeType = 'start' | 'approver' | 'cc' | 'condition' | 'branch';
|
||||
|
||||
export interface FlowNode {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
name: string;
|
||||
// 节点配置(按 type 不同含义不同)
|
||||
props: Record<string, any>;
|
||||
// 链式下一个节点
|
||||
childNode?: FlowNode | null;
|
||||
// 仅 condition 节点:分支数组(每个元素 type=branch)
|
||||
conditionNodes?: FlowNode[];
|
||||
}
|
||||
|
||||
let _seed = 0;
|
||||
|
||||
/** 生成唯一节点 id */
|
||||
export function nid(prefix = 'node'): string {
|
||||
_seed += 1;
|
||||
return `${prefix}_${Date.now().toString(36)}_${_seed}`;
|
||||
}
|
||||
|
||||
/** 发起人节点(根节点,固定存在) */
|
||||
export function createStartNode(): FlowNode {
|
||||
return {
|
||||
id: nid('start'),
|
||||
type: 'start',
|
||||
name: '发起人',
|
||||
props: {
|
||||
// initiatorType: all全员 / user指定成员 / role指定角色
|
||||
initiatorType: 'all',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 审批人节点 */
|
||||
export function createApproverNode(): FlowNode {
|
||||
return {
|
||||
id: nid('approver'),
|
||||
type: 'approver',
|
||||
name: '审批人',
|
||||
props: {
|
||||
// approverType: user指定成员 / role指定角色 / leader主管 / self发起人自己
|
||||
approverType: 'user',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
leaderLevel: 1,
|
||||
// 多人审批方式 and会签(需全部同意) / or或签(一人同意) / sequence依次审批
|
||||
multiMode: 'and',
|
||||
// 审批人为空时 pass自动通过 / admin转交管理员 / stop终止
|
||||
emptyStrategy: 'admin',
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 抄送人节点 */
|
||||
export function createCcNode(): FlowNode {
|
||||
return {
|
||||
id: nid('cc'),
|
||||
type: 'cc',
|
||||
name: '抄送人',
|
||||
props: {
|
||||
// ccType: user指定成员/角色 / field取单据字段中的人员
|
||||
ccType: 'user',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
allowEditCc: false,
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
/** 解析出的页面阶段字段 */
|
||||
export interface StageField {
|
||||
stageKey: string;
|
||||
stageName: string;
|
||||
nodeType: 'approver' | 'cc';
|
||||
field: string;
|
||||
fieldComment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由"当前页字段"生成阶段节点:
|
||||
* 审批阶段(校对/审核/审批/分发) -> 审批人节点,处理人=取单据该字段中的人员;
|
||||
* 抄送阶段 -> 抄送节点,抄送人=取单据该字段中的人员。
|
||||
*/
|
||||
export function createStageNode(stage: StageField): FlowNode {
|
||||
const fieldLabel = stage.fieldComment || stage.field;
|
||||
if (stage.nodeType === 'cc') {
|
||||
const node = createCcNode();
|
||||
node.name = stage.stageName;
|
||||
node.props.ccType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
return node;
|
||||
}
|
||||
const node = createApproverNode();
|
||||
node.name = stage.stageName;
|
||||
node.props.approverType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
return node;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
|
||||
/** 条件分支节点(默认两条分支:条件 + 其它情况) */
|
||||
export function createConditionNode(): FlowNode {
|
||||
return {
|
||||
id: nid('condition'),
|
||||
type: 'condition',
|
||||
name: '条件分支',
|
||||
props: {},
|
||||
conditionNodes: [createBranchNode(1), createBranchNode(2, true)],
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 单个条件分支 */
|
||||
export function createBranchNode(priority: number, isDefault = false): FlowNode {
|
||||
return {
|
||||
id: nid('branch'),
|
||||
type: 'branch',
|
||||
name: isDefault ? '其它情况' : `条件${priority}`,
|
||||
props: {
|
||||
priorityLevel: priority,
|
||||
isDefault,
|
||||
// 条件列表:{ field 字段名, label 字段中文, operator 运算符, value 值 }
|
||||
conditions: [] as any[],
|
||||
// 多条件关系 and / or
|
||||
logic: 'and',
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 运算符选项 */
|
||||
export const OPERATOR_OPTIONS = [
|
||||
{ label: '等于', value: 'eq' },
|
||||
{ label: '不等于', value: 'ne' },
|
||||
{ label: '大于', value: 'gt' },
|
||||
{ label: '大于等于', value: 'gte' },
|
||||
{ label: '小于', value: 'lt' },
|
||||
{ label: '小于等于', value: 'lte' },
|
||||
{ label: '包含', value: 'contains' },
|
||||
{ label: '为空', value: 'empty' },
|
||||
{ label: '不为空', value: 'notEmpty' },
|
||||
];
|
||||
|
||||
/** 节点卡片内容摘要文本 */
|
||||
export function nodeSummary(node: FlowNode): string {
|
||||
if (node.type === 'start') {
|
||||
const t = node.props.initiatorType;
|
||||
if (t === 'all') return '所有人可发起';
|
||||
if (t === 'user') return node.props.userText ? `指定成员:${node.props.userText}` : '请设置发起人';
|
||||
if (t === 'role') return node.props.roleList?.length ? `指定角色(${node.props.roleList.length})` : '请设置发起角色';
|
||||
return '请设置发起人';
|
||||
}
|
||||
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})` : '请设置审批角色';
|
||||
// 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.type === 'cc') {
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段抄送人摘要-----
|
||||
if (node.props.ccType === '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.type === 'branch') {
|
||||
if (node.props.isDefault) return '未满足其它条件时进入此分支';
|
||||
const list = node.props.conditions || [];
|
||||
if (!list.length) return '请设置条件';
|
||||
return list.map((c: any) => `${c.label || c.field} ${operatorText(c.operator)} ${c.value ?? ''}`).join(node.props.logic === 'or' ? ' 或 ' : ' 且 ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function operatorText(op: string): string {
|
||||
return OPERATOR_OPTIONS.find((o) => o.value === op)?.label || op;
|
||||
}
|
||||
|
||||
/** 深度遍历每个节点(含分支) */
|
||||
export function eachNode(node: FlowNode | null | undefined, cb: (n: FlowNode) => void) {
|
||||
if (!node) return;
|
||||
cb(node);
|
||||
if (node.childNode) eachNode(node.childNode, cb);
|
||||
if (node.conditionNodes) node.conditionNodes.forEach((b) => eachNode(b, cb));
|
||||
}
|
||||
|
||||
/** 在 prevId 节点之后插入新节点 */
|
||||
export function insertAfter(root: FlowNode, prevId: string, newNode: FlowNode) {
|
||||
eachNode(root, (n) => {
|
||||
if (n.id === prevId && newNode.id !== prevId) {
|
||||
newNode.childNode = n.childNode || null;
|
||||
n.childNode = newNode;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除节点(普通链节点 / 条件分支,发起人不可删) */
|
||||
export function removeNode(root: FlowNode, id: string): boolean {
|
||||
// 1) 链式节点:找到以其为 childNode 的父节点
|
||||
let parent: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.childNode && n.childNode.id === id) parent = n;
|
||||
});
|
||||
if (parent) {
|
||||
(parent as FlowNode).childNode = (parent as FlowNode).childNode?.childNode || null;
|
||||
return true;
|
||||
}
|
||||
// 2) 条件分支:找到包含该分支的 condition
|
||||
let cond: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.conditionNodes && n.conditionNodes.some((b) => b.id === id)) cond = n;
|
||||
});
|
||||
if (cond) {
|
||||
const c = cond as FlowNode;
|
||||
const arr = c.conditionNodes as FlowNode[];
|
||||
if (arr.length <= 2) {
|
||||
// 只剩两条分支时删除一条 => 整个条件节点收起,保留其后续链
|
||||
let condParent: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.childNode && n.childNode.id === c.id) condParent = n;
|
||||
});
|
||||
if (condParent) (condParent as FlowNode).childNode = c.childNode || null;
|
||||
} else {
|
||||
const i = arr.findIndex((b) => b.id === id);
|
||||
if (i >= 0) arr.splice(i, 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 给条件节点新增一条分支 */
|
||||
export function addBranch(root: FlowNode, conditionId: string) {
|
||||
eachNode(root, (n) => {
|
||||
if (n.id === conditionId && n.conditionNodes) {
|
||||
const next = n.conditionNodes.length + 1;
|
||||
// 新分支插入到"其它情况"默认分支之前
|
||||
const defaultIdx = n.conditionNodes.findIndex((b) => b.props.isDefault);
|
||||
const branch = createBranchNode(next);
|
||||
if (defaultIdx >= 0) {
|
||||
n.conditionNodes.splice(defaultIdx, 0, branch);
|
||||
} else {
|
||||
n.conditionNodes.push(branch);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
34
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
Normal file
34
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 发起审批 API(全局悬浮按钮使用)
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
enum Api {
|
||||
publishedList = '/xslmes/approvalLaunch/publishedList',
|
||||
bizRecords = '/xslmes/approvalLaunch/bizRecords',
|
||||
launch = '/xslmes/approvalLaunch/launch',
|
||||
launchBatch = '/xslmes/approvalLaunch/launchBatch',
|
||||
}
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(可发起的单据类型)
|
||||
*/
|
||||
export const getPublishedFlows = () => defHttp.get({ url: Api.publishedList });
|
||||
|
||||
/**
|
||||
* 根据审批流查询其绑定单据的记录列表
|
||||
*/
|
||||
export const getBizRecords = (params: { flowId: string; keyword?: string }) => defHttp.get({ url: Api.bizRecords, params });
|
||||
|
||||
/**
|
||||
* 发起审批(单条)
|
||||
*/
|
||||
export const launchApproval = (params: { flowId: string; bizDataId: string; bizTitle?: string }) => defHttp.post({ url: Api.launch, params });
|
||||
|
||||
/**
|
||||
* 批量发起审批(列表多选)
|
||||
*/
|
||||
export const launchApprovalBatch = (params: { flowId: string; items: { bizDataId: string; bizTitle?: string }[] }) =>
|
||||
defHttp.post({ url: Api.launchBatch, params });
|
||||
154
jeecgboot-vue3/src/views/system/im/ImApprovalDetailModal.vue
Normal file
154
jeecgboot-vue3/src/views/system/im/ImApprovalDetailModal.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<!--
|
||||
IM 审批卡片「查看详情」弹窗:展示单据全部字段 + 审批进度/历史
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】审批办理-查看详情
|
||||
-->
|
||||
<template>
|
||||
<a-modal v-model:open="open" title="审批单据详情" :width="640" :footer="null" :destroyOnClose="true">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="im-appr-detail">
|
||||
<div class="im-appr-detail-head">
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="审批流">{{ info.flowName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusColor">{{ info.statusText }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发起人">{{ info.applyUserName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前节点">{{ info.currentNodeName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前处理人" :span="2">{{ info.currentHandlersText || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="im-appr-detail-section-title">单据数据</div>
|
||||
<table class="im-appr-detail-table">
|
||||
<tbody>
|
||||
<tr v-for="f in fields" :key="f.label">
|
||||
<th>{{ f.label }}</th>
|
||||
<td>{{ f.value }}</td>
|
||||
</tr>
|
||||
<tr v-if="!fields.length">
|
||||
<td class="im-appr-detail-empty">暂无单据数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<template v-if="history.length">
|
||||
<div class="im-appr-detail-section-title">审批历史</div>
|
||||
<a-timeline class="im-appr-detail-timeline">
|
||||
<a-timeline-item v-for="(h, i) in history" :key="i" :color="h.actionText === '驳回' ? 'red' : 'green'">
|
||||
<div class="im-appr-his-line">
|
||||
<b>{{ h.nodeName }}</b>
|
||||
<span>{{ h.name }} {{ h.actionText }}</span>
|
||||
<span class="im-appr-his-time">{{ h.time }}</span>
|
||||
</div>
|
||||
<div v-if="h.comment" class="im-appr-his-comment">意见:{{ h.comment }}</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</template>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getApprovalDetail } from '/@/views/approval/flow/approvalHandle.api';
|
||||
|
||||
defineOptions({ name: 'ImApprovalDetailModal' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const info = ref<any>({});
|
||||
const fields = ref<{ label: string; value: string }[]>([]);
|
||||
const history = ref<any[]>([]);
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const s = info.value?.status;
|
||||
if (s === '1') return 'green';
|
||||
if (s === '2') return 'red';
|
||||
if (s === '3') return 'default';
|
||||
return 'blue';
|
||||
});
|
||||
|
||||
async function openModal(instanceId: string) {
|
||||
open.value = true;
|
||||
info.value = {};
|
||||
fields.value = [];
|
||||
history.value = [];
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const data: any = await getApprovalDetail(instanceId);
|
||||
info.value = data || {};
|
||||
fields.value = data?.fields || [];
|
||||
history.value = data?.history || [];
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.im-appr-detail-section-title {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-left: 3px solid #1677ff;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.im-appr-detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 32%;
|
||||
background: #fafafa;
|
||||
color: #595959;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.im-appr-detail-empty {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.im-appr-his-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
.im-appr-his-time {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.im-appr-his-comment {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -1,442 +1,426 @@
|
||||
<template>
|
||||
|
||||
<div class="im-biz-record-message">
|
||||
|
||||
<div v-if="showNoPermission" class="im-biz-record-no-permission">暂无当前消息权限</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 单条:详情表 -->
|
||||
<template v-if="isSingleItem">
|
||||
<div class="im-biz-record-item">
|
||||
<div class="im-biz-record-table-wrap">
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
<tbody>
|
||||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||||
<th>{{ field.label }}</th>
|
||||
<td>{{ field.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<template v-if="isSingleItem">
|
||||
|
||||
<div class="im-biz-record-item">
|
||||
|
||||
<div class="im-biz-record-table-wrap">
|
||||
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||||
|
||||
<th>{{ field.label }}</th>
|
||||
|
||||
<td>{{ field.value }}</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<!-- 审批卡片:底部操作按钮 -->
|
||||
<div v-if="isApprovalCard" class="im-biz-record-actions">
|
||||
<a-button size="small" @click="handleDetail(singleItem)">
|
||||
<Icon icon="ant-design:file-search-outlined" />
|
||||
<span>查看详情</span>
|
||||
</a-button>
|
||||
<template v-if="liveActionable">
|
||||
<a-button size="small" type="primary" :loading="approving" @click="handleApprove(singleItem)">
|
||||
<Icon icon="ant-design:check-outlined" />
|
||||
<span>{{ singleItem.actionLabel || '审批' }}</span>
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="openReject(singleItem)">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
<span>拒绝</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
||||
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
|
||||
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
<Icon icon="ant-design:unordered-list-outlined" />
|
||||
<span>跳转至列表</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 普通分享卡片:定位链接 -->
|
||||
<a v-else class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
<span>查看并定位到此数据</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
|
||||
<span>查看并定位到此数据</span>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<!-- 多条:列表表,第一列为定位链接 -->
|
||||
<template v-else>
|
||||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||||
<table class="im-biz-record-table im-biz-record-table--list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="im-biz-record-link-col">链接</th>
|
||||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||||
<td class="im-biz-record-link-col">
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
<span>定位</span>
|
||||
</a>
|
||||
</td>
|
||||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||||
{{ getFieldValue(item, columnLabel) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||||
</template>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<ImApprovalDetailModal ref="detailModalRef" />
|
||||
|
||||
|
||||
<!-- 多条:列表表,第一列为定位链接 -->
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||||
|
||||
<table class="im-biz-record-table im-biz-record-table--list">
|
||||
|
||||
<thead>
|
||||
|
||||
<tr>
|
||||
|
||||
<th class="im-biz-record-link-col">链接</th>
|
||||
|
||||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||||
|
||||
<td class="im-biz-record-link-col">
|
||||
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||||
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
|
||||
<span>定位</span>
|
||||
|
||||
</a>
|
||||
|
||||
</td>
|
||||
|
||||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||||
|
||||
{{ getFieldValue(item, columnLabel) }}
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- 驳回理由弹窗 -->
|
||||
<a-modal v-model:open="rejectOpen" title="驳回审批" :confirmLoading="rejecting" okText="确认驳回" @ok="confirmReject">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="驳回理由" required>
|
||||
<a-textarea v-model:value="rejectReason" :rows="3" placeholder="请填写驳回理由" :maxlength="500" show-count />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
|
||||
|
||||
import {
|
||||
|
||||
getImBizRecordFieldValueByLabel,
|
||||
|
||||
resolveImBizRecordItemFields,
|
||||
|
||||
resolveImBizRecordListColumnLabels,
|
||||
|
||||
} from './imBizRecordMessage';
|
||||
|
||||
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
||||
import { navigateImBizRecordLink } from './imRecordLocate';
|
||||
|
||||
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
|
||||
|
||||
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { approveApproval, rejectApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
|
||||
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
|
||||
|
||||
defineOptions({ name: 'ImBizRecordMessageContent' });
|
||||
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
payload: ImBizRecordPayload;
|
||||
|
||||
mine?: boolean;
|
||||
|
||||
receiverHasBizPagePermission?: boolean;
|
||||
|
||||
}>();
|
||||
|
||||
// 办理成功后通知父级(ImChat)刷新当前会话,使下一节点卡片/结果通知即时出现
|
||||
const emit = defineEmits(['handled']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const detailModalRef = ref();
|
||||
const approving = ref(false);
|
||||
const rejecting = ref(false);
|
||||
const rejectOpen = ref(false);
|
||||
const rejectReason = ref('');
|
||||
const rejectItem = ref<ImBizRecordItem | null>(null);
|
||||
// 本地办理结果:approved / rejected(卡片消息为静态,办理后本地标记)
|
||||
const actionDone = ref<'' | 'approved' | 'rejected'>('');
|
||||
|
||||
// 审批实例实时状态(用于旧节点卡片置灰)
|
||||
interface LiveStatus {
|
||||
exists: boolean;
|
||||
status?: string;
|
||||
statusText?: string;
|
||||
currentNodeId?: string;
|
||||
currentHandlersText?: string;
|
||||
canApprove?: boolean;
|
||||
}
|
||||
const liveStatus = ref<LiveStatus | null>(null);
|
||||
const liveLoaded = ref(false);
|
||||
|
||||
const isSingleItem = computed(() => props.payload.items.length === 1);
|
||||
|
||||
const singleItem = computed(() => props.payload.items[0]);
|
||||
|
||||
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
||||
|
||||
// 审批卡片:单条且带审批实例ID
|
||||
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
||||
|
||||
// 是否仍可办理:本地未办理 且 实例审批中 且 卡片节点==当前节点 且 本人为当前处理人
|
||||
const liveActionable = computed(() => {
|
||||
if (!isApprovalCard.value || actionDone.value || props.mine) {
|
||||
return false;
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
if (!s || !s.exists || s.status !== '0' || !s.canApprove) {
|
||||
return false;
|
||||
}
|
||||
// 携带 nodeId 时严格比对当前节点,区分同一实例的新旧卡片
|
||||
const cardNodeId = singleItem.value?.nodeId;
|
||||
if (cardNodeId) {
|
||||
return s.currentNodeId === cardNodeId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 不可办理时的置灰提示文案
|
||||
const disabledText = computed(() => {
|
||||
if (actionDone.value) {
|
||||
return actionDone.value === 'rejected' ? '已驳回' : '已处理';
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
if (!s) {
|
||||
return liveLoaded.value ? '加载失败' : '';
|
||||
}
|
||||
if (!s.exists) {
|
||||
return '审批已失效';
|
||||
}
|
||||
if (s.status === '1') return '已通过';
|
||||
if (s.status === '2') return '已驳回';
|
||||
if (s.status === '3') return '已撤销';
|
||||
// 审批中但本卡片不可办理
|
||||
const cardNodeId = singleItem.value?.nodeId;
|
||||
if (cardNodeId && s.currentNodeId !== cardNodeId) {
|
||||
return '已流转,无需处理';
|
||||
}
|
||||
return '等待他人处理';
|
||||
});
|
||||
|
||||
async function loadLiveStatus() {
|
||||
const id = singleItem.value?.instanceId;
|
||||
if (!isApprovalCard.value || props.mine || !id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res: any = await getApprovalStatus(id);
|
||||
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
||||
} catch {
|
||||
liveStatus.value = null;
|
||||
} finally {
|
||||
liveLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadLiveStatus);
|
||||
watch(() => singleItem.value?.instanceId, loadLiveStatus);
|
||||
|
||||
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
|
||||
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
|
||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
||||
const canLocate = computed(() => props.mine || hasPagePermission.value);
|
||||
|
||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value);
|
||||
|
||||
const showPeerNoPermissionTip = computed(
|
||||
() => !!props.mine && props.receiverHasBizPagePermission === false,
|
||||
);
|
||||
const showPeerNoPermissionTip = computed(() => !!props.mine && props.receiverHasBizPagePermission === false);
|
||||
|
||||
function resolveItemFields(item: ImBizRecordItem) {
|
||||
|
||||
return resolveImBizRecordItemFields(item);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getFieldValue(item: ImBizRecordItem, label: string) {
|
||||
|
||||
return getImBizRecordFieldValueByLabel(item, label);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleLinkClick(linkPath: string) {
|
||||
|
||||
if (!linkPath || showNoPermission.value) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
await navigateImBizRecordLink(linkPath);
|
||||
|
||||
}
|
||||
|
||||
function handleDetail(item: ImBizRecordItem) {
|
||||
if (!item.instanceId) return;
|
||||
detailModalRef.value?.openModal(item.instanceId);
|
||||
}
|
||||
|
||||
async function handleApprove(item: ImBizRecordItem) {
|
||||
if (!item.instanceId || approving.value) return;
|
||||
try {
|
||||
approving.value = true;
|
||||
const res: any = await approveApproval({ instanceId: item.instanceId });
|
||||
createMessage.success(typeof res === 'string' ? res : '已审批');
|
||||
actionDone.value = 'approved';
|
||||
// 立即刷新本卡片状态(置灰),再通知父级刷新会话
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
approving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(item: ImBizRecordItem) {
|
||||
rejectItem.value = item;
|
||||
rejectReason.value = '';
|
||||
rejectOpen.value = true;
|
||||
}
|
||||
|
||||
async function confirmReject() {
|
||||
const item = rejectItem.value;
|
||||
if (!item?.instanceId) return;
|
||||
if (!rejectReason.value.trim()) {
|
||||
createMessage.warning('请填写驳回理由');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
rejecting.value = true;
|
||||
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
||||
createMessage.success('已驳回');
|
||||
actionDone.value = 'rejected';
|
||||
rejectOpen.value = false;
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
rejecting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.im-biz-record-message {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
min-width: 280px;
|
||||
|
||||
max-width: 420px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-no-permission {
|
||||
|
||||
padding: 12px 10px;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
color: #8c8c8c;
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-peer-tip {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
align-self: flex-start;
|
||||
|
||||
margin-top: 4px;
|
||||
|
||||
padding: 2px 8px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
background: #fff7e6;
|
||||
|
||||
border: 1px solid #ffd591;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
color: #d46b08;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-item {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
/* 审批办理按钮栏 */
|
||||
.im-biz-record-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
|
||||
.im-biz-record-done {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 不可办理置灰提示 */
|
||||
.im-biz-record-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.im-biz-record-table-wrap {
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
background: #fff;
|
||||
|
||||
|
||||
|
||||
&--list {
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-table {
|
||||
|
||||
width: 100%;
|
||||
|
||||
border-collapse: collapse;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
|
||||
|
||||
th,
|
||||
|
||||
td {
|
||||
|
||||
padding: 8px 10px;
|
||||
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
word-break: break-word;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tr:last-child {
|
||||
|
||||
th,
|
||||
|
||||
td {
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--detail {
|
||||
|
||||
table-layout: fixed;
|
||||
|
||||
|
||||
|
||||
th {
|
||||
|
||||
width: 38%;
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
color: #595959;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
td {
|
||||
|
||||
color: #262626;
|
||||
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--list {
|
||||
|
||||
min-width: 100%;
|
||||
|
||||
table-layout: auto;
|
||||
|
||||
|
||||
|
||||
thead th {
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
color: #595959;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
text-align: left;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tbody td {
|
||||
|
||||
color: #262626;
|
||||
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-link-col {
|
||||
|
||||
width: 72px;
|
||||
|
||||
min-width: 72px;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-link {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 4px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: #1677ff;
|
||||
|
||||
text-decoration: underline;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
color: #0958d9;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
:payload="getBizRecordPayload(msg.content)!"
|
||||
:mine="msg.mine"
|
||||
:receiver-has-biz-page-permission="msg.receiverHasBizPagePermission"
|
||||
@handled="handleApprovalHandled"
|
||||
/>
|
||||
<div v-else class="message-content">{{ msg.content }}</div>
|
||||
<div class="message-time">{{ formatTime(msg.createTime) }}</div>
|
||||
@@ -1376,6 +1377,19 @@
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天】收到新消息时:靠近底部则自动滚底,否则显示新消息提示-----------
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理后即时刷新当前会话,连续节点同一处理人无需关闭重开-----
|
||||
// 审批卡片办理(通过/驳回)成功后:下一节点卡片或结果通知已由后端同步发出并入库,
|
||||
// 此处强制刷新当前会话消息,使新卡片立即出现在当前聊天窗口(不依赖 WS 推送时序)。
|
||||
async function handleApprovalHandled() {
|
||||
if (!activeConversationId.value) {
|
||||
return;
|
||||
}
|
||||
await loadMessages(true, { forceRefresh: true });
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理后即时刷新当前会话,连续节点同一处理人无需关闭重开-----
|
||||
|
||||
function handleImChatSocketUi(data: Record<string, any>) {
|
||||
if (data.cmd !== 'chat') {
|
||||
return;
|
||||
|
||||
@@ -16,6 +16,16 @@ export interface ImBizRecordItem {
|
||||
/** v2:表格字段 */
|
||||
fields?: ImBizRecordField[];
|
||||
linkPath: string;
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批卡片办理扩展字段-----
|
||||
/** 审批实例ID(存在则为审批卡片,展示办理按钮) */
|
||||
instanceId?: string;
|
||||
/** 当前节点办理按钮文案(如 审批/审核/批准),存在且 canApprove 时展示 */
|
||||
actionLabel?: string;
|
||||
/** 接收人是否可办理(当前活动处理人 且 审批中) */
|
||||
canApprove?: boolean;
|
||||
/** 该卡片对应的节点ID,用于实时判断是否仍为当前节点(旧节点卡片置灰) */
|
||||
nodeId?: string;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批卡片办理扩展字段-----
|
||||
}
|
||||
|
||||
export interface ImBizRecordPayload {
|
||||
|
||||
Reference in New Issue
Block a user