perf: defer route prefetch and enrich workbench
This commit is contained in:
@@ -111,6 +111,10 @@ function setupCommonGuard(router: Router) {
|
||||
if (preferences.transition.progress) {
|
||||
stopProgress();
|
||||
}
|
||||
|
||||
if (useAccessStore().accessToken) {
|
||||
prefetchAiPlatformPages(to.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,7 +202,6 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
prefetchAiPlatformPages();
|
||||
|
||||
// 子应用根路径重定向(/app/hr -> /app/hr/xxx)
|
||||
if (appContextStore.isSubApp) {
|
||||
|
||||
@@ -18,25 +18,51 @@ type NavigatorWithConnection = Navigator & {
|
||||
};
|
||||
};
|
||||
|
||||
let prefetched = false;
|
||||
const prefetchedGroups = new Set<string>();
|
||||
|
||||
const corePrefetchTasks = [
|
||||
() => import('#/views/_core/user/index.vue'),
|
||||
() => import('#/views/_core/menu/index.vue'),
|
||||
() => import('#/views/_core/role/index.vue'),
|
||||
type PrefetchTask = {
|
||||
key: string;
|
||||
load: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
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 = [
|
||||
() => import('#/views/_core/agent-chat/index.vue'),
|
||||
() => import('#/components/ai-chat-panel/AiChatPanel.vue'),
|
||||
const aiInteractivePrefetchTasks: PrefetchTask[] = [
|
||||
{
|
||||
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 = [
|
||||
() => import('#/views/ai-platform/agent/index.vue'),
|
||||
() => import('#/views/ai-platform/workflow/index.vue'),
|
||||
() => import('#/views/ai-platform/model/index.vue'),
|
||||
() => import('#/views/ai-platform/knowledge/index.vue'),
|
||||
() => import('#/views/ai-platform/workflow-runs/index.vue'),
|
||||
const aiManagementPrefetchTasks: PrefetchTask[] = [
|
||||
{
|
||||
key: 'ai-agent',
|
||||
load: () => import('#/views/ai-platform/agent/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) {
|
||||
@@ -63,38 +89,65 @@ function canPrefetch() {
|
||||
return !/(^|-)2g$/.test(connection?.effectiveType || '');
|
||||
}
|
||||
|
||||
async function runTasksSequentially(tasks: Array<() => Promise<unknown>>) {
|
||||
async function runTasksSequentially(tasks: PrefetchTask[]) {
|
||||
for (const task of tasks) {
|
||||
if (!canPrefetch()) {
|
||||
return;
|
||||
}
|
||||
await task().catch(() => undefined);
|
||||
if (prefetchedGroups.has(task.key)) {
|
||||
continue;
|
||||
}
|
||||
prefetchedGroups.add(task.key);
|
||||
await task.load().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function prefetchAiPlatformPages() {
|
||||
if (prefetched || typeof window === 'undefined') {
|
||||
function delayedPrefetch(tasks: PrefetchTask[], delay: number) {
|
||||
if (!tasks.length || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
prefetched = true;
|
||||
window.setTimeout(() => {
|
||||
scheduleIdle(() => {
|
||||
void runTasksSequentially(corePrefetchTasks);
|
||||
void runTasksSequentially(tasks);
|
||||
});
|
||||
}, 800);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
scheduleIdle(() => {
|
||||
void runTasksSequentially(aiInteractivePrefetchTasks);
|
||||
});
|
||||
}, 4000);
|
||||
function isSystemRoute(path: string) {
|
||||
return path.startsWith('/system/') || path.startsWith('/message/');
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
scheduleIdle(() => {
|
||||
void runTasksSequentially(aiManagementPrefetchTasks);
|
||||
});
|
||||
}, 9000);
|
||||
function isAiPlatformRoute(path: string) {
|
||||
return path.startsWith('/ai-platform/');
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UserAnnouncement } from '#/api/core/announcement';
|
||||
import type { Message } from '#/api/core/message';
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
RealtimeStats,
|
||||
ServerMonitorResponse,
|
||||
} from '#/api/core/server-monitor';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
Bell,
|
||||
Bot,
|
||||
Brain,
|
||||
Database,
|
||||
GitBranch,
|
||||
History,
|
||||
@@ -26,240 +21,515 @@ import {
|
||||
LayoutDashboard,
|
||||
Megaphone,
|
||||
Menu,
|
||||
MessageSquareText,
|
||||
Play,
|
||||
Send,
|
||||
Server,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
} from '@vben/icons';
|
||||
import { preferences } from '@vben/preferences';
|
||||
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' });
|
||||
|
||||
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 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 = [
|
||||
{ label: '智能体', value: '18' },
|
||||
{ label: '工作流', value: '12' },
|
||||
{ label: '运行事件', value: '12.8w' },
|
||||
];
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
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',
|
||||
content: '管理已发布智能体,维护角色、提示词、工具能力和对话入口。',
|
||||
date: 'AI 平台',
|
||||
group: '智能体',
|
||||
desc: '已发布智能体、提示词和对话入口',
|
||||
icon: Bot,
|
||||
title: '智能体管理',
|
||||
title: '智能体',
|
||||
url: '/ai-platform/agent',
|
||||
},
|
||||
{
|
||||
color: '#16a34a',
|
||||
content: '编排自主、对话流、子流程和并行节点,构建协作式任务链路。',
|
||||
date: 'AI 平台',
|
||||
group: '流程',
|
||||
desc: '自主、对话流、子流程和并行节点',
|
||||
icon: GitBranch,
|
||||
title: '流程编排',
|
||||
url: '/ai-platform/workflow',
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
desc: '通用聊天和代码协作入口',
|
||||
icon: MessageSquareText,
|
||||
title: 'Codex',
|
||||
url: '/agent-chat/codex',
|
||||
},
|
||||
{
|
||||
color: '#f97316',
|
||||
content: '查看节点状态、LLM 调用、等待输入、错误和最终输出。',
|
||||
date: '可观测',
|
||||
group: '运行',
|
||||
desc: '节点状态、错误和最终输出',
|
||||
icon: History,
|
||||
title: '执行历史',
|
||||
url: '/ai-platform/workflow-runs',
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
content: '维护默认 chat 模型和供应商,保障智能体运行时可解析模型。',
|
||||
date: '配置',
|
||||
group: '模型',
|
||||
icon: Settings,
|
||||
title: '模型配置',
|
||||
url: '/ai-platform/model',
|
||||
},
|
||||
{
|
||||
color: '#0f766e',
|
||||
content: '沉淀业务知识、提示词资料和协作过程中的可复用上下文。',
|
||||
date: '知识',
|
||||
group: '资料',
|
||||
desc: '业务知识和协作上下文',
|
||||
icon: Database,
|
||||
title: '知识库',
|
||||
url: '/ai-platform/knowledge-base',
|
||||
},
|
||||
{
|
||||
color: '#475569',
|
||||
content: '查看智能体、流程、模型调用和基础管理的整体运行态势。',
|
||||
date: '看板',
|
||||
group: '分析',
|
||||
icon: LayoutDashboard,
|
||||
title: '运行分析',
|
||||
url: '/dashboard/analytics',
|
||||
desc: '供应商、默认 chat 模型',
|
||||
icon: Settings,
|
||||
title: '模型配置',
|
||||
url: '/ai-platform/model',
|
||||
},
|
||||
];
|
||||
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
const adminActions: NavItem[] = [
|
||||
{
|
||||
color: '#2563eb',
|
||||
desc: '账号、状态和组织信息',
|
||||
icon: Users,
|
||||
title: '用户管理',
|
||||
url: '/system/user',
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
desc: '角色、菜单和 API 授权',
|
||||
icon: ShieldCheck,
|
||||
title: '角色权限',
|
||||
url: '/system/role',
|
||||
},
|
||||
{
|
||||
color: '#0f766e',
|
||||
icon: KeyRound,
|
||||
title: 'API 权限',
|
||||
url: '/system/new-permission',
|
||||
},
|
||||
{
|
||||
color: '#f97316',
|
||||
desc: '动态菜单和路由配置',
|
||||
icon: Menu,
|
||||
title: '菜单管理',
|
||||
url: '/system/menu',
|
||||
},
|
||||
{
|
||||
color: '#0f766e',
|
||||
desc: '接口访问权限维护',
|
||||
icon: KeyRound,
|
||||
title: 'API 权限',
|
||||
url: '/system/new-permission',
|
||||
},
|
||||
{
|
||||
color: '#16a34a',
|
||||
desc: '公告发布和阅读统计',
|
||||
icon: Megaphone,
|
||||
title: '公告管理',
|
||||
url: '/message/announcement',
|
||||
},
|
||||
{
|
||||
color: '#475569',
|
||||
desc: '系统消息和流程通知',
|
||||
icon: Inbox,
|
||||
title: '公告列表',
|
||||
url: '/message/announcement-list',
|
||||
title: '消息列表',
|
||||
url: '/message/list',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
const opsActions: NavItem[] = [
|
||||
{
|
||||
completed: false,
|
||||
content: '用真实业务需求触发业务需求分析师,确认能输出可测试需求、边界和下一步。',
|
||||
date: '今日',
|
||||
title: '验证业务需求分析师',
|
||||
color: '#2563eb',
|
||||
desc: '触发需求分析师',
|
||||
icon: Send,
|
||||
title: '需求分析',
|
||||
url: '/ai-platform/agent',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: '检查 start、node_start、llm_chunk、error、complete 等事件在前端是否可读。',
|
||||
date: '今日',
|
||||
title: '检查 SSE 运行事件',
|
||||
color: '#16a34a',
|
||||
desc: '运行 Multica 协作流',
|
||||
icon: Play,
|
||||
title: '协作流程',
|
||||
url: '/ai-platform/workflow',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: '发布智能体允许 model_id 为空,运行时回退到启用的默认 chat 模型。',
|
||||
date: '已完成',
|
||||
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 小时前',
|
||||
color: '#f97316',
|
||||
desc: '查看运行事件',
|
||||
icon: LayoutDashboard,
|
||||
title: '可观测性',
|
||||
url: '/ai-platform/workflow-runs',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: '用户、角色、权限、菜单和公告作为轻量后台的基础模块保留。',
|
||||
date: '今天',
|
||||
title: '基础后台',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: 'AI 平台保留智能体、流程编排、模型配置、知识库和执行历史。',
|
||||
date: '今天',
|
||||
title: 'AI 平台',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: '线上部署路径固定为 <a>/ai-agent-admin</a>,后端由 systemd 托管。',
|
||||
date: '本周',
|
||||
title: '部署',
|
||||
color: '#7c3aed',
|
||||
desc: '检查默认模型',
|
||||
icon: Brain,
|
||||
title: '模型策略',
|
||||
url: '/ai-platform/model',
|
||||
},
|
||||
];
|
||||
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
function navTo(url: string) {
|
||||
router.push(url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
function formatPercent(value?: number) {
|
||||
return `${Number(value || 0).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="p-3">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
:stats="headerStats"
|
||||
>
|
||||
<template #title>
|
||||
早安,{{ userStore.userInfo?.realName || userStore.userInfo?.username || '管理员' }},这里是 AI Agent Admin 工作台
|
||||
</template>
|
||||
<template #description>
|
||||
保留 zq-ai-admin 成熟后台体验,聚焦智能体协作、流程编排、模型配置、知识库和基础权限治理。
|
||||
</template>
|
||||
</WorkbenchHeader>
|
||||
<section class="grid gap-3 xl:grid-cols-[1fr_286px]">
|
||||
<div class="rounded-lg border bg-card p-5 shadow-sm">
|
||||
<div class="flex flex-col gap-5 md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-primary text-lg font-semibold text-primary-foreground">
|
||||
{{ displayName.slice(0, 1) }}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold">
|
||||
{{ greeting }},{{ displayName }},欢迎回来
|
||||
</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="w-full lg:w-3/5">
|
||||
<WorkbenchProject
|
||||
:items="projectItems"
|
||||
title="核心入口"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTrends
|
||||
:items="trendItems"
|
||||
class="mt-5"
|
||||
title="运行动态"
|
||||
/>
|
||||
<div class="rounded-lg border bg-card p-5 shadow-sm">
|
||||
<div class="text-muted-foreground text-sm">当前时间</div>
|
||||
<div class="mt-4 text-2xl font-semibold tabular-nums">{{ currentTime }}</div>
|
||||
<div class="text-muted-foreground mt-2 text-sm">{{ currentDate }}</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
title="基础管理"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo
|
||||
:items="todoItems"
|
||||
class="mt-5"
|
||||
title="协作闭环待办"
|
||||
/>
|
||||
</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 justify-between">
|
||||
<h2 class="text-base font-semibold">AI 协作中心</h2>
|
||||
<button class="text-muted-foreground hover:text-foreground text-sm" @click="navTo('/ai-platform/agent')">
|
||||
更多
|
||||
</button>
|
||||
</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 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>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user