feat: 归档L03与L09审批发布能力

- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象

- 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
2026-04-07 14:41:52 +08:00
parent 7e7c236c2a
commit 3f128e977a
138 changed files with 13035 additions and 346 deletions

View File

@@ -14,7 +14,7 @@ import { useRouter } from 'vue-router';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { $t } from '@easyflow/locales';
import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue';
import { Delete, Edit, Plus, Promotion, Setting } from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
@@ -22,16 +22,22 @@ import {
ElInputNumber,
ElMessage,
ElMessageBox,
ElTag,
} from 'element-plus';
import { tryit } from 'radash';
import { removeBotFromId } from '#/api';
import { submitBotDeleteApproval, submitBotPublishApproval } from '#/api';
import { api } from '#/api/request';
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CardList from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import {
isAiResourceApprovalPending,
isAiResourcePublished,
normalizeAiPublishStatus,
} from '#/views/ai/shared/publish-status';
import { useDictStore } from '#/store';
import Modal from './modal.vue';
@@ -97,37 +103,101 @@ const actions: ActionButton[] = [
},
},
{
icon: Delete,
text: $t('button.delete'),
tone: 'danger',
permission: '/api/v1/bot/remove',
icon: Promotion,
text: (row: BotInfo) =>
isAiResourcePublished(row.publishStatus)
? $t('button.republish')
: $t('button.submitPublishApproval'),
permission: '/api/v1/bot/save',
placement: 'inline',
onClick(row: BotInfo) {
removeBot(row);
handleSubmitPublishApproval(row);
},
},
{
icon: Delete,
text: $t('button.submitDeleteApproval'),
tone: 'danger',
permission: '/api/v1/bot/remove',
placement: 'menu',
onClick(row: BotInfo) {
handleSubmitDeleteApproval(row);
},
},
];
const removeBot = async (bot: BotInfo) => {
const [action] = await tryit(ElMessageBox.confirm)(
$t('message.deleteAlert'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
},
);
if (!action) {
const [err, res] = await tryit(removeBotFromId)(bot.id);
if (!err && res.errorCode === 0) {
ElMessage.success($t('message.deleteOkMessage'));
pageDataRef.value.setQuery({});
}
const handleSubmitPublishApproval = async (bot: BotInfo) => {
if (isAiResourceApprovalPending(bot.publishStatus)) {
ElMessage.warning($t('bot.publishPendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('bot.submitPublishApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'info',
},
);
} catch {
return;
}
const res = await submitBotPublishApproval(String(bot.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
pageDataRef.value?.reload?.();
}
};
const handleSubmitDeleteApproval = async (bot: BotInfo) => {
if (isAiResourceApprovalPending(bot.publishStatus)) {
ElMessage.warning($t('bot.deletePendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('bot.submitDeleteApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
const res = await submitBotDeleteApproval(String(bot.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
pageDataRef.value?.reload?.();
}
};
function resolvePublishStatusMeta(status?: string) {
switch (normalizeAiPublishStatus(status)) {
case 'PUBLISHED':
return {
label: $t('bot.publishStatusPublished'),
type: 'success' as const,
};
case 'PUBLISH_PENDING':
return {
label: $t('bot.publishStatusPublishPending'),
type: 'warning' as const,
};
case 'DELETE_PENDING':
return {
label: $t('bot.publishStatusDeletePending'),
type: 'danger' as const,
};
default:
return {
label: $t('bot.publishStatusDraft'),
type: 'info' as const,
};
}
}
const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
@@ -302,7 +372,18 @@ const getSideList = async () => {
:data="pageList"
:primary-action="primaryAction"
:actions="actions"
/>
>
<template #corner="{ item }">
<ElTag
size="small"
effect="plain"
round
:type="resolvePublishStatusMeta(item.publishStatus).type"
>
{{ resolvePublishStatusMeta(item.publishStatus).label }}
</ElTag>
</template>
</CardList>
</template>
</PageData>
</div>

View File

@@ -34,12 +34,15 @@ import {
ElSkeleton,
ElSlider,
ElSwitch,
ElTag,
ElTooltip,
} from 'element-plus';
import { tryit } from 'radash';
import {
getPerQuestions,
submitBotDeleteApproval,
submitBotPublishApproval,
updateBotApi,
updateBotOptions,
updateLlmId,
@@ -52,6 +55,12 @@ import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import {
isAiResourceApprovalPending,
isAiResourceExternallyVisible,
isAiResourcePublished,
normalizeAiPublishStatus,
} from '#/views/ai/shared/publish-status';
interface SelectedMcpTool {
name: string;
@@ -154,6 +163,46 @@ const publicChatUrl = computed(() => {
const publicChatEmbedUrl = computed(() => {
return buildPublicChatUrl(true);
});
const publishStatusMeta = computed<{
description: string;
label: string;
type: 'danger' | 'info' | 'success' | 'warning';
}>(() => {
switch (normalizeAiPublishStatus(botInfo.value?.publishStatus)) {
case 'PUBLISHED':
return {
label: $t('bot.publishStatusPublished'),
type: 'success',
description: $t('bot.publishStatusPublishedDesc'),
};
case 'PUBLISH_PENDING':
return {
label: $t('bot.publishStatusPublishPending'),
type: 'warning',
description: $t('bot.publishStatusPublishPendingDesc'),
};
case 'DELETE_PENDING':
return {
label: $t('bot.publishStatusDeletePending'),
type: 'danger',
description: $t('bot.publishStatusDeletePendingDesc'),
};
default:
return {
label: $t('bot.publishStatusDraft'),
type: 'info',
description: $t('bot.publishStatusDraftDesc'),
};
}
});
const canUsePublicAccess = computed(() =>
isAiResourceExternallyVisible(botInfo.value?.publishStatus),
);
const publishPrimaryActionLabel = computed(() =>
isAiResourcePublished(botInfo.value?.publishStatus)
? $t('button.republish')
: $t('button.submitPublishApproval'),
);
const iframeCode = computed(() => {
if (!publicChatEmbedUrl.value) {
return '';
@@ -494,6 +543,10 @@ const handleCopyValue = async (value: string, successMessage?: string) => {
};
const openPublicPage = () => {
if (!canUsePublicAccess.value) {
ElMessage.warning($t('bot.publishRequiredHint'));
return;
}
if (!publicChatUrl.value) {
ElMessage.warning($t('bot.chatPublishBaseUrlMissing'));
return;
@@ -767,6 +820,64 @@ const handleDeletePresetQuestion = (item: any) => {
const handlePublishWx = () => {
publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {});
};
const handleSubmitPublishApproval = async () => {
if (!botInfo.value) {
return;
}
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
ElMessage.warning($t('bot.publishPendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('bot.submitPublishApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'info',
},
);
} catch {
return;
}
const res = await submitBotPublishApproval(String(botInfo.value.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
getBotDetail();
} else {
ElMessage.error(res.message || $t('message.saveFailMessage'));
}
};
const handleSubmitDeleteApproval = async () => {
if (!botInfo.value) {
return;
}
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
ElMessage.warning($t('bot.deletePendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('bot.submitDeleteApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
const res = await submitBotDeleteApproval(String(botInfo.value.id));
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveOkMessage'));
getBotDetail();
} else {
ElMessage.error(res.message || $t('message.saveFailMessage'));
}
};
const handleUpdatePublishWx = () => {
api
.post('/api/v1/bot/updateOptions', {
@@ -1352,6 +1463,44 @@ const handleBasicInfoChange = async (
<h1 class="text-base font-medium">
{{ $t('bot.publish') }}
</h1>
<div class="publish-summary-card">
<div class="publish-summary-main">
<div class="publish-summary-label">
{{ $t('bot.publishStatusLabel') }}
</div>
<div class="publish-summary-row">
<ElTag :type="publishStatusMeta.type" effect="plain" round>
{{ publishStatusMeta.label }}
</ElTag>
<span
v-if="botInfo?.currentApprovalInstanceId"
class="publish-summary-instance"
>
#{{ botInfo.currentApprovalInstanceId }}
</span>
</div>
<p class="publish-summary-desc">
{{ publishStatusMeta.description }}
</p>
</div>
<div class="publish-summary-actions">
<ElButton
type="primary"
:disabled="!hasSavePermission"
@click="handleSubmitPublishApproval"
>
{{ publishPrimaryActionLabel }}
</ElButton>
<ElButton
plain
type="danger"
:disabled="!hasSavePermission"
@click="handleSubmitDeleteApproval"
>
{{ $t('button.submitDeleteApproval') }}
</ElButton>
</div>
</div>
<div class="flex w-full flex-col justify-between rounded-lg">
<ElCollapse expand-icon-position="left">
<ElCollapseItem :title="$t('bot.postToWeChatOfficialAccount')">
@@ -1428,12 +1577,23 @@ const handleBasicInfoChange = async (
<label class="publish-external-label">
{{ $t('bot.chatExternalLink') }}
</label>
<div
v-if="!canUsePublicAccess"
class="publish-external-alert"
>
<ElAlert
:title="$t('bot.publishRequiredHint')"
type="info"
:closable="false"
/>
</div>
<ElInput :model-value="publicChatUrl" readonly />
<div class="publish-external-actions">
<ElButton
size="small"
type="primary"
plain
:disabled="!canUsePublicAccess"
@click="handleCopyValue(publicChatUrl)"
>
<ElIcon class="mr-1">
@@ -1441,7 +1601,12 @@ const handleBasicInfoChange = async (
</ElIcon>
{{ $t('bot.copyLink') }}
</ElButton>
<ElButton size="small" type="primary" @click="openPublicPage">
<ElButton
size="small"
type="primary"
:disabled="!canUsePublicAccess"
@click="openPublicPage"
>
<ElIcon class="mr-1">
<Link />
</ElIcon>
@@ -1477,6 +1642,7 @@ const handleBasicInfoChange = async (
<ElButton
size="small"
plain
:disabled="!canUsePublicAccess"
@click="handleCopyValue(iframeCode)"
>
<ElIcon class="mr-1">
@@ -1511,6 +1677,7 @@ const handleBasicInfoChange = async (
width="730"
ref="knowledgeDataRef"
page-url="/api/v1/documentCollection/page"
:extra-query-params="{ publishedOnly: true }"
@get-data="confirmUpdateAiBotKnowledge"
/>
@@ -1520,6 +1687,7 @@ const handleBasicInfoChange = async (
width="730"
ref="workflowDataRef"
page-url="/api/v1/workflow/page"
:extra-query-params="{ publishedOnly: true }"
@get-data="confirmUpdateAiBotWorkflow"
/>
@@ -1643,6 +1811,58 @@ const handleBasicInfoChange = async (
background-color: var(--bot-collapse-itme-back);
}
.publish-summary-card {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
padding: 16px;
background: hsl(var(--surface-subtle) / 78%);
border: 1px solid hsl(var(--line-subtle));
border-radius: 16px;
}
.publish-summary-main {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
min-width: 220px;
}
.publish-summary-label {
font-size: 13px;
font-weight: 600;
color: hsl(var(--muted-foreground));
}
.publish-summary-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.publish-summary-instance {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.publish-summary-desc {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: hsl(var(--text-secondary));
}
.publish-summary-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.publish-wx {
display: flex;
align-items: center;