完善MES审批流设计功能,新增审批可选回调动作、发起人撤销及催办接口,支持审批状态恢复与联动回退,提升审批流程的灵活性与用户体验。

This commit is contained in:
geht
2026-05-29 18:57:09 +08:00
parent aefa44b8a9
commit 0ff4a201b0
33 changed files with 1617 additions and 250 deletions

View File

@@ -1,94 +0,0 @@
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;
}
}

View File

@@ -19,9 +19,6 @@ 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();
@@ -93,9 +90,6 @@ 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不加前缀

View File

@@ -17,6 +17,9 @@ enum Api {
// 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审批流设计】当前页设计上下文-----
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】业务表可选回调动作-----
bizActions = '/xslmes/approvalFlow/bizActions',
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】业务表可选回调动作-----
}
/**
@@ -73,3 +76,11 @@ export const batchDeleteApprovalFlow = (params, handleSuccess) => {
*/
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】业务表可选回调动作(后端@ApprovalBizAction注解扫描)-----
/**
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供节点「回调接口」下拉选择。
* 返回 [{ name, url, method, table, phase, perms }]
*/
export const getApprovalBizActions = (table: string) => defHttp.get({ url: Api.bizActions, params: { table } });
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】业务表可选回调动作-----

View File

@@ -10,6 +10,9 @@ enum Api {
status = '/xslmes/approvalHandle/status',
approve = '/xslmes/approvalHandle/approve',
reject = '/xslmes/approvalHandle/reject',
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
cancel = '/xslmes/approvalHandle/cancel',
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
}
/** 查看单据全部字段 + 审批进度/历史 */
@@ -23,3 +26,8 @@ export const approveApproval = (params: { instanceId: string; comment?: string }
/** 驳回(需填写理由) */
export const rejectApproval = (params: { instanceId: string; reason: string }) => defHttp.post({ url: Api.reject, data: params });
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
/** 撤销(仅发起人本人,审批中可撤回,业务单据恢复到发起时状态) */
export const cancelApproval = (params: { instanceId: string; reason?: string }) => defHttp.post({ url: Api.cancel, data: params });
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----

View File

@@ -4,7 +4,7 @@
@date 2026-05-29 forQH-MES审批流设计新增审批流可视化设计
-->
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave" :wrapClassName="flowApiRecording ? 'flow-design-recording-hide' : ''">
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave">
<div class="fd-design">
<div class="fd-toolbar">
<span class="fd-tb-item">绑定单据<b>{{ record.bizTableName || record.bizTable }}</b></span>
@@ -58,7 +58,6 @@
} 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' });
@@ -69,6 +68,9 @@
const readonly = ref(false);
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
const drawerRef = ref();
// 当前审批流绑定的业务表,供节点配置按表查可选回调动作
const bizTableRef = ref('');
provide('approvalBizTable', bizTableRef);
// 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审批流设计】当前页识别到的候选阶段字段----- -->
@@ -105,6 +107,7 @@
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
readonly.value = !!data?.readonly;
flowCtx.readonly = readonly.value;
bizTableRef.value = data?.record?.bizTable || '';
// 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审批流设计】接收当前页解析出的候选阶段字段----- -->

View File

