feat: 归档L03与L09审批发布能力
- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象 - 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElEmpty,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import BotApprovalSnapshotPreview from '#/views/system/approval/components/BotApprovalSnapshotPreview.vue';
|
||||
import KnowledgeApprovalSnapshotPreview from '#/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue';
|
||||
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const detail = ref<any>(null);
|
||||
|
||||
const resourceLabelMap: Record<string, string> = {
|
||||
BOT: $t('approval.resource.bot'),
|
||||
KNOWLEDGE: $t('approval.resource.knowledge'),
|
||||
WORKFLOW: $t('approval.resource.workflow'),
|
||||
};
|
||||
|
||||
const actionLabelMap: Record<string, string> = {
|
||||
DELETE: $t('approval.action.delete'),
|
||||
PUBLISH: $t('approval.action.publish'),
|
||||
};
|
||||
|
||||
const canOperate = computed(() => {
|
||||
return (
|
||||
!!detail.value?.canApprove ||
|
||||
!!detail.value?.canReject ||
|
||||
!!detail.value?.canRevoke
|
||||
);
|
||||
});
|
||||
|
||||
const workflowSnapshot = computed(() => {
|
||||
if (detail.value?.resourceType !== 'WORKFLOW') {
|
||||
return null;
|
||||
}
|
||||
const snapshot = detail.value?.snapshotJson?.resourceSnapshot;
|
||||
return {
|
||||
title: snapshot?.title || '',
|
||||
description: snapshot?.description || '',
|
||||
content: snapshot?.content || '',
|
||||
};
|
||||
});
|
||||
|
||||
const knowledgeSnapshot = computed(() => {
|
||||
if (detail.value?.resourceType !== 'KNOWLEDGE') {
|
||||
return null;
|
||||
}
|
||||
return detail.value?.snapshotJson?.resourceSnapshot || null;
|
||||
});
|
||||
|
||||
const botSnapshot = computed(() => {
|
||||
if (detail.value?.resourceType !== 'BOT') {
|
||||
return null;
|
||||
}
|
||||
return detail.value?.snapshotJson?.resourceSnapshot || null;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadDetail();
|
||||
});
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/approvalInstance/detail', {
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
detail.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitApprovalAction(action: 'approve' | 'reject' | 'revoke') {
|
||||
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: detail.value?.id,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('approval.message.actionSuccess'));
|
||||
await loadDetail();
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
const map: Record<string, any> = {
|
||||
APPROVED: 'success',
|
||||
PENDING: 'warning',
|
||||
PROCESSING: 'primary',
|
||||
REJECTED: 'danger',
|
||||
REVOKED: 'info',
|
||||
};
|
||||
return map[status] || 'info';
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string) {
|
||||
return $t(`approval.status.${String(status || '').toLowerCase()}`);
|
||||
}
|
||||
|
||||
function formatPayload(payload: Record<string, any>) {
|
||||
return JSON.stringify(payload || {}, null, 2);
|
||||
}
|
||||
|
||||
function formatAccountDisplay(name?: string, id?: null | number | string) {
|
||||
if (name && id) {
|
||||
return `${name}(${id})`;
|
||||
}
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
return id || '-';
|
||||
}
|
||||
|
||||
function formatOperatorId(id?: null | number | string) {
|
||||
return id || '-';
|
||||
}
|
||||
|
||||
function formatOperatorName(name?: null | string) {
|
||||
return name || '-';
|
||||
}
|
||||
|
||||
function formatAssigneeDisplay(row: Record<string, any>) {
|
||||
if (!row?.assigneeType || !row?.assigneeTargetName) {
|
||||
return '-';
|
||||
}
|
||||
if (row.assigneeType === 'ROLE') {
|
||||
return `${$t('approval.assignee.role')}:${row.assigneeTargetName}`;
|
||||
}
|
||||
return `${$t('approval.assignee.user')}:${row.assigneeTargetName}`;
|
||||
}
|
||||
|
||||
function getEventTypeLabel(eventType?: string) {
|
||||
const eventTypeMap: Record<string, string> = {
|
||||
APPROVED: $t('approval.event.approved'),
|
||||
REJECTED: $t('approval.event.rejected'),
|
||||
REVOKED: $t('approval.event.revoked'),
|
||||
STEP_CREATED: $t('approval.event.stepCreated'),
|
||||
SUBMITTED: $t('approval.event.submitted'),
|
||||
};
|
||||
return eventTypeMap[String(eventType || '')] || eventType || '-';
|
||||
}
|
||||
|
||||
function formatEventInfo(row: Record<string, any>) {
|
||||
const payload = row?.payloadJson || {};
|
||||
const eventType = String(row?.eventType || '');
|
||||
|
||||
switch (eventType) {
|
||||
case 'APPROVED': {
|
||||
return payload.stepNo
|
||||
? $t('approval.message.eventApprovedStep', { value: payload.stepNo })
|
||||
: $t('approval.message.eventApproved');
|
||||
}
|
||||
case 'REJECTED': {
|
||||
return payload.stepNo
|
||||
? $t('approval.message.eventRejectedStep', { value: payload.stepNo })
|
||||
: $t('approval.message.eventRejected');
|
||||
}
|
||||
case 'REVOKED': {
|
||||
return payload.stepNo
|
||||
? $t('approval.message.eventRevokedStep', { value: payload.stepNo })
|
||||
: $t('approval.message.eventRevoked');
|
||||
}
|
||||
case 'STEP_CREATED': {
|
||||
return payload.stepName || '-';
|
||||
}
|
||||
case 'SUBMITTED': {
|
||||
return payload.summary || detail.value?.summary || '-';
|
||||
}
|
||||
default: {
|
||||
return payload.stepName || payload.summary || row?.eventType || '-';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-4 p-6" v-loading="loading">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton :icon="ArrowLeft" @click="router.back()">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
||||
<IconifyIcon icon="svg:approval" class="size-5" />
|
||||
<span>{{ $t('approval.title') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canOperate" class="flex items-center gap-3">
|
||||
<ElButton
|
||||
v-if="detail?.canApprove"
|
||||
type="success"
|
||||
@click="submitApprovalAction('approve')"
|
||||
>
|
||||
{{ $t('approval.action.approve') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.canReject"
|
||||
type="danger"
|
||||
@click="submitApprovalAction('reject')"
|
||||
>
|
||||
{{ $t('approval.action.reject') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.canRevoke"
|
||||
@click="submitApprovalAction('revoke')"
|
||||
>
|
||||
{{ $t('approval.action.revoke') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detail">
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.basic') }}</h3>
|
||||
<ElTag :type="getStatusType(detail.status)">
|
||||
{{ getStatusLabel(detail.status) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.summary')">
|
||||
{{ detail.summary || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.currentStep')">
|
||||
{{ detail.currentStepNo || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.resourceType')">
|
||||
{{
|
||||
resourceLabelMap[detail.resourceType] ||
|
||||
detail.resourceType ||
|
||||
'-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.actionType')">
|
||||
{{ actionLabelMap[detail.actionType] || detail.actionType || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.taskId')">
|
||||
{{ detail.id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
|
||||
{{ formatAccountDisplay(detail.applicantName, detail.applicantId) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
|
||||
{{ detail.submittedAt || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.finishedAt')">
|
||||
{{ detail.finishedAt || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</section>
|
||||
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.tasks') }}</h3>
|
||||
</div>
|
||||
<ElTable :data="detail.tasks || []" show-overflow-tooltip>
|
||||
<ElTableColumn
|
||||
prop="stepNo"
|
||||
:label="$t('approval.fields.stepNoLabel')"
|
||||
width="100"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="stepName"
|
||||
:label="$t('approval.fields.stepName')"
|
||||
min-width="180"
|
||||
/>
|
||||
<ElTableColumn :label="$t('approval.fields.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getStatusType(row.status)">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.assigneeTarget')"
|
||||
width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatAssigneeDisplay(row) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('approval.fields.actedBy')" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatAccountDisplay(row.actedByName, row.actedBy) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="actedAt"
|
||||
:label="$t('approval.fields.actedAt')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="comment"
|
||||
:label="$t('approval.fields.comment')"
|
||||
min-width="200"
|
||||
/>
|
||||
</ElTable>
|
||||
</section>
|
||||
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.logs') }}</h3>
|
||||
</div>
|
||||
<ElTable :data="detail.logs || []" show-overflow-tooltip>
|
||||
<ElTableColumn :label="$t('approval.fields.eventType')" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ getEventTypeLabel(row.eventType) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatOperatorId(row.operatorId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.operatorName')"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatOperatorName(row.operatorName) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="created"
|
||||
:label="$t('approval.fields.createdAt')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.eventInfo')"
|
||||
min-width="280"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="approval-detail__event-info">
|
||||
{{ formatEventInfo(row) }}
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</section>
|
||||
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.snapshot') }}</h3>
|
||||
</div>
|
||||
<WorkflowApprovalSnapshotPreview
|
||||
v-if="workflowSnapshot"
|
||||
:title="workflowSnapshot.title"
|
||||
:description="workflowSnapshot.description"
|
||||
:content="workflowSnapshot.content"
|
||||
/>
|
||||
<KnowledgeApprovalSnapshotPreview
|
||||
v-else-if="knowledgeSnapshot"
|
||||
:snapshot="knowledgeSnapshot"
|
||||
/>
|
||||
<BotApprovalSnapshotPreview
|
||||
v-else-if="botSnapshot"
|
||||
:snapshot="botSnapshot"
|
||||
/>
|
||||
<pre v-else class="approval-detail__snapshot">{{
|
||||
formatPayload(detail.snapshotJson)
|
||||
}}</pre>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<ElEmpty v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-detail__panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--glass-border) / 72%) 0%,
|
||||
hsl(var(--surface-panel) / 95%) 100%
|
||||
);
|
||||
border: 1px solid hsl(var(--divider-faint) / 50%);
|
||||
border-radius: 24px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 hsl(var(--glass-border) / 72%),
|
||||
0 18px 36px -32px hsl(var(--primary) / 20%);
|
||||
}
|
||||
|
||||
.approval-detail__panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approval-detail__panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.approval-detail__snapshot,
|
||||
.approval-detail__payload {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
padding: 14px 16px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.9);
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.54);
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.approval-detail__event-info {
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user