- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路 - 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览 - 调整审批管理权限模型为单入口页面加内部按钮权限
503 lines
13 KiB
Vue
503 lines
13 KiB
Vue
<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>
|