@@ -4,7 +4,7 @@
@date 2026-05-29 forQH-MES审批流设计新增审批流可视化设计
-->
<template>
<a-drawer :title="title" :width="480" :open="open && !flowApiRecording" @close="onClose" :maskClosable="!readonly">
<a-drawer :title="title" :width="480" :open="open" @close="onClose" :maskClosable="!readonly">
<template v-if="form">
<a-form layout="vertical">
<a-form-item label="节点名称">
@@ -80,13 +80,17 @@
</a-radio-group>
</a-form-item>
<!-- update-begin---author:GHT ---date:2026-05-29 forQH-MES审批流设计节点回调接口可视化配置(录制业务按钮接口)----- -->
<!-- update-begin---author:GHT ---date:2026-05-29 forQH-MES审批流设计节点回调接口可视化配置(自动识别页面按钮)----- -->
<a-divider style="margin: 16px 0 12px">回调接口审批联动业务</a-divider>
<a-alert
type="info"
show-icon
style="margin-bottom: 12px"
message="审批到对应时机时系统会以「当前处理人」身份调用所选业务接口自动带上单据ID。点击「录制」后设计器会临时隐藏请到页面上点击目标按钮如“批准”系统自动识别其接口并回填。"
:message="
pageActionOptions.length
? '以下为该业务已标注(@ApprovalBizAction的可选接口。审批到对应时机时系统会以「当前处理人」身份调用所选接口自动带上单据ID。'
: '该业务暂未标注可选接口(在后端 Controller 方法上加 @ApprovalBizAction 注解即可出现在此),也可手动填写接口路径。'
"
/>
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
<div class="fd-cb-title">{{ phase.label }}</div>
@@ -96,15 +100,28 @@
<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-space v-if="!readonly" style="margin-top: 4px" :size="6" wrap>
<a-select
v-if="pageActionOptions.length"
style="width: 240px"
placeholder="从页面按钮中选择…"
:options="pageActionOptions"
v-model:value="actionPicker[phase.key]"
show-search
option-filter-prop="label"
@select="(v) => addFromButton(phase.key, v)"
/>
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
</a-space>
</div>
<!-- update-end---author:GHT ---date:2026-05-29 forQH-MES审批流设计节点回调接口可视化配置(录制业务按钮接口)----- -->
<!-- update-begin---author:GHT ---date:2026-05-29 forQH-MES审批流设计驳回统一回退,无需逐节点配置----- -->
<a-alert
type="success"
show-icon
style="margin-top: 4px"
message="驳回 / 撤销 已全局统一:系统会自动执行该业务标注为「驳回时执行(@ApprovalBizAction onReject)」的接口完成回退,无需在此逐节点、逐流程配置。"
/>
<!-- update-end---author:GHT ---date:2026-05-29 forQH-MES审批流设计驳回统一回退,无需逐节点配置----- -->
</template>
<!-- 抄送人 -->
@@ -173,20 +190,11 @@
</a-space>
</template>
</a-drawer>
<!-- update-begin---author:GHT ---date:2026-05-29 forQH-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 forQH-MES审批流设计录制中悬浮提示条----- -->
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, ref, inject, watch } from 'vue';
import type { Ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { defHttp } from '/@/utils/http/axios';
import { ApiSelect } from '/@/components/Form';
@@ -194,25 +202,63 @@
import { OPERATOR_OPTIONS } from './flowTypes';
import type { FlowNode } from './flowTypes';
import { useMessage } from '/@/hooks/web/useMessage';
import { flowApiRecording, startFlowApiRecord, cancelFlowApiRecord } from '/@/utils/flowApiRecorder';
import { getApprovalBizActions } from '../approvalFlow.api';
const props = defineProps<{ readonly?: boolean }>();
const emit = defineEmits(['confirm']);
const { createMessage } = useMessage();
// 当前审批流绑定的业务表(由 FlowDesign 注入),据此向后端查可选回调动作
const bizTable = inject<Ref<string>>('approvalBizTable', ref(''));
// 后端 @ApprovalBizAction 标注的可选业务动作
const bizActions = ref<any[]>([]);
const bizActionsTable = ref('');
async function loadBizActions() {
const table = bizTable.value || '';
if (!table || bizActionsTable.value === table) {
return;
}
try {
const res = await getApprovalBizActions(table);
bizActions.value = Array.isArray(res) ? res : [];
bizActionsTable.value = table;
} catch (e) {
bizActions.value = [];
}
}
// 可选动作下拉项(含真实 url/method
const pageActionOptions = computed(() =>
(bizActions.value || []).map((a) => ({
label: `${a.name}${a.method} ${a.url}`,
value: a.url,
raw: a,
})),
);
watch(
bizTable,
() => {
loadBizActions();
},
{ immediate: true },
);
const open = ref(false);
const node = ref<FlowNode | null>(null);
const form = ref<any>(null);
// 「从页面按钮中选择」下拉的临时选中值(仅作选择器,选完即清空,避免跨节点残留上次的选项)
const actionPicker = ref<Record<string, any>>({ onNodeApprove: undefined, onApprove: undefined, onReject: undefined });
const operatorOptions = OPERATOR_OPTIONS;
const readonly = computed(() => !!props.readonly);
// 回调接口配置:三个触发时机
// 回调接口配置:通过类时机需按节点配置;驳回类(onReject)已全局统一(后端按 @ApprovalBizAction 自动执行),无需在此逐节点维护
const callbackPhases = [
{ key: 'onNodeApprove', label: '本节点通过时执行' },
{ key: 'onApprove', label: '流程最终通过时执行' },
{ key: 'onReject', label: '驳回时执行' },
];
const methodOptions = [
{ label: 'POST', value: 'POST' },
@@ -230,6 +276,8 @@
function openDrawer(n: FlowNode) {
node.value = n;
// 切换节点时清空下拉选择器的残留值
actionPicker.value = { onNodeApprove: undefined, onApprove: undefined, onReject: undefined };
// 编辑副本,确定时回写,避免取消后脏数据
form.value = { name: n.name, props: cloneDeep(n.props) };
// 审批人节点确保回调接口配置结构存在
@@ -252,17 +300,21 @@
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}`);
/** 从「已标注的业务动作」中选择一个接口加入回调(按 url 值查回原始动作) */
function addFromButton(phaseKey: string, url: string) {
const opt = pageActionOptions.value.find((o) => o.value === url);
const raw = opt?.raw;
if (!raw || !raw.url) {
return;
}
}
function cancelRecord() {
cancelFlowApiRecord();
const list = form.value.props.callbackActions[phaseKey];
// 选完即清空下拉,使其回到 placeholder仅作选择器用
actionPicker.value[phaseKey] = undefined;
if (list.some((a) => a.url === raw.url)) {
createMessage.info('该接口已添加');
return;
}
list.push({ name: raw.name, method: raw.method || 'POST', url: raw.url });
}
function onClose() {
@@ -327,33 +379,3 @@
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>

View File

@@ -5,11 +5,6 @@
*/
@line-color: #cacaca;
/* 录制回调接口时,隐藏整个设计器弹窗(含遮罩),让用户能点击业务页面上的真实按钮 */
.flow-design-recording-hide {
display: none !important;
}
.fd-design {
display: flex;
flex-direction: column;

View File

@@ -35,6 +35,13 @@
</template>
<!-- 不可办理(已处理/已流转/非当前处理人)置灰提示 -->
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
<!-- 发起人审批中可撤销 -->
<a-button v-if="canCancel" size="small" danger :loading="cancelling" @click="openCancel(singleItem)">
<Icon icon="ant-design:rollback-outlined" />
<span>撤销</span>
</a-button>
<!-- 发起人审批已结束的状态提示 -->
<span v-else-if="mineEndedText" class="im-biz-record-disabled">{{ mineEndedText }}</span>
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
<Icon icon="ant-design:unordered-list-outlined" />
<span>跳转至列表</span>
@@ -90,17 +97,30 @@
</a-form-item>
</a-form>
</a-modal>
<!-- 撤销弹窗 -->
<a-modal v-model:open="cancelOpen" title="撤销审批" :confirmLoading="cancelling" okText="确认撤销" @ok="confirmCancel">
<a-alert type="warning" show-icon :message="'撤销后流程将终止,单据将恢复到发起审批前的状态。'" style="margin-bottom: 12px" />
<a-form layout="vertical">
<a-form-item label="撤销原因">
<a-textarea v-model:value="cancelReason" :rows="3" placeholder="可选填写撤销原因" :maxlength="500" show-count />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted, watch } from 'vue';
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue';
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
import { navigateImBizRecordLink } from './imRecordLocate';
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
import { onImMessagesUpdated } from './imCache';
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
import { useMessage } from '/@/hooks/web/useMessage';
import { approveApproval, rejectApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
import { approveApproval, rejectApproval, cancelApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
defineOptions({ name: 'ImBizRecordMessageContent' });
@@ -122,8 +142,14 @@
const rejectOpen = ref(false);
const rejectReason = ref('');
const rejectItem = ref<ImBizRecordItem | null>(null);
// 本地办理结果approved / rejected卡片消息为静态办理后本地标记
const actionDone = ref<'' | 'approved' | 'rejected'>('');
// 本地办理结果approved / rejected / cancelled(卡片消息为静态,办理后本地标记)
const actionDone = ref<'' | 'approved' | 'rejected' | 'cancelled'>('');
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
const cancelling = ref(false);
const cancelOpen = ref(false);
const cancelReason = ref('');
const cancelItem = ref<ImBizRecordItem | null>(null);
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
// 审批实例实时状态(用于旧节点卡片置灰)
interface LiveStatus {
@@ -184,11 +210,38 @@
return '等待他人处理';
});
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
// 是否可撤销:审批卡片 且 本人发起(mine) 且 审批中 且 本地未撤销
const canCancel = computed(() => {
if (!isApprovalCard.value || !props.mine || actionDone.value) {
return false;
}
const s = liveStatus.value;
return !!s && s.exists === true && s.status === '0';
});
// 发起人视角的结束态提示
const mineEndedText = computed(() => {
if (!isApprovalCard.value || !props.mine) {
return '';
}
if (actionDone.value === 'cancelled') return '已撤销';
const s = liveStatus.value;
if (!s || !s.exists) return '';
if (s.status === '1') return '已通过';
if (s.status === '2') return '已驳回';
if (s.status === '3') return '已撤销';
return '';
});
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
async function loadLiveStatus() {
const id = singleItem.value?.instanceId;
if (!isApprovalCard.value || props.mine || !id) {
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
if (!isApprovalCard.value || !id) {
return;
}
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
try {
const res: any = await getApprovalStatus(id);
liveStatus.value = (res || { exists: false }) as LiveStatus;
@@ -202,6 +255,33 @@
onMounted(loadLiveStatus);
watch(() => singleItem.value?.instanceId, loadLiveStatus);
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
// 撤销/驳回/流转时后端会向处理人推送IM消息WS到达会触发 onImMessagesUpdated
// 借此实时重新拉取实例状态,使本卡片的「审批/拒绝」按钮即时置灰、不可点击。
let unsubscribeMsgUpdated: (() => void) | null = null;
onMounted(() => {
if (!isApprovalCard.value) {
return;
}
unsubscribeMsgUpdated = onImMessagesUpdated(() => {
// 本地已办理的卡片无需再刷新,避免覆盖本地置灰文案
if (actionDone.value) {
return;
}
// 已是终态(已通过/已驳回/已撤销/已失效)的卡片无需重复拉取
const s = liveStatus.value;
if (s && (s.exists === false || (!!s.status && s.status !== '0'))) {
return;
}
loadLiveStatus();
});
});
onBeforeUnmount(() => {
unsubscribeMsgUpdated?.();
unsubscribeMsgUpdated = null;
});
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
@@ -231,6 +311,13 @@
async function handleApprove(item: ImBizRecordItem) {
if (!item.instanceId || approving.value) return;
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
await loadLiveStatus();
if (!liveActionable.value) {
createMessage.warning(disabledText.value || '该审批已无法办理');
return;
}
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
try {
approving.value = true;
const res: any = await approveApproval({ instanceId: item.instanceId });
@@ -257,6 +344,14 @@
createMessage.warning('请填写驳回理由');
return;
}
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
await loadLiveStatus();
if (!liveActionable.value) {
createMessage.warning(disabledText.value || '该审批已无法办理');
rejectOpen.value = false;
return;
}
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
try {
rejecting.value = true;
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
@@ -269,6 +364,30 @@
rejecting.value = false;
}
}
// update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
function openCancel(item: ImBizRecordItem) {
cancelItem.value = item;
cancelReason.value = '';
cancelOpen.value = true;
}
async function confirmCancel() {
const item = cancelItem.value;
if (!item?.instanceId || cancelling.value) return;
try {
cancelling.value = true;
await cancelApproval({ instanceId: item.instanceId, reason: cancelReason.value.trim() });
createMessage.success('已撤销,单据已恢复到发起前状态');
actionDone.value = 'cancelled';
cancelOpen.value = false;
await loadLiveStatus();
emit('handled');
} finally {
cancelling.value = false;
}
}
// update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
</script>
<style lang="less" scoped>