Files
ai-agent-admin/web/apps/web-ele/src/views/ai-platform/agent/index.vue
T

631 lines
18 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 {
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>