631 lines
18 KiB
Vue
631 lines
18 KiB
Vue
<script setup lang="ts">
|
||
import type {
|
||
AgentCreateInput,
|
||
AgentListItem,
|
||
} from '#/api/ai-platform/ai-platform';
|
||
|
||
import { onMounted, reactive, ref } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
|
||
import { Page } from '@vben/common-ui';
|
||
import {
|
||
Bot,
|
||
CirclePlus,
|
||
Download,
|
||
Edit,
|
||
Menu,
|
||
MessageSquare,
|
||
MoreVertical,
|
||
Play,
|
||
Square,
|
||
Trash2,
|
||
Upload,
|
||
} from '@vben/icons';
|
||
import { $t } from '@vben/locales';
|
||
|
||
import {
|
||
ElButton,
|
||
ElCard,
|
||
ElDialog,
|
||
ElDropdown,
|
||
ElDropdownItem,
|
||
ElDropdownMenu,
|
||
ElEmpty,
|
||
ElForm,
|
||
ElFormItem,
|
||
ElInput,
|
||
ElMessage,
|
||
ElMessageBox,
|
||
ElOption,
|
||
ElPagination,
|
||
ElSelect,
|
||
ElTag,
|
||
ElTooltip,
|
||
} from 'element-plus';
|
||
|
||
import {
|
||
createAgentApi,
|
||
deleteAgentApi,
|
||
disableAgentApi,
|
||
exportAgentConfigApi,
|
||
getAgentListApi,
|
||
publishAgentApi,
|
||
unpublishAgentMenuApi,
|
||
} from '#/api/ai-platform/ai-platform';
|
||
import { useAppContextStore } from '#/store/app-context';
|
||
|
||
import PublishToMenuDialog from './components/PublishToMenuDialog.vue';
|
||
import ImportDialog from './modules/import-dialog.vue';
|
||
|
||
defineOptions({ name: 'AgentList' });
|
||
|
||
const router = useRouter();
|
||
const appContextStore = useAppContextStore();
|
||
|
||
// 搜索关键词
|
||
const searchKeyword = ref('');
|
||
|
||
// 列表数据
|
||
const loading = ref(false);
|
||
const agentList = ref<AgentListItem[]>([]);
|
||
|
||
// 分页
|
||
const pagination = reactive({
|
||
current: 1,
|
||
pageSize: 12,
|
||
total: 0,
|
||
});
|
||
|
||
// 发布到菜单弹窗
|
||
const showPublishToMenuDialog = ref(false);
|
||
const publishingAgent = ref<AgentListItem | null>(null);
|
||
|
||
// 导入弹窗
|
||
const showImportDialog = ref(false);
|
||
|
||
// 创建对话框
|
||
const createDialogVisible = ref(false);
|
||
const createForm = reactive<AgentCreateInput>({
|
||
name: '',
|
||
code: '',
|
||
description: '',
|
||
mode: 'autonomous',
|
||
is_global: false,
|
||
});
|
||
const createLoading = ref(false);
|
||
|
||
// 获取列表
|
||
async function fetchList() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getAgentListApi({
|
||
page: pagination.current,
|
||
pageSize: pagination.pageSize,
|
||
name: searchKeyword.value || undefined,
|
||
applicationId: appContextStore.currentApp?.id,
|
||
});
|
||
agentList.value = res.items;
|
||
pagination.total = res.total;
|
||
} catch (error) {
|
||
console.error('获取智能体列表失败:', error);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// 搜索
|
||
function handleSearch() {
|
||
pagination.current = 1;
|
||
fetchList();
|
||
}
|
||
|
||
// 分页变化
|
||
function handlePageChange(page: number) {
|
||
pagination.current = page;
|
||
fetchList();
|
||
}
|
||
|
||
// 每页条数变化
|
||
function handleSizeChange(size: number) {
|
||
pagination.pageSize = size;
|
||
pagination.current = 1;
|
||
fetchList();
|
||
}
|
||
|
||
// 打开创建对话框
|
||
function openCreateDialog() {
|
||
createForm.name = '';
|
||
createForm.code = '';
|
||
createForm.description = '';
|
||
createForm.mode = 'autonomous';
|
||
createForm.is_global = false;
|
||
createDialogVisible.value = true;
|
||
}
|
||
|
||
// 创建智能体
|
||
async function handleCreate() {
|
||
if (!createForm.name || !createForm.code) {
|
||
ElMessage.warning($t('ai-platform.agent.fillNameAndCode'));
|
||
return;
|
||
}
|
||
|
||
// 验证 code 格式
|
||
if (!/^[a-z]\w*$/i.test(createForm.code)) {
|
||
ElMessage.warning($t('ai-platform.agent.codeFormatError'));
|
||
return;
|
||
}
|
||
|
||
createLoading.value = true;
|
||
try {
|
||
const agent = await createAgentApi({
|
||
...createForm,
|
||
application_id: appContextStore.currentApp?.id,
|
||
});
|
||
ElMessage.success($t('ai-platform.agent.createSuccess'));
|
||
createDialogVisible.value = false;
|
||
// 跳转到编辑页(支持子应用模式)
|
||
router.push(
|
||
appContextStore.getContextPath(`/ai-platform/agent/editor/${agent.id}`),
|
||
);
|
||
} catch (error: any) {
|
||
ElMessage.error(error.message || $t('ai-platform.agent.createFailed'));
|
||
} finally {
|
||
createLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 编辑智能体
|
||
function handleEdit(agent: AgentListItem) {
|
||
router.push(
|
||
appContextStore.getContextPath(`/ai-platform/agent/editor/${agent.id}`),
|
||
);
|
||
}
|
||
|
||
// 对话
|
||
function handleChat(agent: AgentListItem) {
|
||
router.push(
|
||
appContextStore.getContextPath(`/ai-platform/agent/chat/${agent.id}`),
|
||
);
|
||
}
|
||
|
||
// 发布
|
||
async function handlePublish(agent: AgentListItem) {
|
||
try {
|
||
await publishAgentApi(agent.id);
|
||
ElMessage.success($t('ai-platform.agent.publishSuccess'));
|
||
fetchList();
|
||
} catch (error: any) {
|
||
ElMessage.error(error.message || $t('ai-platform.agent.publishFailed'));
|
||
}
|
||
}
|
||
|
||
// 停用
|
||
async function handleDisable(agent: AgentListItem) {
|
||
try {
|
||
await disableAgentApi(agent.id);
|
||
ElMessage.success($t('ai-platform.agent.disableSuccess'));
|
||
fetchList();
|
||
} catch (error: any) {
|
||
ElMessage.error(error.message || $t('ai-platform.agent.operationFailed'));
|
||
}
|
||
}
|
||
|
||
// 删除
|
||
async function handleDelete(agent: AgentListItem) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
$t('ai-platform.agent.deleteConfirm', { name: agent.name }),
|
||
$t('ai-platform.agent.deleteTitle'),
|
||
{ type: 'warning' },
|
||
);
|
||
await deleteAgentApi(agent.id);
|
||
ElMessage.success($t('ai-platform.agent.deleteSuccess'));
|
||
fetchList();
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') {
|
||
ElMessage.error(error.message || $t('ai-platform.agent.deleteFailed'));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取状态标签类型
|
||
function getStatusType(status: string) {
|
||
switch (status) {
|
||
case 'disabled': {
|
||
return 'danger';
|
||
}
|
||
case 'published': {
|
||
return 'success';
|
||
}
|
||
default: {
|
||
return 'info';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取状态文本
|
||
function getStatusText(status: string) {
|
||
switch (status) {
|
||
case 'disabled': {
|
||
return $t('ai-platform.agent.status.disabled');
|
||
}
|
||
case 'published': {
|
||
return $t('ai-platform.agent.status.published');
|
||
}
|
||
default: {
|
||
return $t('ai-platform.agent.status.draft');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取模式文本
|
||
function getModeText(mode: string) {
|
||
return mode === 'autonomous'
|
||
? $t('ai-platform.agent.mode.autonomous')
|
||
: $t('ai-platform.agent.mode.dialogFlow');
|
||
}
|
||
|
||
// 发布到菜单
|
||
function handlePublishToMenu(agent: AgentListItem) {
|
||
publishingAgent.value = agent;
|
||
showPublishToMenuDialog.value = true;
|
||
}
|
||
|
||
// 发布到菜单成功回调
|
||
function handlePublishedToMenu() {
|
||
fetchList();
|
||
}
|
||
|
||
// 取消发布菜单
|
||
async function handleUnpublishMenu(agent: AgentListItem) {
|
||
try {
|
||
await unpublishAgentMenuApi(agent.id);
|
||
ElMessage.success($t('ai-platform.agent.unpublishMenuSuccess'));
|
||
fetchList();
|
||
} catch (error: any) {
|
||
ElMessage.error(error.message || $t('ai-platform.agent.operationFailed'));
|
||
}
|
||
}
|
||
|
||
function downloadBlob(blob: Blob, filename: string) {
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.append(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
async function handleExport(agent: AgentListItem) {
|
||
try {
|
||
const blob = await exportAgentConfigApi(agent.id);
|
||
downloadBlob(blob, `${agent.code}.json`);
|
||
ElMessage.success($t('ai-platform.agent.importExport.exportSuccess'));
|
||
} catch {
|
||
ElMessage.error($t('ai-platform.agent.importExport.exportFailed'));
|
||
}
|
||
}
|
||
|
||
function handleImported() {
|
||
fetchList();
|
||
}
|
||
|
||
// 处理更多菜单命令
|
||
function handleCommand(command: string, agent: AgentListItem) {
|
||
switch (command) {
|
||
case 'export': {
|
||
handleExport(agent);
|
||
break;
|
||
}
|
||
case 'chat': {
|
||
handleChat(agent);
|
||
break;
|
||
}
|
||
case 'disable': {
|
||
handleDisable(agent);
|
||
break;
|
||
}
|
||
case 'publish': {
|
||
handlePublish(agent);
|
||
break;
|
||
}
|
||
case 'publishToMenu': {
|
||
handlePublishToMenu(agent);
|
||
break;
|
||
}
|
||
case 'unpublishMenu': {
|
||
handleUnpublishMenu(agent);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchList();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="agent-list-page">
|
||
<Page auto-content-height v-loading="loading">
|
||
<template #title>
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-4">
|
||
<ElInput
|
||
v-model="searchKeyword"
|
||
:placeholder="$t('ai-platform.agent.searchPlaceholder')"
|
||
clearable
|
||
class="w-64"
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
<ElButton @click="handleSearch">
|
||
{{ $t('ai-platform.agent.search') }}
|
||
</ElButton>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<ElButton @click="showImportDialog = true">
|
||
<Upload class="mr-1 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.importExport.import') }}
|
||
</ElButton>
|
||
<ElButton type="primary" @click="openCreateDialog">
|
||
<CirclePlus class="mr-1 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.create') }}
|
||
</ElButton>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 智能体卡片列表 -->
|
||
<div
|
||
v-if="agentList.length > 0"
|
||
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
|
||
>
|
||
<ElCard
|
||
v-for="agent in agentList"
|
||
:key="agent.id"
|
||
class="agent-card cursor-pointer transition-shadow"
|
||
shadow="hover"
|
||
:body-style="{ padding: '0' }"
|
||
style="border: none"
|
||
@click="handleEdit(agent)"
|
||
>
|
||
<div class="p-4">
|
||
<!-- 头部:图标 + 右侧信息区 -->
|
||
<div class="mb-4 flex gap-3">
|
||
<div
|
||
class="bg-primary/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg"
|
||
>
|
||
<Bot class="text-primary h-5 w-5" />
|
||
</div>
|
||
<div class="min-w-0 flex-1">
|
||
<!-- name + 操作 -->
|
||
<div class="flex items-center justify-between">
|
||
<div class="min-w-0 flex-1 truncate text-sm font-medium">
|
||
{{ agent.name }}
|
||
</div>
|
||
<div
|
||
class="flex flex-shrink-0 items-center -space-x-1"
|
||
@click.stop
|
||
>
|
||
<ElTooltip
|
||
:content="$t('ai-platform.agent.edit')"
|
||
placement="top"
|
||
>
|
||
<ElButton text size="small" @click="handleEdit(agent)">
|
||
<Edit class="h-3.5 w-3.5" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
<ElTooltip
|
||
:content="$t('ai-platform.agent.delete')"
|
||
placement="top"
|
||
>
|
||
<ElButton text size="small" @click="handleDelete(agent)">
|
||
<Trash2 class="h-3.5 w-3.5" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
<ElDropdown
|
||
trigger="click"
|
||
@command="(cmd: string) => handleCommand(cmd, agent)"
|
||
>
|
||
<ElButton text size="small">
|
||
<MoreVertical class="h-3.5 w-3.5" />
|
||
</ElButton>
|
||
<template #dropdown>
|
||
<ElDropdownMenu>
|
||
<ElDropdownItem
|
||
v-if="agent.status === 'published'"
|
||
command="chat"
|
||
>
|
||
<MessageSquare class="mr-2 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.chat') }}
|
||
</ElDropdownItem>
|
||
<ElDropdownItem
|
||
v-if="agent.status === 'draft'"
|
||
command="publish"
|
||
>
|
||
<Play class="mr-2 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.publish') }}
|
||
</ElDropdownItem>
|
||
<ElDropdownItem
|
||
v-if="
|
||
agent.status === 'published' && !agent.has_menu
|
||
"
|
||
command="publishToMenu"
|
||
>
|
||
<Menu class="mr-2 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.publishToMenu') }}
|
||
</ElDropdownItem>
|
||
<ElDropdownItem
|
||
v-if="
|
||
agent.status === 'published' && agent.has_menu
|
||
"
|
||
command="unpublishMenu"
|
||
>
|
||
<Square class="mr-2 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.unpublishMenu') }}
|
||
</ElDropdownItem>
|
||
<ElDropdownItem command="export">
|
||
<Download class="mr-2 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.importExport.export') }}
|
||
</ElDropdownItem>
|
||
<ElDropdownItem
|
||
v-if="agent.status === 'published'"
|
||
command="disable"
|
||
>
|
||
<Square class="mr-2 h-4 w-4" />
|
||
{{ $t('ai-platform.agent.disable') }}
|
||
</ElDropdownItem>
|
||
</ElDropdownMenu>
|
||
</template>
|
||
</ElDropdown>
|
||
</div>
|
||
</div>
|
||
<!-- code -->
|
||
<div class="text-muted-foreground font-mono text-xs">
|
||
{{ agent.code }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 描述 -->
|
||
<div
|
||
class="text-muted-foreground mb-4 line-clamp-1 min-h-[18px] text-xs"
|
||
>
|
||
{{ agent.description || $t('ai-platform.agent.noDescription') }}
|
||
</div>
|
||
<!-- 标签 -->
|
||
<div class="mb-4 flex items-center justify-between">
|
||
<div class="flex gap-1">
|
||
<ElTag size="small" :type="getStatusType(agent.status) as any">
|
||
{{ getStatusText(agent.status) }}
|
||
</ElTag>
|
||
<ElTag size="small" type="info">
|
||
{{ getModeText(agent.mode) }}
|
||
</ElTag>
|
||
</div>
|
||
</div>
|
||
<!-- 应用名称 + 创建时间 -->
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-muted-foreground text-xs">
|
||
{{ agent.model_name || '未配置模型' }} ·
|
||
{{ agent.conversation_count || 0 }} 次对话
|
||
</div>
|
||
<span class="text-muted-foreground text-xs">
|
||
{{ agent.sys_create_datetime }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</ElCard>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<ElEmpty v-else :description="$t('ai-platform.agent.empty')" />
|
||
|
||
<!-- 分页 -->
|
||
<template #footer>
|
||
<div class="flex w-full items-center justify-end">
|
||
<ElPagination
|
||
v-model:current-page="pagination.current"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[12, 24, 36, 48]"
|
||
:pager-count="7"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
background
|
||
size="small"
|
||
@current-change="handlePageChange"
|
||
@size-change="handleSizeChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</Page>
|
||
|
||
<ImportDialog v-model="showImportDialog" @imported="handleImported" />
|
||
|
||
<!-- 发布到菜单弹窗 -->
|
||
<PublishToMenuDialog
|
||
v-if="publishingAgent"
|
||
v-model="showPublishToMenuDialog"
|
||
:agent-id="publishingAgent.id"
|
||
:agent-name="publishingAgent.name"
|
||
:agent-code="publishingAgent.code"
|
||
@published="handlePublishedToMenu"
|
||
/>
|
||
|
||
<!-- 创建对话框 -->
|
||
<ElDialog
|
||
v-model="createDialogVisible"
|
||
:title="$t('ai-platform.agent.create')"
|
||
width="500px"
|
||
>
|
||
<ElForm label-width="80px">
|
||
<ElFormItem :label="$t('ai-platform.agent.form.name')" required>
|
||
<ElInput
|
||
v-model="createForm.name"
|
||
:placeholder="$t('ai-platform.agent.form.namePlaceholder')"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem :label="$t('ai-platform.agent.form.code')" required>
|
||
<ElInput
|
||
v-model="createForm.code"
|
||
:placeholder="$t('ai-platform.agent.form.codePlaceholder')"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem :label="$t('ai-platform.agent.form.mode')">
|
||
<ElSelect v-model="createForm.mode" style="width: 100%">
|
||
<ElOption
|
||
:label="$t('ai-platform.agent.mode.autonomousLabel')"
|
||
value="autonomous"
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<div>
|
||
<div>{{ $t('ai-platform.agent.mode.autonomousLabel') }}</div>
|
||
<div class="text-muted-foreground text-xs">
|
||
{{ $t('ai-platform.agent.mode.autonomousDesc') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ElOption>
|
||
<ElOption
|
||
:label="$t('ai-platform.agent.mode.dialogFlowLabel')"
|
||
value="dialog_flow"
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<div>
|
||
<div>{{ $t('ai-platform.agent.mode.dialogFlowLabel') }}</div>
|
||
<div class="text-muted-foreground text-xs">
|
||
{{ $t('ai-platform.agent.mode.dialogFlowDesc') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ElOption>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem :label="$t('ai-platform.agent.form.description')">
|
||
<ElInput
|
||
v-model="createForm.description"
|
||
:rows="3"
|
||
:placeholder="$t('ai-platform.agent.form.descriptionPlaceholder')"
|
||
type="textarea"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="createDialogVisible = false">
|
||
{{ $t('ai-platform.agent.cancel') }}
|
||
</ElButton>
|
||
<ElButton :loading="createLoading" type="primary" @click="handleCreate">
|
||
{{ $t('ai-platform.agent.create') }}
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.agent-card :deep(.el-card__body) {
|
||
padding: 0;
|
||
}
|
||
</style>
|