feat: 支持发起人撤回审批
- 在我发起和审批详情中提供申请人撤回操作并恢复资源状态 - 补齐审批说明列表展示、动作权限校验和防重复提交 - 为审批决策增加行锁并补充撤回权限回归测试
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
@@ -41,11 +42,24 @@ const actionLabelMap: Record<string, string> = {
|
||||
PUBLISH: $t('approval.action.publish'),
|
||||
};
|
||||
|
||||
const canApproveAction = computed(
|
||||
() =>
|
||||
!!detail.value?.canApprove &&
|
||||
hasPermission(['/api/v1/approvalInstance/approve']),
|
||||
);
|
||||
const canRejectAction = computed(
|
||||
() =>
|
||||
!!detail.value?.canReject &&
|
||||
hasPermission(['/api/v1/approvalInstance/reject']),
|
||||
);
|
||||
const canRevokeAction = computed(
|
||||
() =>
|
||||
!!detail.value?.canRevoke &&
|
||||
hasPermission(['/api/v1/approvalInstance/revoke']),
|
||||
);
|
||||
const canOperate = computed(() => {
|
||||
return (
|
||||
!!detail.value?.canApprove ||
|
||||
!!detail.value?.canReject ||
|
||||
!!detail.value?.canRevoke
|
||||
canApproveAction.value || canRejectAction.value || canRevokeAction.value
|
||||
);
|
||||
});
|
||||
|
||||
@@ -258,7 +272,7 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
|
||||
<div v-if="canOperate" class="flex items-center gap-3">
|
||||
<ElButton
|
||||
v-if="detail?.canApprove"
|
||||
v-if="canApproveAction"
|
||||
type="success"
|
||||
:disabled="Boolean(approvalActionLoading)"
|
||||
:loading="approvalActionLoading === 'approve'"
|
||||
@@ -267,7 +281,7 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
{{ $t('approval.action.approve') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.canReject"
|
||||
v-if="canRejectAction"
|
||||
type="danger"
|
||||
:disabled="Boolean(approvalActionLoading)"
|
||||
:loading="approvalActionLoading === 'reject'"
|
||||
@@ -276,7 +290,7 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
{{ $t('approval.action.reject') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.canRevoke"
|
||||
v-if="canRevokeAction"
|
||||
type="warning"
|
||||
:disabled="Boolean(approvalActionLoading)"
|
||||
:loading="approvalActionLoading === 'revoke'"
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
import { api } from '#/api/request';
|
||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
@@ -101,6 +101,7 @@ const pendingPageRef = ref();
|
||||
const processedPageRef = ref();
|
||||
const initiatedPageRef = ref();
|
||||
const pendingBadgeCount = ref(0);
|
||||
const approvalActionLoadingKey = ref('');
|
||||
|
||||
const flowQuery = ref({
|
||||
actionType: '',
|
||||
@@ -235,7 +236,8 @@ async function syncRouteTab() {
|
||||
const currentQueryTab = String(route.query.tab || '');
|
||||
if (
|
||||
fallbackTab &&
|
||||
(route.path !== '/sys/approval' || currentQueryTab !== String(fallbackTab.name))
|
||||
(route.path !== '/sys/approval' ||
|
||||
currentQueryTab !== String(fallbackTab.name))
|
||||
) {
|
||||
await router.replace({
|
||||
path: '/sys/approval',
|
||||
@@ -395,31 +397,71 @@ async function submitApprovalAction(
|
||||
action: 'approve' | 'reject' | 'revoke',
|
||||
row: any,
|
||||
) {
|
||||
const loadingKey = `${action}:${String(row.id)}`;
|
||||
if (approvalActionLoadingKey.value) {
|
||||
return;
|
||||
}
|
||||
approvalActionLoadingKey.value = loadingKey;
|
||||
const titleMap = {
|
||||
approve: $t('approval.action.approve'),
|
||||
reject: $t('approval.action.reject'),
|
||||
revoke: $t('approval.action.revoke'),
|
||||
};
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
$t('approval.placeholder.actionComment'),
|
||||
titleMap[action],
|
||||
{
|
||||
inputValue: '',
|
||||
inputType: 'textarea',
|
||||
},
|
||||
);
|
||||
const res = await api.post(`/api/v1/approvalInstance/${action}`, {
|
||||
comment: value || '',
|
||||
instanceId: row.id,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
try {
|
||||
let value = '';
|
||||
try {
|
||||
const promptResult = await ElMessageBox.prompt(
|
||||
$t('approval.placeholder.actionComment'),
|
||||
titleMap[action],
|
||||
{
|
||||
inputValue: '',
|
||||
inputType: 'textarea',
|
||||
},
|
||||
);
|
||||
value = promptResult.value || '';
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const res = await api.post(`/api/v1/approvalInstance/${action}`, {
|
||||
comment: value,
|
||||
instanceId: row.id,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('approval.message.actionSuccess'));
|
||||
pendingPageRef.value?.reload();
|
||||
processedPageRef.value?.reload();
|
||||
initiatedPageRef.value?.reload();
|
||||
await refreshPendingBadgeCount();
|
||||
} finally {
|
||||
approvalActionLoadingKey.value = '';
|
||||
}
|
||||
ElMessage.success($t('approval.message.actionSuccess'));
|
||||
pendingPageRef.value?.reload();
|
||||
processedPageRef.value?.reload();
|
||||
initiatedPageRef.value?.reload();
|
||||
refreshPendingBadgeCount();
|
||||
}
|
||||
|
||||
function isApprovalActionLoading(
|
||||
action: 'approve' | 'reject' | 'revoke',
|
||||
row: any,
|
||||
) {
|
||||
return approvalActionLoadingKey.value === `${action}:${String(row.id)}`;
|
||||
}
|
||||
|
||||
function canSubmitApprovalAction(
|
||||
action: 'approve' | 'reject' | 'revoke',
|
||||
row: any,
|
||||
) {
|
||||
const actionFlagMap = {
|
||||
approve: row.canApprove,
|
||||
reject: row.canReject,
|
||||
revoke: row.canRevoke,
|
||||
};
|
||||
return (
|
||||
Boolean(actionFlagMap[action]) &&
|
||||
hasPermission([`/api/v1/approvalInstance/${action}`])
|
||||
);
|
||||
}
|
||||
|
||||
function openInstanceDetail(row: any) {
|
||||
@@ -456,6 +498,10 @@ function getActionLabel(value: string) {
|
||||
ACTION_OPTIONS.find((item) => item.value === value)?.label || value || '-'
|
||||
);
|
||||
}
|
||||
|
||||
function formatApplicationReason(value?: null | string) {
|
||||
return String(value || '').trim() || '-';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -731,6 +777,14 @@ function getActionLabel(value: string) {
|
||||
:label="$t('approval.fields.summary')"
|
||||
min-width="220"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.applicationReason')"
|
||||
min-width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatApplicationReason(row.applicationReason) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.resourceType')"
|
||||
width="120"
|
||||
@@ -782,17 +836,21 @@ function getActionLabel(value: string) {
|
||||
{{ $t('button.view') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="row.canApprove"
|
||||
v-if="canSubmitApprovalAction('approve', row)"
|
||||
link
|
||||
type="success"
|
||||
:disabled="Boolean(approvalActionLoadingKey)"
|
||||
:loading="isApprovalActionLoading('approve', row)"
|
||||
@click="submitApprovalAction('approve', row)"
|
||||
>
|
||||
{{ $t('approval.action.approve') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="row.canReject"
|
||||
v-if="canSubmitApprovalAction('reject', row)"
|
||||
link
|
||||
type="danger"
|
||||
:disabled="Boolean(approvalActionLoadingKey)"
|
||||
:loading="isApprovalActionLoading('reject', row)"
|
||||
@click="submitApprovalAction('reject', row)"
|
||||
>
|
||||
{{ $t('approval.action.reject') }}
|
||||
@@ -903,6 +961,14 @@ function getActionLabel(value: string) {
|
||||
:label="$t('approval.fields.summary')"
|
||||
min-width="220"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.applicationReason')"
|
||||
min-width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatApplicationReason(row.applicationReason) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.resourceType')"
|
||||
width="120"
|
||||
@@ -1052,6 +1118,14 @@ function getActionLabel(value: string) {
|
||||
:label="$t('approval.fields.summary')"
|
||||
min-width="220"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.applicationReason')"
|
||||
min-width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatApplicationReason(row.applicationReason) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.resourceType')"
|
||||
width="120"
|
||||
@@ -1098,8 +1172,11 @@ function getActionLabel(value: string) {
|
||||
{{ $t('button.view') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="row.canRevoke"
|
||||
v-if="canSubmitApprovalAction('revoke', row)"
|
||||
link
|
||||
type="warning"
|
||||
:disabled="Boolean(approvalActionLoadingKey)"
|
||||
:loading="isApprovalActionLoading('revoke', row)"
|
||||
@click="submitApprovalAction('revoke', row)"
|
||||
>
|
||||
{{ $t('approval.action.revoke') }}
|
||||
|
||||
Reference in New Issue
Block a user