feat: add lightweight admin workspace

This commit is contained in:
2026-06-21 17:48:43 +08:00
parent 56c1eec051
commit f9050c95d1
13 changed files with 603 additions and 16 deletions
@@ -1,6 +1,7 @@
{
"home": "首页",
"controlCenter": "控制中心",
"workspace": "工作台",
"aiPlatform": "AI 平台",
"aiAgent": "智能体",
"workflowOrchestration": "流程编排",
+1 -1
View File
@@ -16,7 +16,7 @@ export const overridesPreferences = defineOverridesPreferences({
enablePreferences: false,
accessMode: 'mixed',
authPageLayout: 'panel-center',
defaultHomePath: '/ai-platform/agent',
defaultHomePath: '/dashboard/workspace',
layout: 'header-sidebar-nav',
preferencesButtonPosition: 'header',
},
+4
View File
@@ -179,9 +179,12 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/_core/fallback/**/*.vue',
'../views/_core/file-preview/index.vue',
'../views/_core/menu/index.vue',
'../views/_core/message/index.vue',
'../views/_core/page-render/index.vue',
'../views/_core/permission/index.vue',
'../views/_core/role/index.vue',
'../views/_core/system-config/index.vue',
'../views/_core/ui-config/index.vue',
'../views/_core/user/index.vue',
'../views/ai-platform/agent/editor/index.vue',
'../views/ai-platform/agent/index.vue',
@@ -191,6 +194,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/ai-platform/workflow/editor/index.vue',
'../views/ai-platform/workflow/index.vue',
'../views/ai-platform/workflow-runs/index.vue',
'../views/dashboard/workspace/index.vue',
]);
const layoutMap: ComponentRecordType = {
+2 -2
View File
@@ -206,7 +206,7 @@ function setupAccessGuard(router: Router) {
if (to.path === subAppRootPath) {
const defaultHome =
preferences.app.defaultHomePath || '/ai-platform/agent';
preferences.app.defaultHomePath || '/dashboard/workspace';
// 确保路径包含子应用前缀
const subAppTargetPath = defaultHome.startsWith(subAppRootPath)
? defaultHome
@@ -222,7 +222,7 @@ function setupAccessGuard(router: Router) {
const redirectPath = normalizeHomePath(
(from.query.redirect ??
(to.path === preferences.app.defaultHomePath
? userInfo.homePath
? preferences.app.defaultHomePath
: to.fullPath)) as string,
);
+14
View File
@@ -1,4 +1,7 @@
export const LIGHT_MENU_NAMES = new Set([
'ControlCenter',
'Dashboard',
'DashboardWorkspace',
'AIAgent',
'AIKnowledgeDetail',
'AIModelConfig',
@@ -8,15 +11,21 @@ export const LIGHT_MENU_NAMES = new Set([
'AnnouncementList',
'AnnouncementManage',
'CodexAgentChat',
'MessageCenter',
'MessageList',
'KnowledgeBase',
'SystemManagement',
'SystemConfig',
'SystemMenu',
'SystemPermission',
'SystemRole',
'UIConfig',
'UserManagement',
]);
const LIGHT_ROUTE_PATH_PREFIXES = [
'/control-center',
'/dashboard',
'/ai-platform/agent',
'/ai-platform/knowledge',
'/ai-platform/knowledge-base',
@@ -24,10 +33,15 @@ const LIGHT_ROUTE_PATH_PREFIXES = [
'/ai-platform/workflow',
'/ai-platform/workflow-runs',
'/agent-chat',
'/message/list',
'/message/announcement',
'/message/announcement-list',
'/page-render/main_home',
'/system/config',
'/system/menu',
'/system/new-permission',
'/system/role',
'/system/ui-config',
'/system/user',
];
@@ -4,6 +4,7 @@ import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
import aiPlatformRoutes from './modules/ai-platform';
import dashboardRoutes from './modules/dashboard';
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
@@ -12,6 +13,7 @@ import aiPlatformRoutes from './modules/ai-platform';
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules({
'./modules/ai-platform.ts': { default: aiPlatformRoutes },
'./modules/dashboard.ts': { default: dashboardRoutes },
});
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
@@ -5,7 +5,7 @@ const routes: RouteRecordRaw[] = [
meta: { hideInMenu: true },
name: 'ControlCenterLegacyRedirect',
path: '/control-center',
redirect: '/ai-platform/agent',
redirect: '/dashboard/workspace',
},
{
meta: {
@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
},
name: 'PageRenderMainHome',
path: '/page-render/main_home',
redirect: '/ai-platform/agent',
redirect: '/dashboard/workspace',
},
{
meta: { hideInMenu: true },
@@ -0,0 +1,28 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
hideInMenu: true,
icon: 'lucide:layout-dashboard',
order: 0,
title: 'menu-title.controlCenter',
},
name: 'Dashboard',
path: '/dashboard',
redirect: '/dashboard/workspace',
children: [
{
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
hideInMenu: true,
title: 'menu-title.workspace',
},
name: 'DashboardWorkspace',
path: '/dashboard/workspace',
},
],
},
];
export default routes;
@@ -53,7 +53,7 @@ const appDynamicTitle = ref(true);
const appWatermark = ref(false);
const appWatermarkContent = ref('');
const appEnableCheckUpdates = ref(true);
const appDefaultHomePath = ref('/ai-platform/agent');
const appDefaultHomePath = ref('/dashboard/workspace');
const appEnablePreferences = ref(true);
const footerEnable = ref(true);
@@ -106,7 +106,7 @@ function getDefaultConfig() {
defaultHomePath:
overrides.app?.defaultHomePath ||
preferences.app.defaultHomePath ||
'/ai-platform/agent',
'/dashboard/workspace',
enablePreferences: overrides.app?.enablePreferences ?? true,
},
footer: {
@@ -229,7 +229,7 @@ async function loadConfig() {
// 获取带子应用前缀的首页路径
function getDefaultHomePathWithPrefix(): string {
const path = appDefaultHomePath.value || '/ai-platform/agent';
const path = appDefaultHomePath.value || '/dashboard/workspace';
const appCode = appContextStore.appCode;
// 如果是子应用模式且路径不包含子应用前缀,自动添加
if (appCode && !path.startsWith(`/app/${appCode}`)) {
@@ -0,0 +1,534 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui/es/page';
import { useUserStore } from '@vben/stores';
import {
Bell,
Bot,
Brain,
GitFork,
KeyRound,
Megaphone,
MessageSquare,
Settings,
Shield,
Users,
Workflow,
} from '@vben/icons';
import { ElButton, ElCard, ElTag } from 'element-plus';
defineOptions({ name: 'DashboardWorkspace' });
type Accent = 'blue' | 'cyan' | 'green' | 'orange' | 'violet';
interface MetricItem {
accent: Accent;
label: string;
title: string;
value: string;
}
interface NavItem {
accent: Accent;
description: string;
icon: any;
path: string;
title: string;
}
const router = useRouter();
const userStore = useUserStore();
const displayName = computed(
() => userStore.userInfo?.realName || userStore.userInfo?.username || '管理员',
);
const metrics: MetricItem[] = [
{
accent: 'blue',
label: '协作入口',
title: '智能体',
value: 'Agent',
},
{
accent: 'cyan',
label: '运行闭环',
title: '流程编排',
value: 'Flow',
},
{
accent: 'green',
label: '基础治理',
title: '权限与角色',
value: 'Auth',
},
{
accent: 'orange',
label: '运营触达',
title: '公告消息',
value: 'Notice',
},
];
const aiNavItems: NavItem[] = [
{
accent: 'blue',
description: '创建、发布和调试业务智能体',
icon: Bot,
path: '/ai-platform/agent',
title: '智能体管理',
},
{
accent: 'cyan',
description: '编排多智能体、LLM 和工具节点',
icon: Workflow,
path: '/ai-platform/workflow',
title: '流程编排',
},
{
accent: 'violet',
description: '查看运行事件、节点状态和失败原因',
icon: GitFork,
path: '/ai-platform/workflow-runs',
title: '执行历史',
},
{
accent: 'green',
description: '维护可用模型和默认 Chat 模型',
icon: Brain,
path: '/ai-platform/model',
title: '模型配置',
},
];
const adminNavItems: NavItem[] = [
{
accent: 'green',
description: '维护后台账号、状态和基础资料',
icon: Users,
path: '/system/user',
title: '用户管理',
},
{
accent: 'violet',
description: '管理角色权限和资源范围',
icon: Shield,
path: '/system/role',
title: '角色权限',
},
{
accent: 'blue',
description: '维护菜单、路由和页面入口',
icon: KeyRound,
path: '/system/menu',
title: '菜单管理',
},
{
accent: 'orange',
description: '发布公告并查看阅读反馈',
icon: Megaphone,
path: '/message/announcement',
title: '公告管理',
},
];
const runtimeItems = [
{
description: '聊天、流程运行和系统通知会集中进入消息中心。',
icon: MessageSquare,
path: '/message/list',
title: '消息中心',
},
{
description: '检查最新公告和未读提醒。',
icon: Bell,
path: '/message/announcement-list',
title: '公告列表',
},
{
description: '调整系统配置、模型参数和界面偏好。',
icon: Settings,
path: '/system/config',
title: '系统配置',
},
];
function go(path: string) {
router.push(path);
}
</script>
<template>
<Page auto-content-height>
<div class="workspace-page">
<section class="workspace-hero">
<div class="min-w-0">
<div class="hero-kicker">AI Agent Admin</div>
<h1>你好{{ displayName }}</h1>
<p>
当前后台保留基础治理和 AI 协作主链路优先处理智能体流程编排运行观测和权限管理
</p>
</div>
<div class="hero-actions">
<ElButton type="primary" @click="go('/ai-platform/agent')">
<Bot class="mr-1 size-4" />
智能体
</ElButton>
<ElButton @click="go('/ai-platform/workflow')">
<Workflow class="mr-1 size-4" />
流程编排
</ElButton>
</div>
</section>
<section class="metric-grid">
<ElCard
v-for="item in metrics"
:key="item.title"
class="metric-card"
shadow="never"
>
<div class="metric-top">
<span :class="['accent-dot', `accent-${item.accent}`]"></span>
<span>{{ item.label }}</span>
</div>
<div class="metric-value">{{ item.value }}</div>
<div class="metric-title">{{ item.title }}</div>
</ElCard>
</section>
<section class="content-grid">
<ElCard class="panel-card" shadow="never">
<template #header>
<div class="panel-header">
<span>AI 协作</span>
<ElTag effect="plain" size="small">核心链路</ElTag>
</div>
</template>
<div class="nav-grid">
<button
v-for="item in aiNavItems"
:key="item.path"
class="nav-tile"
type="button"
@click="go(item.path)"
>
<span :class="['tile-icon', `tile-${item.accent}`]">
<component :is="item.icon" class="size-5" />
</span>
<span class="tile-copy">
<strong>{{ item.title }}</strong>
<span>{{ item.description }}</span>
</span>
</button>
</div>
</ElCard>
<ElCard class="panel-card" shadow="never">
<template #header>
<div class="panel-header">
<span>基础后台</span>
<ElTag effect="plain" size="small" type="success">已保留</ElTag>
</div>
</template>
<div class="nav-grid">
<button
v-for="item in adminNavItems"
:key="item.path"
class="nav-tile"
type="button"
@click="go(item.path)"
>
<span :class="['tile-icon', `tile-${item.accent}`]">
<component :is="item.icon" class="size-5" />
</span>
<span class="tile-copy">
<strong>{{ item.title }}</strong>
<span>{{ item.description }}</span>
</span>
</button>
</div>
</ElCard>
</section>
<ElCard class="panel-card" shadow="never">
<template #header>
<div class="panel-header">
<span>运维与消息</span>
<ElTag effect="plain" size="small" type="warning">轻量入口</ElTag>
</div>
</template>
<div class="runtime-list">
<button
v-for="item in runtimeItems"
:key="item.path"
class="runtime-row"
type="button"
@click="go(item.path)"
>
<span class="runtime-icon">
<component :is="item.icon" class="size-4" />
</span>
<span class="runtime-copy">
<strong>{{ item.title }}</strong>
<span>{{ item.description }}</span>
</span>
</button>
</div>
</ElCard>
</div>
</Page>
</template>
<style scoped>
.workspace-page {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
}
.workspace-hero {
align-items: center;
background:
linear-gradient(135deg, rgb(36 99 235 / 8%), transparent 42%),
var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
display: flex;
gap: 20px;
justify-content: space-between;
padding: 22px 24px;
}
.hero-kicker {
color: var(--el-color-primary);
font-size: 13px;
font-weight: 700;
margin-bottom: 8px;
}
.workspace-hero h1 {
color: var(--el-text-color-primary);
font-size: 26px;
font-weight: 700;
line-height: 1.25;
margin: 0;
}
.workspace-hero p {
color: var(--el-text-color-regular);
line-height: 1.7;
margin: 8px 0 0;
max-width: 720px;
}
.hero-actions {
display: flex;
flex-shrink: 0;
gap: 8px;
}
.metric-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.metric-card,
.panel-card {
border-radius: 8px;
}
.metric-card :deep(.el-card__body) {
padding: 16px;
}
.metric-top {
align-items: center;
color: var(--el-text-color-secondary);
display: flex;
font-size: 12px;
gap: 8px;
}
.accent-dot {
border-radius: 999px;
display: inline-flex;
height: 8px;
width: 8px;
}
.accent-blue {
background: var(--el-color-primary);
}
.accent-cyan {
background: var(--el-color-info);
}
.accent-green {
background: var(--el-color-success);
}
.accent-orange {
background: var(--el-color-warning);
}
.accent-violet {
background: var(--el-color-danger);
}
.metric-value {
color: var(--el-text-color-primary);
font-size: 24px;
font-weight: 700;
margin-top: 14px;
}
.metric-title {
color: var(--el-text-color-regular);
font-size: 13px;
margin-top: 4px;
}
.content-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel-header {
align-items: center;
color: var(--el-text-color-primary);
display: flex;
font-weight: 700;
justify-content: space-between;
}
.nav-grid {
display: grid;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.nav-tile,
.runtime-row {
align-items: center;
background: var(--el-bg-color-page);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
color: inherit;
cursor: pointer;
display: flex;
gap: 12px;
min-width: 0;
padding: 14px;
text-align: left;
transition:
border-color 180ms ease,
box-shadow 180ms ease,
transform 180ms ease;
}
.nav-tile:hover,
.runtime-row:hover {
border-color: var(--el-color-primary-light-5);
box-shadow: 0 8px 24px rgb(15 23 42 / 8%);
transform: translateY(-1px);
}
.tile-icon,
.runtime-icon {
align-items: center;
border-radius: 8px;
display: inline-flex;
flex-shrink: 0;
height: 38px;
justify-content: center;
width: 38px;
}
.tile-blue {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
.tile-cyan {
background: var(--el-color-info-light-9);
color: var(--el-color-info);
}
.tile-green {
background: var(--el-color-success-light-9);
color: var(--el-color-success);
}
.tile-orange {
background: var(--el-color-warning-light-9);
color: var(--el-color-warning);
}
.tile-violet {
background: var(--el-color-danger-light-9);
color: var(--el-color-danger);
}
.tile-copy,
.runtime-copy {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.tile-copy strong,
.runtime-copy strong {
color: var(--el-text-color-primary);
font-size: 14px;
line-height: 1.25;
}
.tile-copy span,
.runtime-copy span {
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.45;
}
.runtime-list {
display: grid;
gap: 10px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.runtime-icon {
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
}
@media (max-width: 1180px) {
.metric-grid,
.runtime-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.workspace-hero,
.hero-actions {
align-items: stretch;
flex-direction: column;
}
.content-grid,
.metric-grid,
.nav-grid,
.runtime-list {
grid-template-columns: 1fr;
}
}
</style>