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
+4
View File
@@ -195,6 +195,10 @@ web/apps/web-ele/src/views/online-dev/*
!web/apps/web-ele/src/views/online-dev/page-render/ !web/apps/web-ele/src/views/online-dev/page-render/
!web/apps/web-ele/src/views/online-dev/page-render/** !web/apps/web-ele/src/views/online-dev/page-render/**
web/apps/web-ele/src/views/dashboard/ web/apps/web-ele/src/views/dashboard/
!web/apps/web-ele/src/views/dashboard/
web/apps/web-ele/src/views/dashboard/*
!web/apps/web-ele/src/views/dashboard/workspace/
!web/apps/web-ele/src/views/dashboard/workspace/**
web/apps/web-ele/src/views/demos/ web/apps/web-ele/src/views/demos/
web/apps/web-ele/src/views/zq-smart-table/ web/apps/web-ele/src/views/zq-smart-table/
web/apps/web-ele/src/api/online-dev/ web/apps/web-ele/src/api/online-dev/
+3 -3
View File
@@ -17,7 +17,7 @@ from core.menu.schema import MenuCreate, MenuUpdate
menu_cache = CacheManager(prefix="menu:") menu_cache = CacheManager(prefix="menu:")
# 缓存key # 缓存key
AI_AGENT_ADMIN_MENU_CACHE_VERSION = "v11" AI_AGENT_ADMIN_MENU_CACHE_VERSION = "v12"
MENU_TREE_CACHE_KEY = f"tree:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}" MENU_TREE_CACHE_KEY = f"tree:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}"
USER_ROUTE_CACHE_PREFIX = f"user_route:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}:" USER_ROUTE_CACHE_PREFIX = f"user_route:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}:"
@@ -51,8 +51,8 @@ def _filter_ai_agent_admin_menus(menus: List[Menu]) -> List[Menu]:
def _normalize_ai_agent_admin_menu(menu: Menu) -> None: def _normalize_ai_agent_admin_menu(menu: Menu) -> None:
"""Keep the retained control center on the lightweight admin dashboard.""" """Keep the retained control center on the lightweight admin dashboard."""
if menu.name == "ControlCenter": if menu.name == "ControlCenter":
menu.path = "/page-render/main_home" menu.path = "/dashboard/workspace"
menu.component = "_core/page-render/index" menu.component = "/dashboard/workspace/index"
menu.query = None menu.query = None
+5 -5
View File
@@ -7038,9 +7038,9 @@
"name": "ControlCenter", "name": "ControlCenter",
"title": "menu-title.controlCenter", "title": "menu-title.controlCenter",
"authCode": null, "authCode": null,
"path": "/ai-platform/agent", "path": "/dashboard/workspace",
"type": "menu", "type": "menu",
"component": "/ai-platform/agent/index", "component": "/dashboard/workspace/index",
"redirect": null, "redirect": null,
"activePath": null, "activePath": null,
"query": null, "query": null,
@@ -9386,7 +9386,7 @@
"fields": { "fields": {
"application_id": "23CFVEUHdZcOQyku1mbIO", "application_id": "23CFVEUHdZcOQyku1mbIO",
"config_key": "frontend_preferences", "config_key": "frontend_preferences",
"config_value": "{\"app\":{\"defaultHomePath\":\"/ai-platform/agent\"}}", "config_value": "{\"app\":{\"defaultHomePath\":\"/dashboard/workspace\"}}",
"config_type": "preferences", "config_type": "preferences",
"description": "子应用UI偏好配置", "description": "子应用UI偏好配置",
"status": true, "status": true,
@@ -9406,7 +9406,7 @@
"fields": { "fields": {
"application_id": "uRFPsrZ0BSJodp-qesFq6", "application_id": "uRFPsrZ0BSJodp-qesFq6",
"config_key": "frontend_preferences", "config_key": "frontend_preferences",
"config_value": "{\"app\":{\"defaultHomePath\":\"/ai-platform/agent\"}}", "config_value": "{\"app\":{\"defaultHomePath\":\"/dashboard/workspace\"}}",
"config_type": "preferences", "config_type": "preferences",
"description": "子应用UI偏好配置", "description": "子应用UI偏好配置",
"status": true, "status": true,
@@ -12988,7 +12988,7 @@
"cover": "", "cover": "",
"app_type": "ai", "app_type": "ai",
"status": "published", "status": "published",
"home_path": "/ai-platform/agent", "home_path": "/dashboard/workspace",
"version": 1, "version": 1,
"config": {}, "config": {},
"owner_id": null, "owner_id": null,
@@ -1,6 +1,7 @@
{ {
"home": "首页", "home": "首页",
"controlCenter": "控制中心", "controlCenter": "控制中心",
"workspace": "工作台",
"aiPlatform": "AI 平台", "aiPlatform": "AI 平台",
"aiAgent": "智能体", "aiAgent": "智能体",
"workflowOrchestration": "流程编排", "workflowOrchestration": "流程编排",
+1 -1
View File
@@ -16,7 +16,7 @@ export const overridesPreferences = defineOverridesPreferences({
enablePreferences: false, enablePreferences: false,
accessMode: 'mixed', accessMode: 'mixed',
authPageLayout: 'panel-center', authPageLayout: 'panel-center',
defaultHomePath: '/ai-platform/agent', defaultHomePath: '/dashboard/workspace',
layout: 'header-sidebar-nav', layout: 'header-sidebar-nav',
preferencesButtonPosition: 'header', preferencesButtonPosition: 'header',
}, },
+4
View File
@@ -179,9 +179,12 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/_core/fallback/**/*.vue', '../views/_core/fallback/**/*.vue',
'../views/_core/file-preview/index.vue', '../views/_core/file-preview/index.vue',
'../views/_core/menu/index.vue', '../views/_core/menu/index.vue',
'../views/_core/message/index.vue',
'../views/_core/page-render/index.vue', '../views/_core/page-render/index.vue',
'../views/_core/permission/index.vue', '../views/_core/permission/index.vue',
'../views/_core/role/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/_core/user/index.vue',
'../views/ai-platform/agent/editor/index.vue', '../views/ai-platform/agent/editor/index.vue',
'../views/ai-platform/agent/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/editor/index.vue',
'../views/ai-platform/workflow/index.vue', '../views/ai-platform/workflow/index.vue',
'../views/ai-platform/workflow-runs/index.vue', '../views/ai-platform/workflow-runs/index.vue',
'../views/dashboard/workspace/index.vue',
]); ]);
const layoutMap: ComponentRecordType = { const layoutMap: ComponentRecordType = {
+2 -2
View File
@@ -206,7 +206,7 @@ function setupAccessGuard(router: Router) {
if (to.path === subAppRootPath) { if (to.path === subAppRootPath) {
const defaultHome = const defaultHome =
preferences.app.defaultHomePath || '/ai-platform/agent'; preferences.app.defaultHomePath || '/dashboard/workspace';
// 确保路径包含子应用前缀 // 确保路径包含子应用前缀
const subAppTargetPath = defaultHome.startsWith(subAppRootPath) const subAppTargetPath = defaultHome.startsWith(subAppRootPath)
? defaultHome ? defaultHome
@@ -222,7 +222,7 @@ function setupAccessGuard(router: Router) {
const redirectPath = normalizeHomePath( const redirectPath = normalizeHomePath(
(from.query.redirect ?? (from.query.redirect ??
(to.path === preferences.app.defaultHomePath (to.path === preferences.app.defaultHomePath
? userInfo.homePath ? preferences.app.defaultHomePath
: to.fullPath)) as string, : to.fullPath)) as string,
); );
+14
View File
@@ -1,4 +1,7 @@
export const LIGHT_MENU_NAMES = new Set([ export const LIGHT_MENU_NAMES = new Set([
'ControlCenter',
'Dashboard',
'DashboardWorkspace',
'AIAgent', 'AIAgent',
'AIKnowledgeDetail', 'AIKnowledgeDetail',
'AIModelConfig', 'AIModelConfig',
@@ -8,15 +11,21 @@ export const LIGHT_MENU_NAMES = new Set([
'AnnouncementList', 'AnnouncementList',
'AnnouncementManage', 'AnnouncementManage',
'CodexAgentChat', 'CodexAgentChat',
'MessageCenter',
'MessageList',
'KnowledgeBase', 'KnowledgeBase',
'SystemManagement', 'SystemManagement',
'SystemConfig',
'SystemMenu', 'SystemMenu',
'SystemPermission', 'SystemPermission',
'SystemRole', 'SystemRole',
'UIConfig',
'UserManagement', 'UserManagement',
]); ]);
const LIGHT_ROUTE_PATH_PREFIXES = [ const LIGHT_ROUTE_PATH_PREFIXES = [
'/control-center',
'/dashboard',
'/ai-platform/agent', '/ai-platform/agent',
'/ai-platform/knowledge', '/ai-platform/knowledge',
'/ai-platform/knowledge-base', '/ai-platform/knowledge-base',
@@ -24,10 +33,15 @@ const LIGHT_ROUTE_PATH_PREFIXES = [
'/ai-platform/workflow', '/ai-platform/workflow',
'/ai-platform/workflow-runs', '/ai-platform/workflow-runs',
'/agent-chat', '/agent-chat',
'/message/list',
'/message/announcement', '/message/announcement',
'/message/announcement-list',
'/page-render/main_home',
'/system/config',
'/system/menu', '/system/menu',
'/system/new-permission', '/system/new-permission',
'/system/role', '/system/role',
'/system/ui-config',
'/system/user', '/system/user',
]; ];
@@ -4,6 +4,7 @@ import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core'; import { coreRoutes, fallbackNotFoundRoute } from './core';
import aiPlatformRoutes from './modules/ai-platform'; import aiPlatformRoutes from './modules/ai-platform';
import dashboardRoutes from './modules/dashboard';
// 有需要可以自行打开注释,并创建文件夹 // 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
@@ -12,6 +13,7 @@ import aiPlatformRoutes from './modules/ai-platform';
/** 动态路由 */ /** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules({ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules({
'./modules/ai-platform.ts': { default: aiPlatformRoutes }, './modules/ai-platform.ts': { default: aiPlatformRoutes },
'./modules/dashboard.ts': { default: dashboardRoutes },
}); });
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */ /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
@@ -5,7 +5,7 @@ const routes: RouteRecordRaw[] = [
meta: { hideInMenu: true }, meta: { hideInMenu: true },
name: 'ControlCenterLegacyRedirect', name: 'ControlCenterLegacyRedirect',
path: '/control-center', path: '/control-center',
redirect: '/ai-platform/agent', redirect: '/dashboard/workspace',
}, },
{ {
meta: { meta: {
@@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
}, },
name: 'PageRenderMainHome', name: 'PageRenderMainHome',
path: '/page-render/main_home', path: '/page-render/main_home',
redirect: '/ai-platform/agent', redirect: '/dashboard/workspace',
}, },
{ {
meta: { hideInMenu: true }, 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 appWatermark = ref(false);
const appWatermarkContent = ref(''); const appWatermarkContent = ref('');
const appEnableCheckUpdates = ref(true); const appEnableCheckUpdates = ref(true);
const appDefaultHomePath = ref('/ai-platform/agent'); const appDefaultHomePath = ref('/dashboard/workspace');
const appEnablePreferences = ref(true); const appEnablePreferences = ref(true);
const footerEnable = ref(true); const footerEnable = ref(true);
@@ -106,7 +106,7 @@ function getDefaultConfig() {
defaultHomePath: defaultHomePath:
overrides.app?.defaultHomePath || overrides.app?.defaultHomePath ||
preferences.app.defaultHomePath || preferences.app.defaultHomePath ||
'/ai-platform/agent', '/dashboard/workspace',
enablePreferences: overrides.app?.enablePreferences ?? true, enablePreferences: overrides.app?.enablePreferences ?? true,
}, },
footer: { footer: {
@@ -229,7 +229,7 @@ async function loadConfig() {
// 获取带子应用前缀的首页路径 // 获取带子应用前缀的首页路径
function getDefaultHomePathWithPrefix(): string { function getDefaultHomePathWithPrefix(): string {
const path = appDefaultHomePath.value || '/ai-platform/agent'; const path = appDefaultHomePath.value || '/dashboard/workspace';
const appCode = appContextStore.appCode; const appCode = appContextStore.appCode;
// 如果是子应用模式且路径不包含子应用前缀,自动添加 // 如果是子应用模式且路径不包含子应用前缀,自动添加
if (appCode && !path.startsWith(`/app/${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>