perf: defer route prefetch and enrich workbench
This commit is contained in:
@@ -111,6 +111,10 @@ function setupCommonGuard(router: Router) {
|
|||||||
if (preferences.transition.progress) {
|
if (preferences.transition.progress) {
|
||||||
stopProgress();
|
stopProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useAccessStore().accessToken) {
|
||||||
|
prefetchAiPlatformPages(to.path);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +202,6 @@ function setupAccessGuard(router: Router) {
|
|||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
accessStore.setIsAccessChecked(true);
|
||||||
prefetchAiPlatformPages();
|
|
||||||
|
|
||||||
// 子应用根路径重定向(/app/hr -> /app/hr/xxx)
|
// 子应用根路径重定向(/app/hr -> /app/hr/xxx)
|
||||||
if (appContextStore.isSubApp) {
|
if (appContextStore.isSubApp) {
|
||||||
|
|||||||
@@ -18,25 +18,51 @@ type NavigatorWithConnection = Navigator & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let prefetched = false;
|
const prefetchedGroups = new Set<string>();
|
||||||
|
|
||||||
const corePrefetchTasks = [
|
type PrefetchTask = {
|
||||||
() => import('#/views/_core/user/index.vue'),
|
key: string;
|
||||||
() => import('#/views/_core/menu/index.vue'),
|
load: () => Promise<unknown>;
|
||||||
() => import('#/views/_core/role/index.vue'),
|
};
|
||||||
|
|
||||||
|
const corePrefetchTasks: PrefetchTask[] = [
|
||||||
|
{ key: 'system-user', load: () => import('#/views/_core/user/index.vue') },
|
||||||
|
{ key: 'system-menu', load: () => import('#/views/_core/menu/index.vue') },
|
||||||
|
{ key: 'system-role', load: () => import('#/views/_core/role/index.vue') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const aiInteractivePrefetchTasks = [
|
const aiInteractivePrefetchTasks: PrefetchTask[] = [
|
||||||
() => import('#/views/_core/agent-chat/index.vue'),
|
{
|
||||||
() => import('#/components/ai-chat-panel/AiChatPanel.vue'),
|
key: 'agent-chat',
|
||||||
|
load: () => import('#/views/_core/agent-chat/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ai-chat-panel',
|
||||||
|
load: () => import('#/components/ai-chat-panel/AiChatPanel.vue'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const aiManagementPrefetchTasks = [
|
const aiManagementPrefetchTasks: PrefetchTask[] = [
|
||||||
() => import('#/views/ai-platform/agent/index.vue'),
|
{
|
||||||
() => import('#/views/ai-platform/workflow/index.vue'),
|
key: 'ai-agent',
|
||||||
() => import('#/views/ai-platform/model/index.vue'),
|
load: () => import('#/views/ai-platform/agent/index.vue'),
|
||||||
() => import('#/views/ai-platform/knowledge/index.vue'),
|
},
|
||||||
() => import('#/views/ai-platform/workflow-runs/index.vue'),
|
{
|
||||||
|
key: 'ai-workflow',
|
||||||
|
load: () => import('#/views/ai-platform/workflow/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ai-model',
|
||||||
|
load: () => import('#/views/ai-platform/model/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ai-knowledge',
|
||||||
|
load: () => import('#/views/ai-platform/knowledge/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ai-workflow-runs',
|
||||||
|
load: () => import('#/views/ai-platform/workflow-runs/index.vue'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function scheduleIdle(callback: () => void) {
|
function scheduleIdle(callback: () => void) {
|
||||||
@@ -63,38 +89,65 @@ function canPrefetch() {
|
|||||||
return !/(^|-)2g$/.test(connection?.effectiveType || '');
|
return !/(^|-)2g$/.test(connection?.effectiveType || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTasksSequentially(tasks: Array<() => Promise<unknown>>) {
|
async function runTasksSequentially(tasks: PrefetchTask[]) {
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
if (!canPrefetch()) {
|
if (!canPrefetch()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await task().catch(() => undefined);
|
if (prefetchedGroups.has(task.key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prefetchedGroups.add(task.key);
|
||||||
|
await task.load().catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefetchAiPlatformPages() {
|
function delayedPrefetch(tasks: PrefetchTask[], delay: number) {
|
||||||
if (prefetched || typeof window === 'undefined') {
|
if (!tasks.length || typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
prefetched = true;
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
scheduleIdle(() => {
|
scheduleIdle(() => {
|
||||||
void runTasksSequentially(corePrefetchTasks);
|
void runTasksSequentially(tasks);
|
||||||
});
|
});
|
||||||
}, 800);
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
window.setTimeout(() => {
|
function isSystemRoute(path: string) {
|
||||||
scheduleIdle(() => {
|
return path.startsWith('/system/') || path.startsWith('/message/');
|
||||||
void runTasksSequentially(aiInteractivePrefetchTasks);
|
}
|
||||||
});
|
|
||||||
}, 4000);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
function isAiPlatformRoute(path: string) {
|
||||||
scheduleIdle(() => {
|
return path.startsWith('/ai-platform/');
|
||||||
void runTasksSequentially(aiManagementPrefetchTasks);
|
}
|
||||||
});
|
|
||||||
}, 9000);
|
function isAgentChatRoute(path: string) {
|
||||||
|
return path.startsWith('/agent-chat/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchAiPlatformPages(currentPath = '') {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSystemRoute(currentPath)) {
|
||||||
|
delayedPrefetch(corePrefetchTasks, 6000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAgentChatRoute(currentPath)) {
|
||||||
|
delayedPrefetch(aiInteractivePrefetchTasks, 1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAiPlatformRoute(currentPath)) {
|
||||||
|
delayedPrefetch(aiManagementPrefetchTasks, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delayedPrefetch(corePrefetchTasks, 7000);
|
||||||
|
delayedPrefetch(aiInteractivePrefetchTasks, 12_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { prefetchAiPlatformPages };
|
export { prefetchAiPlatformPages };
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { UserAnnouncement } from '#/api/core/announcement';
|
||||||
|
import type { Message } from '#/api/core/message';
|
||||||
import type {
|
import type {
|
||||||
WorkbenchProjectItem,
|
RealtimeStats,
|
||||||
WorkbenchQuickNavItem,
|
ServerMonitorResponse,
|
||||||
WorkbenchTodoItem,
|
} from '#/api/core/server-monitor';
|
||||||
WorkbenchTrendItem,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WorkbenchHeader,
|
Bell,
|
||||||
WorkbenchProject,
|
|
||||||
WorkbenchQuickNav,
|
|
||||||
WorkbenchTodo,
|
|
||||||
WorkbenchTrends,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
import {
|
|
||||||
Bot,
|
Bot,
|
||||||
|
Brain,
|
||||||
Database,
|
Database,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
History,
|
History,
|
||||||
@@ -26,240 +21,515 @@ import {
|
|||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
Menu,
|
Menu,
|
||||||
|
MessageSquareText,
|
||||||
|
Play,
|
||||||
|
Send,
|
||||||
|
Server,
|
||||||
Settings,
|
Settings,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Users,
|
Users,
|
||||||
} from '@vben/icons';
|
} from '@vben/icons';
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import { useUserStore } from '@vben/stores';
|
import { useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
|
||||||
|
import {
|
||||||
|
getUserAnnouncementListApi,
|
||||||
|
getUnreadAnnouncementCountApi,
|
||||||
|
} from '#/api/core/announcement';
|
||||||
|
import {
|
||||||
|
getMessageListApi,
|
||||||
|
getUnreadCountApi,
|
||||||
|
} from '#/api/core/message';
|
||||||
|
import {
|
||||||
|
getRealtimeStatsApi,
|
||||||
|
getServerOverviewApi,
|
||||||
|
} from '#/api/core/server-monitor';
|
||||||
|
import {
|
||||||
|
getAgentListApi,
|
||||||
|
getWorkflowListApi,
|
||||||
|
} from '#/api/ai-platform/ai-platform';
|
||||||
|
|
||||||
defineOptions({ name: 'DashboardWorkspace' });
|
defineOptions({ name: 'DashboardWorkspace' });
|
||||||
|
|
||||||
|
type IconType = string | typeof Bot;
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
color: string;
|
||||||
|
desc: string;
|
||||||
|
icon: IconType;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricItem {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const now = ref(new Date());
|
||||||
|
const announcements = ref<UserAnnouncement[]>([]);
|
||||||
|
const messages = ref<Message[]>([]);
|
||||||
|
const unreadAnnouncements = ref(0);
|
||||||
|
const unreadMessages = ref(0);
|
||||||
|
const agentCount = ref(0);
|
||||||
|
const workflowCount = ref(0);
|
||||||
|
const serverOverview = ref<null | ServerMonitorResponse>(null);
|
||||||
|
const realtimeStats = ref<null | RealtimeStats>(null);
|
||||||
|
const loading = ref({
|
||||||
|
announcements: false,
|
||||||
|
messages: false,
|
||||||
|
server: false,
|
||||||
|
});
|
||||||
|
|
||||||
const headerStats = [
|
let timer: ReturnType<typeof setInterval> | undefined;
|
||||||
{ label: '智能体', value: '18' },
|
|
||||||
{ label: '工作流', value: '12' },
|
|
||||||
{ label: '运行事件', value: '12.8w' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const projectItems: WorkbenchProjectItem[] = [
|
const displayName = computed(
|
||||||
|
() => userStore.userInfo?.realName || userStore.userInfo?.username || '管理员',
|
||||||
|
);
|
||||||
|
|
||||||
|
const greeting = computed(() => {
|
||||||
|
const hour = now.value.getHours();
|
||||||
|
if (hour < 6) return '夜深了';
|
||||||
|
if (hour < 11) return '早上好';
|
||||||
|
if (hour < 14) return '中午好';
|
||||||
|
if (hour < 18) return '下午好';
|
||||||
|
return '晚上好';
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentTime = computed(() =>
|
||||||
|
now.value.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentDate = computed(() =>
|
||||||
|
now.value.toLocaleDateString('zh-CN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const metrics = computed<MetricItem[]>(() => [
|
||||||
|
{ label: '智能体', value: String(agentCount.value) },
|
||||||
|
{ label: '工作流', value: String(workflowCount.value) },
|
||||||
|
{ label: '未读消息', value: String(unreadMessages.value) },
|
||||||
|
{ label: '未读公告', value: String(unreadAnnouncements.value) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const aiActions: NavItem[] = [
|
||||||
{
|
{
|
||||||
color: '#2563eb',
|
color: '#2563eb',
|
||||||
content: '管理已发布智能体,维护角色、提示词、工具能力和对话入口。',
|
desc: '已发布智能体、提示词和对话入口',
|
||||||
date: 'AI 平台',
|
|
||||||
group: '智能体',
|
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
title: '智能体管理',
|
title: '智能体',
|
||||||
url: '/ai-platform/agent',
|
url: '/ai-platform/agent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#16a34a',
|
color: '#16a34a',
|
||||||
content: '编排自主、对话流、子流程和并行节点,构建协作式任务链路。',
|
desc: '自主、对话流、子流程和并行节点',
|
||||||
date: 'AI 平台',
|
|
||||||
group: '流程',
|
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
title: '流程编排',
|
title: '流程编排',
|
||||||
url: '/ai-platform/workflow',
|
url: '/ai-platform/workflow',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
color: '#7c3aed',
|
||||||
|
desc: '通用聊天和代码协作入口',
|
||||||
|
icon: MessageSquareText,
|
||||||
|
title: 'Codex',
|
||||||
|
url: '/agent-chat/codex',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
color: '#f97316',
|
color: '#f97316',
|
||||||
content: '查看节点状态、LLM 调用、等待输入、错误和最终输出。',
|
desc: '节点状态、错误和最终输出',
|
||||||
date: '可观测',
|
|
||||||
group: '运行',
|
|
||||||
icon: History,
|
icon: History,
|
||||||
title: '执行历史',
|
title: '执行历史',
|
||||||
url: '/ai-platform/workflow-runs',
|
url: '/ai-platform/workflow-runs',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
color: '#7c3aed',
|
|
||||||
content: '维护默认 chat 模型和供应商,保障智能体运行时可解析模型。',
|
|
||||||
date: '配置',
|
|
||||||
group: '模型',
|
|
||||||
icon: Settings,
|
|
||||||
title: '模型配置',
|
|
||||||
url: '/ai-platform/model',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
color: '#0f766e',
|
color: '#0f766e',
|
||||||
content: '沉淀业务知识、提示词资料和协作过程中的可复用上下文。',
|
desc: '业务知识和协作上下文',
|
||||||
date: '知识',
|
|
||||||
group: '资料',
|
|
||||||
icon: Database,
|
icon: Database,
|
||||||
title: '知识库',
|
title: '知识库',
|
||||||
url: '/ai-platform/knowledge-base',
|
url: '/ai-platform/knowledge-base',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#475569',
|
color: '#475569',
|
||||||
content: '查看智能体、流程、模型调用和基础管理的整体运行态势。',
|
desc: '供应商、默认 chat 模型',
|
||||||
date: '看板',
|
icon: Settings,
|
||||||
group: '分析',
|
title: '模型配置',
|
||||||
icon: LayoutDashboard,
|
url: '/ai-platform/model',
|
||||||
title: '运行分析',
|
|
||||||
url: '/dashboard/analytics',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
const adminActions: NavItem[] = [
|
||||||
{
|
{
|
||||||
color: '#2563eb',
|
color: '#2563eb',
|
||||||
|
desc: '账号、状态和组织信息',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
title: '用户管理',
|
title: '用户管理',
|
||||||
url: '/system/user',
|
url: '/system/user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#7c3aed',
|
color: '#7c3aed',
|
||||||
|
desc: '角色、菜单和 API 授权',
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
title: '角色权限',
|
title: '角色权限',
|
||||||
url: '/system/role',
|
url: '/system/role',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
color: '#0f766e',
|
|
||||||
icon: KeyRound,
|
|
||||||
title: 'API 权限',
|
|
||||||
url: '/system/new-permission',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
color: '#f97316',
|
color: '#f97316',
|
||||||
|
desc: '动态菜单和路由配置',
|
||||||
icon: Menu,
|
icon: Menu,
|
||||||
title: '菜单管理',
|
title: '菜单管理',
|
||||||
url: '/system/menu',
|
url: '/system/menu',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
color: '#0f766e',
|
||||||
|
desc: '接口访问权限维护',
|
||||||
|
icon: KeyRound,
|
||||||
|
title: 'API 权限',
|
||||||
|
url: '/system/new-permission',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
color: '#16a34a',
|
color: '#16a34a',
|
||||||
|
desc: '公告发布和阅读统计',
|
||||||
icon: Megaphone,
|
icon: Megaphone,
|
||||||
title: '公告管理',
|
title: '公告管理',
|
||||||
url: '/message/announcement',
|
url: '/message/announcement',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#475569',
|
color: '#475569',
|
||||||
|
desc: '系统消息和流程通知',
|
||||||
icon: Inbox,
|
icon: Inbox,
|
||||||
title: '公告列表',
|
title: '消息列表',
|
||||||
url: '/message/announcement-list',
|
url: '/message/list',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
const opsActions: NavItem[] = [
|
||||||
{
|
{
|
||||||
completed: false,
|
color: '#2563eb',
|
||||||
content: '用真实业务需求触发业务需求分析师,确认能输出可测试需求、边界和下一步。',
|
desc: '触发需求分析师',
|
||||||
date: '今日',
|
icon: Send,
|
||||||
title: '验证业务需求分析师',
|
title: '需求分析',
|
||||||
|
url: '/ai-platform/agent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
completed: false,
|
color: '#16a34a',
|
||||||
content: '检查 start、node_start、llm_chunk、error、complete 等事件在前端是否可读。',
|
desc: '运行 Multica 协作流',
|
||||||
date: '今日',
|
icon: Play,
|
||||||
title: '检查 SSE 运行事件',
|
title: '协作流程',
|
||||||
|
url: '/ai-platform/workflow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
completed: true,
|
color: '#f97316',
|
||||||
content: '发布智能体允许 model_id 为空,运行时回退到启用的默认 chat 模型。',
|
desc: '查看运行事件',
|
||||||
date: '已完成',
|
icon: LayoutDashboard,
|
||||||
title: '模型兜底策略',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completed: false,
|
|
||||||
content: '继续观察 AI 平台、智能体聊天和运行记录页面的首屏加载与预取效果。',
|
|
||||||
date: '本周',
|
|
||||||
title: '页面切换性能',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completed: true,
|
|
||||||
content: '保留 systemd 非 Docker 部署路径,Docker 入口仅作为可选方式保留。',
|
|
||||||
date: '已完成',
|
|
||||||
title: '部署路径收敛',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const trendItems: WorkbenchTrendItem[] = [
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-1',
|
|
||||||
content: '智能体运行时已增加默认 chat 模型解析,避免 <a>model_id 为空</a> 直接失败。',
|
|
||||||
date: '刚刚',
|
|
||||||
title: '运行时',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-2',
|
|
||||||
content: '工作流运行记录聚焦节点状态、错误原因、子流程和并行分支结果。',
|
|
||||||
date: '1 小时前',
|
|
||||||
title: '可观测性',
|
title: '可观测性',
|
||||||
|
url: '/ai-platform/workflow-runs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: 'svg:avatar-3',
|
color: '#7c3aed',
|
||||||
content: '用户、角色、权限、菜单和公告作为轻量后台的基础模块保留。',
|
desc: '检查默认模型',
|
||||||
date: '今天',
|
icon: Brain,
|
||||||
title: '基础后台',
|
title: '模型策略',
|
||||||
},
|
url: '/ai-platform/model',
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-4',
|
|
||||||
content: 'AI 平台保留智能体、流程编排、模型配置、知识库和执行历史。',
|
|
||||||
date: '今天',
|
|
||||||
title: 'AI 平台',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-1',
|
|
||||||
content: '线上部署路径固定为 <a>/ai-agent-admin</a>,后端由 systemd 托管。',
|
|
||||||
date: '本周',
|
|
||||||
title: '部署',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
function navTo(url: string) {
|
||||||
if (nav.url?.startsWith('http')) {
|
router.push(url).catch((error) => {
|
||||||
openWindow(nav.url);
|
console.error('Navigation failed:', error);
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nav.url?.startsWith('/')) {
|
function formatPercent(value?: number) {
|
||||||
router.push(nav.url).catch((error) => {
|
return `${Number(value || 0).toFixed(1)}%`;
|
||||||
console.error('Navigation failed:', error);
|
}
|
||||||
});
|
|
||||||
|
function formatMemory(gb?: number) {
|
||||||
|
const size = Number(gb || 0);
|
||||||
|
if (size <= 0) return '0 GB';
|
||||||
|
if (size < 1) return `${(size * 1024).toFixed(0)} MB`;
|
||||||
|
return `${size.toFixed(1)} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAiSummary() {
|
||||||
|
try {
|
||||||
|
const [agents, workflows] = await Promise.all([
|
||||||
|
getAgentListApi({ page: 1, pageSize: 1 }),
|
||||||
|
getWorkflowListApi({ page: 1, pageSize: 1 }),
|
||||||
|
]);
|
||||||
|
agentCount.value = agents.total || agents.items?.length || 0;
|
||||||
|
workflowCount.value = workflows.total || workflows.items?.length || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载 AI 统计失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAnnouncements() {
|
||||||
|
loading.value.announcements = true;
|
||||||
|
try {
|
||||||
|
const [list, count] = await Promise.all([
|
||||||
|
getUserAnnouncementListApi({ page: 1, pageSize: 4 }),
|
||||||
|
getUnreadAnnouncementCountApi(),
|
||||||
|
]);
|
||||||
|
announcements.value = list.items || [];
|
||||||
|
unreadAnnouncements.value = count.count || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载公告失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value.announcements = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMessages() {
|
||||||
|
loading.value.messages = true;
|
||||||
|
try {
|
||||||
|
const [list, count] = await Promise.all([
|
||||||
|
getMessageListApi({ page: 1, pageSize: 4 }),
|
||||||
|
getUnreadCountApi(),
|
||||||
|
]);
|
||||||
|
messages.value = list.items || [];
|
||||||
|
unreadMessages.value = count.total || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value.messages = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadServerOverview() {
|
||||||
|
loading.value.server = true;
|
||||||
|
try {
|
||||||
|
const [overview, realtime] = await Promise.all([
|
||||||
|
getServerOverviewApi(),
|
||||||
|
getRealtimeStatsApi(),
|
||||||
|
]);
|
||||||
|
serverOverview.value = overview;
|
||||||
|
realtimeStats.value = realtime;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务器信息失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value.server = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timer = setInterval(() => {
|
||||||
|
now.value = new Date();
|
||||||
|
}, 1000);
|
||||||
|
void loadAiSummary();
|
||||||
|
void loadAnnouncements();
|
||||||
|
void loadMessages();
|
||||||
|
void loadServerOverview();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<WorkbenchHeader
|
<section class="grid gap-3 xl:grid-cols-[1fr_286px]">
|
||||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
<div class="rounded-lg border bg-card p-5 shadow-sm">
|
||||||
:stats="headerStats"
|
<div class="flex flex-col gap-5 md:flex-row md:items-center md:justify-between">
|
||||||
>
|
<div class="flex items-center gap-4">
|
||||||
<template #title>
|
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-primary text-lg font-semibold text-primary-foreground">
|
||||||
早安,{{ userStore.userInfo?.realName || userStore.userInfo?.username || '管理员' }},这里是 AI Agent Admin 工作台
|
{{ displayName.slice(0, 1) }}
|
||||||
</template>
|
</div>
|
||||||
<template #description>
|
<div>
|
||||||
保留 zq-ai-admin 成熟后台体验,聚焦智能体协作、流程编排、模型配置、知识库和基础权限治理。
|
<h1 class="text-xl font-semibold">
|
||||||
</template>
|
{{ greeting }},{{ displayName }},欢迎回来
|
||||||
</WorkbenchHeader>
|
</h1>
|
||||||
|
<p class="text-muted-foreground mt-1 text-sm">
|
||||||
|
AI Agent Admin 已保留 zq-ai-admin 成熟后台能力,聚焦智能体协作、流程编排和基础权限治理。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid min-w-72 grid-cols-4 gap-4 text-center">
|
||||||
|
<div v-for="item in metrics" :key="item.label">
|
||||||
|
<div class="text-xl font-semibold">{{ item.value }}</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-col gap-4 lg:flex-row">
|
<div class="rounded-lg border bg-card p-5 shadow-sm">
|
||||||
<div class="w-full lg:w-3/5">
|
<div class="text-muted-foreground text-sm">当前时间</div>
|
||||||
<WorkbenchProject
|
<div class="mt-4 text-2xl font-semibold tabular-nums">{{ currentTime }}</div>
|
||||||
:items="projectItems"
|
<div class="text-muted-foreground mt-2 text-sm">{{ currentDate }}</div>
|
||||||
title="核心入口"
|
|
||||||
@click="navTo"
|
|
||||||
/>
|
|
||||||
<WorkbenchTrends
|
|
||||||
:items="trendItems"
|
|
||||||
class="mt-5"
|
|
||||||
title="运行动态"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full lg:w-2/5">
|
</section>
|
||||||
<WorkbenchQuickNav
|
|
||||||
:items="quickNavItems"
|
<section class="mt-3 grid gap-3 xl:grid-cols-[1.4fr_1fr]">
|
||||||
title="基础管理"
|
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||||
@click="navTo"
|
<div class="mb-5 flex items-center justify-between">
|
||||||
/>
|
<h2 class="text-base font-semibold">AI 协作中心</h2>
|
||||||
<WorkbenchTodo
|
<button class="text-muted-foreground hover:text-foreground text-sm" @click="navTo('/ai-platform/agent')">
|
||||||
:items="todoItems"
|
更多
|
||||||
class="mt-5"
|
</button>
|
||||||
title="协作闭环待办"
|
</div>
|
||||||
/>
|
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||||
|
<button
|
||||||
|
v-for="item in aiActions"
|
||||||
|
:key="item.title"
|
||||||
|
class="group flex items-start gap-3 rounded-md border p-4 text-left transition-colors hover:bg-accent"
|
||||||
|
@click="navTo(item.url)"
|
||||||
|
>
|
||||||
|
<span class="flex size-10 shrink-0 items-center justify-center rounded-full text-white" :style="{ backgroundColor: item.color }">
|
||||||
|
<component :is="item.icon" class="size-5" />
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0">
|
||||||
|
<span class="block font-medium">{{ item.title }}</span>
|
||||||
|
<span class="text-muted-foreground mt-1 line-clamp-2 block text-xs leading-5">{{ item.desc }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||||
|
<div class="mb-5 flex items-center justify-between">
|
||||||
|
<h2 class="text-base font-semibold">基础管理</h2>
|
||||||
|
<button class="text-muted-foreground hover:text-foreground text-sm" @click="navTo('/system/user')">
|
||||||
|
更多
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 border-b border-r">
|
||||||
|
<button
|
||||||
|
v-for="item in adminActions"
|
||||||
|
:key="item.title"
|
||||||
|
class="flex h-28 flex-col items-center justify-center gap-2 border-l border-t text-center transition-colors hover:bg-accent"
|
||||||
|
@click="navTo(item.url)"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" class="size-7" :style="{ color: item.color }" />
|
||||||
|
<span class="text-sm font-medium">{{ item.title }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-3 grid gap-3 xl:grid-cols-[1fr_1fr]">
|
||||||
|
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||||
|
<div class="mb-4 flex items-center gap-2">
|
||||||
|
<Megaphone class="text-muted-foreground size-4" />
|
||||||
|
<h2 class="text-base font-semibold">最新公告</h2>
|
||||||
|
<span v-if="unreadAnnouncements" class="ml-auto rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||||
|
{{ unreadAnnouncements }} 未读
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading.announcements" class="text-muted-foreground py-10 text-center text-sm">加载公告中...</div>
|
||||||
|
<div v-else-if="announcements.length" class="divide-y">
|
||||||
|
<button
|
||||||
|
v-for="item in announcements"
|
||||||
|
:key="item.id"
|
||||||
|
class="block w-full py-3 text-left"
|
||||||
|
@click="navTo('/message/announcement-list')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="truncate font-medium">{{ item.title }}</span>
|
||||||
|
<span v-if="!item.is_read" class="size-2 rounded-full bg-primary"></span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted-foreground mt-1 line-clamp-1 text-sm">{{ item.summary || item.content }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button v-else class="text-muted-foreground flex h-40 w-full flex-col items-center justify-center gap-2 text-sm" @click="navTo('/message/announcement-list')">
|
||||||
|
<Megaphone class="size-10 opacity-30" />
|
||||||
|
暂无公告
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||||
|
<div class="mb-4 flex items-center gap-2">
|
||||||
|
<Bell class="text-muted-foreground size-4" />
|
||||||
|
<h2 class="text-base font-semibold">消息通知</h2>
|
||||||
|
<span v-if="unreadMessages" class="ml-auto rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||||
|
{{ unreadMessages }} 未读
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading.messages" class="text-muted-foreground py-10 text-center text-sm">加载消息中...</div>
|
||||||
|
<div v-else-if="messages.length" class="divide-y">
|
||||||
|
<button
|
||||||
|
v-for="item in messages"
|
||||||
|
:key="item.id"
|
||||||
|
class="block w-full py-3 text-left"
|
||||||
|
@click="navTo('/message/list')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="truncate font-medium">{{ item.title }}</span>
|
||||||
|
<span v-if="item.status === 'unread'" class="size-2 rounded-full bg-primary"></span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted-foreground mt-1 line-clamp-1 text-sm">{{ item.content }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button v-else class="text-muted-foreground flex h-40 w-full flex-col items-center justify-center gap-2 text-sm" @click="navTo('/message/list')">
|
||||||
|
<Inbox class="size-10 opacity-30" />
|
||||||
|
暂无消息
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-3 grid gap-3 xl:grid-cols-[1.4fr_1fr]">
|
||||||
|
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||||
|
<div class="mb-5 flex items-center gap-2">
|
||||||
|
<Server class="text-muted-foreground size-4" />
|
||||||
|
<h2 class="text-base font-semibold">运行环境</h2>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading.server" class="text-muted-foreground py-10 text-center text-sm">加载服务器信息中...</div>
|
||||||
|
<div v-else class="grid gap-4 md:grid-cols-4">
|
||||||
|
<div class="rounded-md bg-muted/40 p-4">
|
||||||
|
<div class="text-muted-foreground text-xs">CPU</div>
|
||||||
|
<div class="mt-3 text-xl font-semibold">{{ formatPercent(realtimeStats?.cpu_percent) }}</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">
|
||||||
|
{{ serverOverview?.cpu_info?.physical_cores || 0 }}核 / {{ serverOverview?.cpu_info?.total_cores || 0 }}线程
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted/40 p-4">
|
||||||
|
<div class="text-muted-foreground text-xs">内存</div>
|
||||||
|
<div class="mt-3 text-xl font-semibold">{{ formatPercent(realtimeStats?.memory_percent) }}</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">
|
||||||
|
{{ formatMemory(realtimeStats?.memory_details?.used) }} /
|
||||||
|
{{ formatMemory(realtimeStats?.memory_details?.total) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted/40 p-4">
|
||||||
|
<div class="text-muted-foreground text-xs">部署</div>
|
||||||
|
<div class="mt-3 text-xl font-semibold">systemd</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">/ai-agent-admin</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted/40 p-4">
|
||||||
|
<div class="text-muted-foreground text-xs">运行状态</div>
|
||||||
|
<div class="mt-3 text-xl font-semibold">可用</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">非 Docker 主部署</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border bg-card p-4 shadow-sm">
|
||||||
|
<div class="mb-5 flex items-center gap-2">
|
||||||
|
<LayoutDashboard class="text-muted-foreground size-4" />
|
||||||
|
<h2 class="text-base font-semibold">快捷入口</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
v-for="item in opsActions"
|
||||||
|
:key="item.title"
|
||||||
|
class="rounded-md border p-3 text-left transition-colors hover:bg-accent"
|
||||||
|
@click="navTo(item.url)"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" class="mb-3 size-5" :style="{ color: item.color }" />
|
||||||
|
<div class="font-medium">{{ item.title }}</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-xs">{{ item.desc }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user