Build lightweight AI agent admin
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user