feat: 收敛AI资源发布审批生命周期

- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路

- 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览

- 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
2026-04-09 17:13:54 +08:00
parent 81125ce55c
commit 4e565aef99
68 changed files with 3859 additions and 817 deletions

View File

@@ -36,6 +36,7 @@ const resourceLabelMap: Record<string, string> = {
const actionLabelMap: Record<string, string> = {
DELETE: $t('approval.action.delete'),
OFFLINE: $t('approval.action.offline'),
PUBLISH: $t('approval.action.publish'),
};
@@ -138,18 +139,28 @@ 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}`;
function formatAccountDisplay(
name?: string,
account?: null | string,
fallbackId?: null | number | string,
) {
if (name && account) {
return `${name}${account}`;
}
if (name) {
return name;
}
return id || '-';
if (account) {
return account;
}
return fallbackId || '-';
}
function formatOperatorId(id?: null | number | string) {
return id || '-';
function formatOperatorId(
account?: null | string,
fallbackId?: null | number | string,
) {
return account || fallbackId || '-';
}
function formatOperatorName(name?: null | string) {
@@ -276,7 +287,13 @@ function formatEventInfo(row: Record<string, any>) {
{{ detail.id || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
{{ formatAccountDisplay(detail.applicantName, detail.applicantId) }}
{{
formatAccountDisplay(
detail.applicantName,
detail.applicantAccount,
detail.applicantId,
)
}}
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
{{ detail.submittedAt || '-' }}
@@ -347,7 +364,7 @@ function formatEventInfo(row: Record<string, any>) {
</ElTableColumn>
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
<template #default="{ row }">
{{ formatOperatorId(row.operatorId) }}
{{ formatOperatorId(row.operatorAccount, row.operatorId) }}
</template>
</ElTableColumn>
<ElTableColumn

View File

@@ -31,7 +31,7 @@ defineExpose({
});
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
type ActionType = '' | 'DELETE' | 'PUBLISH';
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
type AssigneeType = 'ROLE' | 'USER';
type ScopeType = 'CATEGORY' | 'DEPT';
type FlowStatus = 'DISABLED' | 'ENABLED';
@@ -77,6 +77,7 @@ const RESOURCE_OPTIONS = [
const ACTION_OPTIONS = [
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
{ label: $t('approval.action.delete'), value: 'DELETE' },
];

View File

@@ -23,6 +23,7 @@ import {
} from 'element-plus';
import { api } from '#/api/request';
import { hasPermission } from '#/api/common/hasPermission';
import ListPageShell from '#/components/page/ListPageShell.vue';
import PageData from '#/components/page/PageData.vue';
import { $t } from '#/locales';
@@ -40,6 +41,7 @@ const RESOURCE_OPTIONS = [
const ACTION_OPTIONS = [
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
{ label: $t('approval.action.delete'), value: 'DELETE' },
];
@@ -66,21 +68,25 @@ const TAB_CONFIG = [
label: $t('approval.tab.flow'),
name: 'flow',
path: '/sys/approval/flow',
permission: '/page/approval/flow',
},
{
label: $t('approval.tab.pending'),
name: 'pending',
path: '/sys/approval/pending',
permission: '/page/approval/pending',
},
{
label: $t('approval.tab.processed'),
name: 'processed',
path: '/sys/approval/processed',
permission: '/page/approval/processed',
},
{
label: $t('approval.tab.initiated'),
name: 'initiated',
path: '/sys/approval/initiated',
permission: '/page/approval/initiated',
},
] as const;
@@ -115,14 +121,23 @@ const pendingBadgeText = computed(() =>
const hasApprovalRootMenu = computed(
() => !!accessStore.getMenuByPath('/sys/approval'),
);
const hasLegacyTabMenus = computed(() =>
TAB_CONFIG.some((item) => !!accessStore.getMenuByPath(item.path)),
);
const hasButtonTabPermissions = computed(() =>
TAB_CONFIG.some((item) => hasPermission([item.permission])),
);
const visibleTabs = computed(() => {
const matchedTabs = TAB_CONFIG.filter((item) =>
accessStore.getMenuByPath(item.path),
);
if (matchedTabs.length > 0) {
return matchedTabs;
if (!hasApprovalRootMenu.value) {
return [];
}
return hasApprovalRootMenu.value ? [...TAB_CONFIG] : [];
if (hasButtonTabPermissions.value) {
return TAB_CONFIG.filter((item) => hasPermission([item.permission]));
}
if (hasLegacyTabMenus.value) {
return TAB_CONFIG.filter((item) => accessStore.getMenuByPath(item.path));
}
return [];
});
const visibleTabNames = computed(() =>
visibleTabs.value.map((item) => item.name),
@@ -139,7 +154,7 @@ const instanceStatusOptions = computed(() => {
});
onMounted(() => {
void syncRouteTab(route.path);
void syncRouteTab();
});
watch(activeTab, () => {
@@ -153,9 +168,9 @@ watch(activeTab, () => {
}
});
watch(
[() => route.path, visibleTabNames],
async ([path]) => {
await syncRouteTab(path);
[() => route.fullPath, visibleTabNames],
async () => {
await syncRouteTab();
},
{ immediate: true },
);
@@ -177,19 +192,55 @@ function resolveTabNameByPath(path: string): ApprovalTabName {
return 'flow';
}
async function syncRouteTab(path: string) {
function resolveTabNameByRoute() {
const queryTab = String(route.query.tab || '');
if (
queryTab === 'flow' ||
queryTab === 'pending' ||
queryTab === 'processed' ||
queryTab === 'initiated'
) {
return queryTab as ApprovalTabName;
}
return resolveTabNameByPath(route.path);
}
async function syncRouteTab() {
if (visibleTabs.value.length === 0) {
activeTab.value = 'flow';
pendingBadgeCount.value = 0;
return;
}
const expectedTab = resolveTabNameByPath(path);
const fallbackTab = visibleTabs.value[0];
const queryTab = String(route.query.tab || '');
// 进入审批管理根路径时,默认落到当前可见页签中排序第一个的页签。
if (route.path === '/sys/approval' && !queryTab && fallbackTab) {
activeTab.value = fallbackTab.name;
if (fallbackTab.name !== 'flow') {
await router.replace({
path: '/sys/approval',
query: { tab: fallbackTab.name },
});
return;
}
}
const expectedTab = resolveTabNameByRoute();
if (visibleTabNames.value.includes(expectedTab)) {
activeTab.value = expectedTab;
} else {
if (fallbackTab && path !== fallbackTab.path) {
await router.replace(fallbackTab.path);
const fallbackQuery =
fallbackTab?.name === 'flow' ? {} : { tab: fallbackTab?.name };
const currentQueryTab = String(route.query.tab || '');
if (
fallbackTab &&
(route.path !== '/sys/approval' || currentQueryTab !== String(fallbackTab.name))
) {
await router.replace({
path: '/sys/approval',
query: fallbackQuery,
});
return;
}
activeTab.value = fallbackTab?.name || 'flow';
@@ -206,10 +257,16 @@ function handleTabChange(name: number | string) {
return;
}
const target = visibleTabs.value.find((item) => item.name === name);
if (!target || route.path === target.path) {
const nextQuery = name === 'flow' ? {} : { tab: name };
const currentQueryTab = String(route.query.tab || '');
const currentTab = currentQueryTab || 'flow';
if (!target || currentTab === name) {
return;
}
router.push(target.path);
router.replace({
path: '/sys/approval',
query: nextQuery,
});
}
function reloadCurrentTab() {