Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/bots/index.vue
陈子默 4e565aef99 feat: 收敛AI资源发布审批生命周期
- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路

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

- 调整审批管理权限模型为单入口页面加内部按钮权限
2026-04-09 17:13:54 +08:00

503 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import type { BotInfo } from '@easyflow/types';
import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import { computed, markRaw, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { $t } from '@easyflow/locales';
import { Delete, Edit, Plus, Promotion, Setting } from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElTag,
} from 'element-plus';
import { tryit } from 'radash';
import {
submitBotDeleteApproval,
submitBotOfflineApproval,
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 {
canAiResourceDelete,
canAiResourceOffline,
canAiResourcePublish,
canAiResourceRepublish,
isAiResourceApprovalPending,
resolveAiResourceDisplayStatus,
} from '#/views/ai/shared/publish-status';
import { useDictStore } from '#/store';
import Modal from './modal.vue';
interface FieldDefinition {
// 字段名称
prop: string;
// 字段标签
label: string;
// 字段类型input, number, select, radio, checkbox, switch, date, datetime
type?: 'input' | 'number';
// 是否必填
required?: boolean;
// 占位符
placeholder?: string;
}
onMounted(() => {
initDict();
getSideList();
});
const router = useRouter();
const pageDataRef = ref();
const modalRef = ref<InstanceType<typeof Modal>>();
const dictStore = useDictStore();
// 操作按钮配置
const headerButtons = [
{
key: 'create',
text: `${$t('button.create')}${$t('bot.chatAssistant')}`,
icon: markRaw(Plus),
type: 'primary',
data: { action: 'create' },
permission: '/api/v1/documentCollection/save',
},
];
function resolveNavTitle(row: BotInfo) {
return (row as Record<string, any>)?.title || row?.name || '';
}
const primaryAction: CardPrimaryAction = {
icon: Setting,
text: $t('button.setting'),
onClick(row: BotInfo) {
router.push({
path: `/ai/bots/setting/${row.id}`,
query: {
pageKey: '/ai/bots',
navTitle: resolveNavTitle(row),
},
});
},
};
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
placement: 'inline',
onClick(row: BotInfo) {
modalRef.value?.open('edit', row);
},
},
{
icon: Promotion,
text: (row: BotInfo) =>
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
? $t('button.republish')
: $t('button.publish'),
permission: '/api/v1/bot/save',
placement: 'inline',
visible: (row: BotInfo) =>
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
onClick(row: BotInfo) {
handlePublishAction(row);
},
},
{
icon: Promotion,
text: $t('button.offline'),
permission: '/api/v1/bot/save',
placement: 'menu',
visible: (row: BotInfo) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
onClick(row: BotInfo) {
handleOfflineAction(row);
},
},
{
icon: Delete,
text: $t('button.delete'),
tone: 'danger',
permission: '/api/v1/bot/remove',
placement: 'menu',
visible: (row: BotInfo) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
onClick(row: BotInfo) {
handleSubmitDeleteApproval(row);
},
},
];
function isRepublishAction(bot: BotInfo) {
return canAiResourceRepublish(bot.displayPublishStatus, bot.publishStatus);
}
const handlePublishAction = async (bot: BotInfo) => {
if (
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
) {
ElMessage.warning($t('bot.publishPendingHint'));
return;
}
try {
await ElMessageBox.confirm(
isRepublishAction(bot)
? $t('bot.submitRepublishApprovalConfirm')
: $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 handleOfflineAction = async (bot: BotInfo) => {
if (
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
) {
ElMessage.warning($t('bot.publishPendingHint'));
return;
}
try {
await ElMessageBox.confirm(
$t('bot.submitOfflineApprovalConfirm'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
const res = await submitBotOfflineApproval(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.displayPublishStatus, 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 resolvePublishStatusMetaByInstance(
displayPublishStatus?: string,
publishStatus?: string,
) {
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
case 'PUBLISHED':
return {
label: $t('bot.publishStatusPublished'),
type: 'success' as const,
};
case 'PUBLISH_PENDING':
return {
label: $t('bot.publishStatusPublishPending'),
type: 'warning' as const,
};
case 'OFFLINE_PENDING':
return {
label: $t('bot.publishStatusOfflinePending'),
type: 'warning' as const,
};
case 'OFFLINE':
return {
label: $t('bot.publishStatusOffline'),
type: 'info' 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 });
};
const handleButtonClick = () => {
modalRef.value?.open('create');
};
const fieldDefinitions = ref<FieldDefinition[]>([
{
prop: 'categoryName',
label: $t('aiWorkflowCategory.categoryName'),
type: 'input',
required: true,
placeholder: $t('aiWorkflowCategory.categoryName'),
},
{
prop: 'sortNo',
label: $t('aiWorkflowCategory.sortNo'),
type: 'number',
required: false,
placeholder: $t('aiWorkflowCategory.sortNo'),
},
]);
const formData = ref<any>({});
const dialogVisible = ref(false);
const formRef = ref<FormInstance>();
const saveLoading = ref(false);
const sideList = ref<any[]>([]);
const controlBtns = [
{
icon: Edit,
label: $t('button.edit'),
onClick(row: any) {
showControlDialog(row);
},
},
{
type: 'danger',
icon: Delete,
label: $t('button.delete'),
onClick(row: any) {
removeCategory(row);
},
},
];
const footerButton = {
icon: Plus,
label: $t('button.add'),
onClick() {
showControlDialog({});
},
};
const formRules = computed(() => {
const rules: Record<string, any[]> = {};
fieldDefinitions.value.forEach((field) => {
const fieldRules = [];
if (field.required) {
fieldRules.push({
required: true,
message: `${$t('message.required')}`,
trigger: 'blur',
});
}
if (fieldRules.length > 0) {
rules[field.prop] = fieldRules;
}
});
return rules;
});
function initDict() {
dictStore.fetchDictionary('dataStatus');
}
function changeCategory(category: any) {
pageDataRef.value.setQuery({ categoryId: category.id });
}
function showControlDialog(item: any) {
formRef.value?.resetFields();
formData.value = { ...item };
dialogVisible.value = true;
}
function removeCategory(row: any) {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
api
.post('/api/v1/botCategory/remove', { id: row.id })
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
done();
getSideList();
}
})
.catch(() => {
instance.confirmButtonLoading = false;
});
} else {
done();
}
},
}).catch(() => {});
}
function handleSubmit() {
formRef.value?.validate((valid) => {
if (valid) {
saveLoading.value = true;
const url = formData.value.id
? '/api/v1/botCategory/update'
: '/api/v1/botCategory/save';
api.post(url, formData.value).then((res) => {
saveLoading.value = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
dialogVisible.value = false;
getSideList();
}
});
}
});
}
const getSideList = async () => {
const [, res] = await tryit(api.get)('/api/v1/botCategory/visibleList', {
params: { sortKey: 'sortNo', sortType: 'asc' },
});
if (res && res.errorCode === 0) {
sideList.value = [
{
id: '',
categoryName: $t('common.allCategories'),
},
...res.data,
];
}
};
</script>
<template>
<div class="flex h-full flex-col gap-6 p-6">
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@button-click="handleButtonClick"
/>
<div class="flex flex-1 gap-6">
<PageSide
label-key="categoryName"
value-key="id"
:menus="sideList"
:control-btns="controlBtns"
:footer-button="footerButton"
@change="changeCategory"
/>
<div class="h-[calc(100vh-192px)] flex-1 overflow-auto">
<PageData
ref="pageDataRef"
page-url="/api/v1/bot/page"
:page-sizes="[12, 18, 24]"
:page-size="12"
>
<template #default="{ pageList }">
<CardList
:default-icon="defaultAvatar"
:data="pageList"
:primary-action="primaryAction"
:actions="actions"
>
<template #corner="{ item }">
<ElTag
size="small"
effect="plain"
round
:type="resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).type"
>
{{ resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label }}
</ElTag>
</template>
</CardList>
</template>
</PageData>
</div>
</div>
<!-- 创建&编辑Bot弹窗 -->
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
<EasyFlowFormModal
v-model:open="dialogVisible"
:closable="!saveLoading"
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
:confirm-loading="saveLoading"
:confirm-text="$t('button.confirm')"
:submitting="saveLoading"
@confirm="handleSubmit"
>
<ElForm
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="easyflow-modal-form easyflow-modal-form--compact"
>
<!-- 动态生成表单项 -->
<ElFormItem
v-for="field in fieldDefinitions"
:key="field.prop"
:label="field.label"
:prop="field.prop"
>
<ElInput
v-if="!field.type || field.type === 'input'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
/>
<ElInputNumber
v-else-if="field.type === 'number'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
</EasyFlowFormModal>
</div>
</template>