- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路 - 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览 - 调整审批管理权限模型为单入口页面加内部按钮权限
473 lines
14 KiB
Vue
473 lines
14 KiB
Vue
<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'),
|
||
OFFLINE: $t('approval.action.offline'),
|
||
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,
|
||
account?: null | string,
|
||
fallbackId?: null | number | string,
|
||
) {
|
||
if (name && account) {
|
||
return `${name}(${account})`;
|
||
}
|
||
if (name) {
|
||
return name;
|
||
}
|
||
if (account) {
|
||
return account;
|
||
}
|
||
return fallbackId || '-';
|
||
}
|
||
|
||
function formatOperatorId(
|
||
account?: null | string,
|
||
fallbackId?: null | number | string,
|
||
) {
|
||
return account || fallbackId || '-';
|
||
}
|
||
|
||
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.applicantAccount,
|
||
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.operatorAccount, 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>
|