Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,642 @@
<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,
ElSwitch,
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 isMainApp = !appContextStore.currentApp?.id;
// 搜索关键词
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">
<span v-if="agent.application_name">{{
agent.application_name
}}</span>
<span v-else>{{ $t('ai-platform.agent.mainApp') }}</span>
</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>
<ElFormItem
v-if="isMainApp"
:label="$t('ai-platform.agent.form.globalVisible')"
>
<ElSwitch v-model="createForm.is_global" />
</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>