بنقرة واحدة
cube-layout
在 cube-front 框架中新增、注册或切换页面布局。 当用户说"新增布局"、"自定义布局"、"修改布局"、"切换布局"、"使用 XXX 布局"时使用。 包含布局组件创建、框架注册、应用使用的完整流程。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
在 cube-front 框架中新增、注册或切换页面布局。 当用户说"新增布局"、"自定义布局"、"修改布局"、"切换布局"、"使用 XXX 布局"时使用。 包含布局组件创建、框架注册、应用使用的完整流程。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
根据后端 NewLife.Cube 控制器定义,在前端项目创建页面组件。当用户说"新增页面"、"创建页面"、"添加功能页面"、"给 XX 模块加页面"时使用。
初始化一个新的前端项目,使用 cube-front 框架。 当用户说"初始化项目"、"创建新项目"、"搭建前端项目"、"使用 cube-front 初始化"时使用此技能。 自动配置布局、状态管理(Pinia)、路由、API请求库、多语言支持、容器化部署等核心功能,开箱即用。
LOV(List of Values)值集系统使用指南。涵盖后端配置、前端组件、API 封装、枚举自动注册全链路。当用户说"配置值集"、"使用LovSelect"、"添加LOV"、"值集选择"、"枚举下拉"、"使用LOV"时使用。
命令式弹窗的代码组织规范与最佳实践。 当用户问"弹窗怎么组织"、"弹窗放哪"、"弹窗目录结构"、"弹窗代码规范"、"新增弹窗怎么写"、"重构弹窗结构"时使用。 核心原则:Vue 组件 + openXxx.ts 打开函数作为"一对"放在一起,按业务归属就近放置。
(已迁移) 此技能已更名为 cube-add-page,请使用 cube-add-page 技能根据控制器创建前端页面。当你需要创建或修改 Cube 前端 API 文件时也请使用 cube-add-page。
在 cube-front 微前端架构中新增一个子应用/模块。 当用户说"新增应用"、"创建子应用"、"添加新模块"、"新建页面"、"创建新页面"时使用。 注意:新增的应用放在调用技能的目录(项目根目录 apps/),而不是 cube-front 目录。
| name | cube-layout |
| description | 在 cube-front 框架中新增、注册或切换页面布局。 当用户说"新增布局"、"自定义布局"、"修改布局"、"切换布局"、"使用 XXX 布局"时使用。 包含布局组件创建、框架注册、应用使用的完整流程。 |
当需要以下场景时使用:
框架内置了 CyberLayout(赛博风格布局),默认已注册并使用,无需任何配置:
cube-front/core/layouts/CyberLayout/ ← 默认使用
cube-front/core/layouts/MainLayout/ ← 备用布局
cube-front/core/layouts/TopMenu/ ← 备用布局
在项目 src/main.ts 中调用 registerLayout 注册自定义布局,并设为当前布局:
// src/main.ts
import { initApp } from 'cube-front/core/initApp';
import 'cube-front/core/global.css';
import { registerLayout } from 'cube-front/core/composables/useLayout';
import MainLayout from 'cube-front/core/layouts/MainLayout';
// 注册布局并立即切换为当前布局(第二个参数 setAsCurrent = true)
registerLayout({
id: 'main',
label: '主布局',
icon: '⊟',
description: '侧边栏 + 内容区布局',
component: MainLayout,
}, true);
initApp();
关键:
registerLayout(option, setAsCurrent)是注册布局的唯一入口。
setAsCurrent = true:立即切换为该布局(写入localStorage)setAsCurrent = false(默认):仅注册,不切换(可通过setLayout(id)或布局切换器后续切换)不要使用
provide(app, LayoutKey, ...)—— 那是旧机制,RootLayout不读取它。
重要:布局样式必须使用 Element Plus CSS token(
--el-*)或 Cube Layout token(--cube-layout-*),禁止硬编码色值、自定义 CSS 变量或第三方 token 体系。 框架global.css和core/cube-layout-vars.css提供了完整的 Token 体系,布局组件直接引用即可。
┌───────────────────────────────────────────────┐
│ --el-* 体系(Element Plus 语义 Token) │
│ --el-bg-color, --el-text-color-primary │ ← 页面背景/文字/边框/填充
│ --el-color-primary, --el-border-color │ ← 主色/边框
├───────────────────────────────────────────────┤
│ --cube-layout-* 体系(布局结构 Token) │
│ --cube-layout-sidebar-width │ ← 侧边栏宽度
│ --cube-layout-menu-item-color │ ← 菜单项颜色
│ --cube-layout-nav-height │ ← 导航栏高度
│ --cube-layout-tabsview-* │ ← 标签页颜色
└───────────────────────────────────────────────┘
--el-*)| 变量 | 用途 |
|---|---|
--el-bg-color | 页面主体背景色 |
--el-bg-color-overlay | 弹窗/卡片/浮层面板背景 |
--el-fill-color | 填充色(输入框背景) |
--el-fill-color-light | 浅填充色(搜索栏背景) |
--el-text-color-primary | 一级文字色(标题/正文) |
--el-text-color-regular | 二级文字色(次要信息) |
--el-text-color-secondary | 三级文字色(提示文字) |
--el-text-color-placeholder | 占位符文字色 |
--el-border-color | 常规边框色 |
--el-border-color-light | 浅边框色(分割线) |
--el-color-primary | 主色(按钮/链接/激活态) |
--el-color-success | 成功色 |
--el-color-warning | 警告色 |
--el-color-danger | 危险色(错误/删除) |
--el-border-radius-base | 基础圆角(卡片/弹窗) |
--el-border-radius-small | 小圆角(按钮/输入框) |
--el-box-shadow-light | 浅阴影(卡片) |
--el-box-shadow | 常规阴影(下拉面板) |
--cube-layout-*)| 变量 | 用途 | 默认值 |
|---|---|---|
--cube-layout-sidebar-width | 侧边栏展开宽度 | 220px |
--cube-layout-sidebar-collapsed-width | 侧边栏折叠宽度 | 64px |
--cube-layout-nav-height | 导航栏高度 | 52px |
--cube-layout-content-padding | 内容区内边距 | 24px |
--cube-layout-sidebar-bg | 侧边栏背景 | var(--el-bg-color-overlay) |
--cube-layout-sidebar-border-color | 侧边栏边框色 | var(--el-border-color) |
--cube-layout-menu-item-color | 菜单项文字色 | var(--el-text-color-regular) |
--cube-layout-menu-item-hover-bg | 菜单项悬浮背景 | var(--el-color-primary-light-9) |
--cube-layout-menu-item-active-color | 菜单项激活色 | var(--el-color-primary) |
--cube-layout-nav-bg | 导航栏背景 | var(--el-bg-color-overlay) |
--cube-layout-nav-border-color | 导航栏边框色 | var(--el-border-color) |
--cube-layout-breadcrumb-item-color | 面包屑文字色 | var(--el-text-color-secondary) |
--cube-layout-breadcrumb-current-color | 面包屑当前色 | var(--el-text-color-primary) |
--cube-layout-tabsview-bg | 标签页栏背景 | var(--el-bg-color-overlay) |
完整
--cube-layout-*变量清单见cube-front/core/cube-layout-vars.css
| 规则 | 说明 |
|---|---|
✅ 必须使用 --el-* 或 --cube-layout-* | background: var(--el-bg-color-overlay) |
✅ 必须使用 var(--xxx) 引用 | color: var(--el-text-color-primary) |
✅ 布局专属尺寸变量以 --cube-layout- 前缀 | --cube-layout-sidebar-width |
| ✅ 带合理 fallback | var(--cube-layout-nav-height, 60px) |
❌ 禁止使用旧版自定义 token(--bg-*、--text-*、--accent-*、--sidebar-*、--navbar-*) | 已废弃,不再支持 |
| ❌ 禁止硬编码色值 | 不得出现 #fff、#1e293b、rgba(x,x,x,x) 等 |
❌ 禁止在组件 scoped style 中覆盖 --el-* 变量 | 全局变量在各布局 variables.css 中统一管理 |
推荐:在布局的 styles/variables.css 中使用两层覆盖:
/* styles/variables.css — 同时覆盖 --el-* 和 --cube-layout-* */
:root,
[data-theme="aurora"] {
/* 1) 覆盖 Element Plus 通用 Token */
--el-color-primary: #2563eb;
--el-bg-color: #f8fafc;
--el-bg-color-overlay: #ffffff;
--el-text-color-primary: #1e293b;
--el-text-color-regular: #475569;
--el-border-color: #e2e8f0;
/* 2) 覆盖 Cube Layout 布局 Token */
--cube-layout-sidebar-width: 220px;
--cube-layout-sidebar-collapsed-width: 64px;
--cube-layout-nav-height: 60px;
--cube-layout-sidebar-bg: var(--el-bg-color-overlay);
--cube-layout-menu-item-active-bg: linear-gradient(135deg, rgba(37,99,235,0.1), rgba(16,185,129,0.1));
}
[data-theme="aurora-dark"] {
/* 深色模式只覆盖差异化值 */
--el-bg-color: #0f172a;
--el-bg-color-overlay: #1e293b;
--el-text-color-primary: #f1f5f9;
...
}
/* ❌ 错误:硬编码 */
.sidebar { background: #1e1e1e; color: #e8eaed; }
/* ❌ 错误:使用已废弃的自定义 token */
.sidebar { background: var(--bg-secondary); color: var(--text-primary); }
/* ✅ 正确:使用 Element Plus token */
.sidebar { background: var(--el-bg-color-overlay); color: var(--el-text-color-primary); }
/* ✅ 正确:使用 Cube Layout token */
.sidebar { background: var(--cube-layout-sidebar-bg); }
注意:首先判断在哪个项目新增布局:
- cube-front 框架项目:布局将作为内置布局,默认被所有应用使用
- 用户自己的项目(如 NewLife.Cube.Vue):布局仅当前应用可用
适用于需要为当前项目定制独特外观的情况。
在项目 src/layouts/ 下创建布局目录:
NewLife.Cube.Vue/src/layouts/
└── CustomLayout/
├── index.vue # 主布局入口
├── Sidebar.vue # 侧边栏(可选)
└── Navbar.vue # 顶部导航栏(可选)
src/layouts/CustomLayout/index.vue:
<template>
<div class="custom-layout">
<aside class="layout-sidebar">
<slot name="sidebar">
<div class="logo">自定义Logo</div>
</slot>
</aside>
<main class="layout-main">
<header class="layout-navbar">
<slot name="header">
<h1>{{ title }}</h1>
</slot>
</header>
<div class="layout-content">
<slot></slot>
</div>
</main>
</div>
</template>
<script setup lang="ts">
defineProps<{
title?: string;
}>();
</script>
<style scoped lang="scss">
.custom-layout {
display: flex;
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
}
.layout-sidebar {
width: var(--custom-sidebar-width, 260px);
flex-shrink: 0;
background: var(--bg-secondary);
border-right: 1px solid var(--border-default);
}
.layout-main {
flex: 1;
display: flex;
flex-direction: column;
}
.layout-navbar {
height: 64px;
padding: 0 24px;
background: var(--navbar-bg, var(--bg-secondary));
border-bottom: 1px solid var(--navbar-border, var(--border-subtle));
color: var(--navbar-text, var(--text-secondary));
}
.layout-content {
flex: 1;
padding: 24px;
}
</style>
src/main.ts:
import { initApp } from 'cube-front/core/initApp';
import 'cube-front/core/global.css';
import { registerLayout } from 'cube-front/core/composables/useLayout';
import CustomLayout from './layouts/CustomLayout/index.vue';
// 注册自定义布局并立即切换为当前布局
registerLayout({
id: 'custom',
label: '自定义布局',
icon: '◉',
description: '自定义布局描述',
component: CustomLayout,
}, true);
initApp();
适用于需要让布局成为框架内置布局,所有应用默认使用的情况。
在 cube-front/core/layouts/ 下创建布局目录:
cube-front/core/layouts/
└── YourLayout/
├── index.vue # 主布局入口
├── Sidebar.vue # 侧边栏(可选)
├── Navbar.vue # 顶部导航栏(可选)
└── styles/
├── variables.scss # CSS 变量(可选)
└── theme.scss # 主题样式(可选)
cube-front/core/layouts/YourLayout/index.vue:
<template>
<div class="your-layout">
<aside class="layout-sidebar">
<slot name="sidebar">
<div class="logo">Logo</div>
</slot>
</aside>
<main class="layout-main">
<header class="layout-navbar">
<slot name="header">
<h1>{{ title }}</h1>
</slot>
</header>
<div class="layout-content">
<slot></slot>
</div>
</main>
</div>
</template>
<script setup lang="ts">
defineProps<{
title?: string;
}>();
</script>
<style lang="scss" scoped>
.your-layout {
display: flex;
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
}
.layout-sidebar {
width: 260px;
flex-shrink: 0;
background: var(--bg-secondary);
border-right: 1px solid var(--border-default);
}
.layout-main {
flex: 1;
display: flex;
flex-direction: column;
}
.layout-navbar {
height: 64px;
padding: 0 24px;
background: var(--navbar-bg, var(--bg-secondary));
border-bottom: 1px solid var(--navbar-border, var(--border-subtle));
color: var(--navbar-text, var(--text-secondary));
}
.layout-content {
flex: 1;
padding: 24px;
}
</style>
编辑 cube-front/core/main.ts:
import { initApp } from './initApp';
import YourLayout from './layouts/YourLayout/index.vue';
import { registerLayout } from './composables/useLayout';
// 注册新布局
registerLayout({
id: 'your-layout', // 唯一标识
label: '你的布局', // 显示名称
icon: '◉', // 图标
description: '布局描述',
component: YourLayout,
});
initApp();
在 registerLayout 时设置第二个参数 setAsCurrent = true:
registerLayout({
id: 'your-layout',
label: '你的布局',
icon: '◉',
description: '布局描述',
component: YourLayout,
}, true); // setAsCurrent = true,立即切换为该布局
注意:
registerLayout的第二个参数是setAsCurrent(布尔值),不是isDefault。
true:立即切换为该布局(写入localStorage)false(默认):仅注册到列表,不切换
// src/main.ts
import { initApp } from 'cube-front/core/initApp';
import 'cube-front/core/global.css';
import { registerLayout } from 'cube-front/core/composables/useLayout';
import CustomLayout from './layouts/CustomLayout/index.vue';
// 注册自定义布局并立即切换
registerLayout({
id: 'custom',
label: '自定义布局',
icon: '◉',
description: '自定义布局描述',
component: CustomLayout,
}, true);
initApp();
使用 useLayout 组合函数:
import { useLayout } from 'cube-front/core/composables/useLayout';
const { currentLayout, setLayout, availableLayouts } = useLayout();
// 切换到指定布局
setLayout('main-layout');
// 获取当前布局
console.log(currentLayout.value);
在路由 meta 中指定布局 ID:
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
meta: {
layout: 'your-layout',
},
},
];
| 布局 ID | 路径 | 特点 |
|---|---|---|
cyber | cube-front/core/layouts/CyberLayout/ | 深色科技风格 + 霓虹发光 + 主题切换 |
main | cube-front/core/layouts/MainLayout/ | 侧边栏 + 内容区,Element Plus 风格 |
top-menu | cube-front/core/layouts/TopMenu/ | 顶部导航栏 + 内容区 |
CyberLayout 是框架默认布局,特点:
#0a0e14)#4ade80)/ 青色(#22d3ee)霓虹发光CSS 变量(可在项目覆盖):
:root {
--cyber-bg-primary: #0a0e14;
--cyber-accent-green: #4ade80;
--cyber-accent-cyan: #22d3ee;
--cyber-sidebar-width: 260px;
--cyber-nav-height: 64px;
}
[data-theme="light"] {
--cyber-bg-primary: #f5f7fa;
--cyber-accent-green: #16a34a;
--cyber-accent-cyan: #0891b2;
}
提示:自定义布局的主题变量命名建议以布局名做前缀(如
--aurora-xxx),而非--cyber-xxx,避免与 CyberLayout 冲突。具体规则见上方「CSS Token 规范」章节。
| 插槽名 | 说明 |
|---|---|
default | 主内容区(页面内容) |
sidebar | 侧边栏内容 |
header | 顶部导航内容 |
pnpm dev重要:新增布局必须实现以下全部功能,才能成为生产级布局。 仅实现 UI 骨架(静态 HTML)属于"玩具",不能上线。
在 src/main.ts 中调用 registerLayout 注册布局:
// src/main.ts
import { initApp } from 'cube-front/core/initApp';
import 'cube-front/core/global.css';
import { registerLayout } from 'cube-front/core/composables/useLayout';
import AuroraLayout from './layouts/AuroraLayout/index.vue';
registerLayout({
id: 'aurora',
label: '极光蓝绿',
icon: '◉',
description: '极光蓝绿风格布局',
component: AuroraLayout,
}, true); // true = 立即切换为当前布局
initApp();
<template>
<div class="aurora-layout">
<Sidebar />
<main class="aurora-main">
<Navbar />
<div class="aurora-content">
<slot></slot>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import Sidebar from './Sidebar.vue';
import Navbar from './Navbar.vue';
</script>
<style>
@import './styles/variables.css';
</style>
必须功能:
<LogoBrand /> 自动读取配置中的 logo 和 system title<SearchBar mode="box" /> 实时搜索菜单menuStore.treeMenus 获取树形菜单<MenuItem> 组件渲染菜单(自动处理递归、图标、高亮、跳转)<UserProfile variant="sidebar" dropup> 含退出登录推荐实现(使用 MenuItem 组件):
<script setup lang="ts">
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useMenuStore } from 'cube-front/core/stores/menu';
import LogoBrand from 'cube-front/core/components/LogoBrand.vue';
import SearchBar from 'cube-front/core/components/SearchBar.vue';
import MenuItem from 'cube-front/core/components/MenuItem.vue';
import UserProfile from 'cube-front/core/components/UserProfile.vue';
const menuStore = useMenuStore();
const { treeMenus, activeMenu, loading, hasMenus } = storeToRefs(menuStore);
const menuGroups = computed(() => treeMenus.value ?? []);
</script>
<template>
<aside class="sidebar">
<!-- Logo(自动读取配置) -->
<div class="sidebar-logo"><LogoBrand /></div>
<!-- 搜索框 -->
<div class="sidebar-search"><SearchBar mode="box" placeholder="搜索菜单..." /></div>
<!-- 菜单(MenuItem 自动处理递归/图标/高亮/跳转) -->
<nav class="sidebar-menu">
<template v-for="group in menuGroups" :key="group.id">
<div v-if="group.children?.length" class="menu-group">
<div class="menu-group-title">{{ group.title || group.name }}</div>
<MenuItem v-for="item in group.children" :key="item.id" :menu="item" :activeMenu="activeMenu" />
</div>
</template>
<div v-if="!hasMenus" class="menu-empty">{{ loading ? '加载菜单中...' : '暂无菜单数据' }}</div>
</nav>
<!-- 底部用户区域 -->
<div class="sidebar-user">
<UserProfile variant="sidebar" dropup>
<template #extra-options>
<ModeSwitcher />
<ThemeSwitcher />
<LayoutSwitcher />
<NotificationBell />
</template>
</UserProfile>
</div>
</aside>
</template>
MenuItem 自动处理了菜单的所有交互(展开/折叠、递归、图标、高亮、跳转),无需手写。
仅当需要自定义交互时才手写菜单(不推荐):
必须功能:
menuStore.activeMenu.title 获取<UserProfile variant="navbar" /> 自动对接 userStore.userInfo<LayoutSwitcher /><ThemeSwitcher /><ModeSwitcher /><NotificationBell /><SearchBar mode="icon" />完整示例:
<script setup lang="ts">
import { computed } from 'vue';
import { useMenuStore } from 'cube-front/core/stores/menu';
import LayoutSwitcher from 'cube-front/core/components/LayoutSwitcher.vue';
import ThemeSwitcher from 'cube-front/core/components/ThemeSwitcher.vue';
import ModeSwitcher from 'cube-front/core/components/ModeSwitcher.vue';
import NotificationBell from 'cube-front/core/components/NotificationBell.vue';
import UserProfile from 'cube-front/core/components/UserProfile.vue';
import SearchBar from 'cube-front/core/components/SearchBar.vue';
const menuStore = useMenuStore();
const pageTitle = computed(() => menuStore.activeMenu?.title || menuStore.activeMenu?.name || '仪表盘');
</script>
<template>
<header class="navbar">
<!-- 左侧标题 -->
<div class="navbar-left">
<h2>{{ pageTitle }}</h2>
</div>
<!-- 右侧工具栏 -->
<div class="navbar-right">
<SearchBar mode="icon" />
<ModeSwitcher />
<ThemeSwitcher />
<LayoutSwitcher />
<NotificationBell />
<UserProfile variant="navbar" />
</div>
</header>
</template>
基于 activeMenu.parentMenu 反向指针链向上递推:
const breadcrumbPath = computed(() => {
if (!activeMenu.value) return [];
const path = [];
let current = activeMenu.value;
while (current) {
path.unshift({ name: current.name, title: current.title });
current = current.parentMenu;
}
return path;
});
基于框架 CSS Token 定义布局专属变量:
:root,
[data-theme='aurora'] {
/* 布局尺寸 */
--aurora-sidebar-width: 220px;
--aurora-nav-height: 60px;
--aurora-content-padding: 24px;
/* 布局颜色(基于 Layer 1 派生) */
--aurora-bg: var(--bg-primary);
--aurora-sidebar-bg: var(--bg-secondary);
--aurora-nav-bg: var(--bg-secondary);
--aurora-text-primary: var(--text-primary);
--aurora-text-secondary: var(--text-secondary);
--aurora-text-muted: var(--text-muted);
--aurora-border: var(--border-default);
--aurora-sidebar-border: var(--border-default);
/* 品牌渐变 */
--aurora-logo-gradient: linear-gradient(135deg, var(--blue-600), var(--green-500));
--aurora-primary-gradient: linear-gradient(135deg, var(--blue-600), var(--green-500));
/* 圆角 */
--aurora-radius-sm: 8px;
--aurora-radius-md: 12px;
/* 动画 */
--aurora-transition: all 0.2s ease;
}
| 规则 | 说明 |
|---|---|
| ✅ 必须引用 Layer 1 语义变量 | background: var(--bg-primary) |
✅ 布局专属变量以 --{布局名}- 前缀 | --aurora-sidebar-width |
| ✅ 基于 Layer 1 派生布局颜色 | --aurora-bg: var(--bg-primary) |
| ❌ 禁止硬编码色值 | 除非是设计原语(Layer 0)且需注释说明 |
| 功能 | 组件/方式 | 数据来源 | 说明 |
|---|---|---|---|
| 布局注册 | registerLayout() | useLayout() | main.ts 中调用 |
| Logo/标题 | <LogoBrand /> | config.base.logo + title | 自动读取配置 |
| 菜单动态数据 | <MenuItem /> | menuStore.treeMenus | 递归渲染,非静态数据 |
| 菜单图标 | MenuItem 内置 | menu.icon → Element Plus | 支持 EP 图标 + 默认兜底 |
| 活跃菜单高亮 | MenuItem 内置 | menuStore.activeMenu | 自动高亮当前项 + 祖先 |
| 菜单搜索 | <SearchBar mode="box" /> | menuStore.flatMenus | 模糊搜索 + 面包屑 + 跳转 |
| 用户信息 | <UserProfile variant="navbar" /> | userStore.userInfo | 头像/用户名/退出登录 |
| 页面标题 | menuStore.activeMenu.title | menuStore | 面包屑或标题显示 |
| 布局切换器 | <LayoutSwitcher /> | useLayout() | 多布局时自动显示 |
| 主题切换器 | <ThemeSwitcher /> | useTheme() | 主题选择下拉 |
| 模式切换器 | <ModeSwitcher /> | useTheme().toggleMode() | 明/暗模式切换 |
| 通知按钮 | <NotificationBell /> | 无 | UI 占位,可扩展 |
| 用户头像下拉 | <UserProfile variant="sidebar" dropup /> | userStore | 侧边栏底部 + 额外选项插槽 |
| 面包屑导航 | 手动基于 parentMenu 链 | activeMenu.parentMenu | 沿反向指针递推 |
| CSS Token 变量 | variables.css | Layer 1 派生 | --aurora-xxx 命名前缀 |
关键:所有展示用户/菜单/Logo 的组件都已对接真实 Store 或配置,无需手动获取数据。
| 组件 | 路径 | 对接数据 | 用途 |
|---|---|---|---|
LogoBrand | core/components/LogoBrand.vue | config.base.logo + .title | Logo 图片 + 系统标题 |
MenuItem | core/components/MenuItem.vue | menuStore | 树形菜单(递归/图标/高亮/跳转) |
SearchBar | core/components/SearchBar.vue | menuStore.flatMenus | 菜单搜索(关键词→结果→跳转) |
LayoutSwitcher | core/components/LayoutSwitcher.vue | useLayout() | 布局切换下拉 |
ThemeSwitcher | core/components/ThemeSwitcher.vue | useTheme() | 主题选择下拉 |
ModeSwitcher | core/components/ModeSwitcher.vue | useTheme() | 明/暗模式切换 |
NotificationBell | core/components/NotificationBell.vue | 无 | 通知铃铛 |
UserProfile | core/components/UserProfile.vue | userStore.userInfo | 用户头像下拉(navbar/sidebar 两种变体) |
UserAvatar | core/components/UserAvatar.vue | userStore.userInfo | 用户头像首字母(轻量无下拉) |
ActionButton | core/components/ActionButton.vue | 无 | 通用图标按钮基类 |
SwitcherDropdown | core/components/SwitcherDropdown.vue | 无 | 通用下拉切换器 |
| Store | 路径 | 用途 |
|---|---|---|
useMenuStore | cube-front/core/stores/menu.ts | 菜单树、活跃菜单 |
useUserStore | cube-front/core/stores/user.ts | 用户信息、登出 |
useTheme | cube-front/core/composables/useTheme.ts | 主题切换 |
| 需求 | 在哪里新增 | 是否需要 registerLayout |
|---|---|---|
| 仅当前项目使用 | 用户项目 src/layouts/ | 是,registerLayout(option, true) 切换为当前 |
| 所有应用共用 | cube-front core/layouts/ | 是,在 core/main.ts 中注册 |
| 设为框架默认 | cube-front core/layouts/ | 是,registerLayout(option, true) 设为当前布局 |
核心规则:无论哪种场景,都必须调用
registerLayout才能让布局生效。RootLayout.vue只读取useLayout的currentComponent,不读取provide(LayoutKey)。
本文档以 AuroraLayout(极光蓝绿) 的实际实现过程为例,完整记录从零到生产可用的全部步骤、复用的框架组件、菜单对接方式、踩坑记录与最佳实践。 后续新增布局时,按此流程执行即可避免常见错误。
需求分析 → 研究框架 → 设计 CSS Token → 创建目录结构
→ 实现 index.vue → 实现 Sidebar(对接 menuStore)
→ 实现 Navbar(对接 userStore + 框架组件)
→ 注册布局(registerLayout)→ 构建验证 → 浏览器验证
| 阶段 | 产出 | 耗时参考 |
|---|---|---|
| 需求分析 + 研究框架 | 确定复用哪些框架组件/Store | 30 分钟 |
| CSS Token 设计 | variables.css | 20 分钟 |
| 组件实现 | index.vue + Sidebar.vue + Navbar.vue | 1-2 小时 |
| 注册 + 验证 | main.ts 修改 + 构建 + 浏览器检查 | 30 分钟 |
以下组件均来自 cube-front/core/components/,无需自行实现,直接 import 使用即可。
以下组件均来自 cube-front/core/components/,无需自行实现,直接 import 使用即可。
所有组件都已对接真实数据(menuStore / userStore / config),不是静态 UI。
| 组件 | 路径 | 对接数据 | 用途 |
|---|---|---|---|
LogoBrand | core/components/LogoBrand.vue | getConfig().base.logo + .title | Logo 图片 + 系统标题,props: collapsed |
MenuItem | core/components/MenuItem.vue | menuStore 递归渲染 | 树形菜单项,支持 Element Plus 图标,props: menu depth activeMenu |
SearchBar | core/components/SearchBar.vue | menuStore.flatMenus 模糊搜索 | 菜单搜索框(mode="box")/ 图标按钮(mode="icon") |
UserProfile | core/components/UserProfile.vue | userStore.userInfo | 用户头像 + 下拉菜单(个人资料/退出登录),variant: navbar / sidebar |
UserAvatar | core/components/UserAvatar.vue | userStore.userInfo | 仅用户头像首字母 + 名称,props: size showName |
LayoutSwitcher | core/components/LayoutSwitcher.vue | useLayout() | 布局切换下拉(仅多布局时显示) |
ThemeSwitcher | core/components/ThemeSwitcher.vue | useTheme() | 主题选择下拉 |
ModeSwitcher | core/components/ModeSwitcher.vue | useTheme().toggleMode() | 明/暗模式切换按钮 |
NotificationBell | core/components/NotificationBell.vue | 无(UI 占位) | 通知铃铛按钮 |
ActionButton | core/components/ActionButton.vue | 无 | 通用图标按钮基类 |
<script setup lang="ts">
import LogoBrand from 'cube-front/core/components/LogoBrand.vue';
</script>
<template>
<!-- 折叠时隐藏标题 -->
<LogoBrand :collapsed="false" />
</template>
| 特性 | 说明 |
|---|---|
| 数据来源 | getConfig().base.logo — logo 图片 URL/路径 |
getConfig().base.title — 系统标题文字 | |
| 配置位置 | configs/config.ts → base: { logo: '/favicon.svg', title: '系统名称' } |
| 图片处理 | 自动计算宽高比,正方形图片隐藏标题避免重复 |
| Props | collapsed?: boolean — 折叠模式隐藏标题 |
| 无需传参 | 不传 collapsed 时默认展开显示 |
<script setup lang="ts">
import MenuItem from 'cube-front/core/components/MenuItem.vue';
import { useMenuStore } from 'cube-front/core/stores/menu';
import { storeToRefs } from 'pinia';
const menuStore = useMenuStore();
const { activeMenu } = storeToRefs(menuStore);
</script>
<template>
<MenuItem v-for="item in topMenus" :key="item.id" :menu="item" :activeMenu="activeMenu" />
</template>
| 特性 | 说明 |
|---|---|
| 数据来源 | props.menu — TreeMenuItem 对象 |
| 递归渲染 | 自动递归 menu.children,每层自动缩进 |
| 图标支持 | Element Plus 图标(优先)→ 内联 SVG(fallback)→ 默认 Menu 图标 |
| 图标匹配 | icon 字段值(如 "User")→ @element-plus/icons-vue 中查找 PascalCase 匹配 |
| 展开/折叠 | 有子菜单时点击切换展开 |
| 激活高亮 | 根据 activeMenu 自动判断当前项和祖先高亮 |
| 点击跳转 | 叶子节点调用 openMenuTab() + menuStore.setActiveMenu() |
| Props | menu: TreeMenuItem — 菜单项数据 |
depth?: number — 缩进层级(默认 0) | |
activeMenu?: TreeMenuItem — 当前活跃菜单 | |
| CSS 变量 | --radius-sm --sidebar-item-hover --sidebar-item-active --accent --text-secondary |
<script setup lang="ts">
import SearchBar from 'cube-front/core/components/SearchBar.vue';
</script>
<template>
<!-- 搜索框模式:输入文字弹出搜索结果下拉 -->
<SearchBar mode="box" placeholder="搜索菜单..." />
<!-- 图标模式:仅显示搜索图标按钮 -->
<SearchBar mode="icon" />
</template>
| 特性 | 说明 |
|---|---|
| 数据来源 | menuStore.flatMenus — 所有平铺菜单(含无路径的父级) |
| 匹配规则 | 按 name 和 title 模糊匹配,取前 10 条 |
| 结果展示 | 显示匹配菜单名 + 面包屑路径(父级 › 当前) |
| 高亮 | 匹配字符用 <mark> 标签高亮 |
| 点击行为 | 调用 openMenuTab() 跳转 + menuStore.setActiveMenu() 更新活跃状态 |
| Props | mode: 'icon' | 'box' — 显示模式 |
placeholder?: string — 输入框占位文字 |
<script setup lang="ts">
import UserProfile from 'cube-front/core/components/UserProfile.vue';
</script>
<template>
<!-- 顶部栏模式:下拉向下 -->
<UserProfile variant="navbar" />
<!-- 侧边栏底部模式:下拉向上 -->
<UserProfile variant="sidebar" dropup />
</template>
| 特性 | 说明 |
|---|---|
| 数据来源 | userStore.userInfo.displayName → 用户名 |
userStore.userInfo.id → 头像 URL ${baseUrl}/Cube/Avatar/${userId} | |
| 真实可用 | ✅ 登录后自动填充,无需手动获取 |
| 头像降级 | 加载失败时显示用户名首字母大写 |
| 内置操作 | 「个人资料」→ /profile;「退出登录」→ userStore.logout() |
| Slots | #extra-options — 在用户信息卡之前插入自定义内容 |
| Props | variant: 'navbar' | 'sidebar' — 显示变体 |
dropup: boolean — 是否向上弹出(sidebar 模式用) |
⚠️ 注意:组件名为
UserProfile,不是NavbarUserProfile。CyberLayout 的 Navbar 中引用NavbarUserProfile是旧代码遗留问题,实际该文件不存在,新布局应直接使用UserProfile。
<script setup lang="ts">
import LayoutSwitcher from 'cube-front/core/components/LayoutSwitcher.vue';
</script>
<template>
<LayoutSwitcher />
</template>
| 特性 | 说明 |
|---|---|
| 自动隐藏 | 仅注册了 1 个布局时不显示(v-if="layouts.length > 1") |
| 数据来源 | 读取 useLayout().layouts 和 currentLayoutId |
| 切换逻辑 | 内部调用 useLayout().setLayout(layoutId) |
| Props | onChange?: (layoutId: string) => void — 自定义切换回调 |
| 无需传参 | 默认行为即可满足绝大多数场景 |
<script setup lang="ts">
import ThemeSwitcher from 'cube-front/core/components/ThemeSwitcher.vue';
</script>
<template>
<ThemeSwitcher />
</template>
| 特性 | 说明 |
|---|---|
| 主题来源 | 读取框架已注册的主题列表 |
| 切换逻辑 | 修改 document.documentElement.setAttribute('data-theme', themeId) |
| Props | 无必填参数,开箱即用 |
<script setup lang="ts">
import ModeSwitcher from 'cube-front/core/components/ModeSwitcher.vue';
</script>
<template>
<ModeSwitcher />
</template>
| 特性 | 说明 |
|---|---|
| 模式 | light / dark / 跟随系统 |
| 存储 | 写入 localStorage,刷新后保持 |
| Props | 无必填参数 |
<script setup lang="ts">
import NotificationBell from 'cube-front/core/components/NotificationBell.vue';
</script>
<template>
<NotificationBell show-label @click="handleClick" />
</template>
| 特性 | 说明 |
|---|---|
| 图标 | 铃铛 SVG,内置样式 |
| Props | showLabel?: boolean(默认 false)、label?: string |
| 事件 | @click — 点击时触发 |
| 扩展 | 后续可接入真实通知 API,当前仅 UI 占位 |
<script setup lang="ts">
import UserAvatar from 'cube-front/core/components/UserAvatar.vue';
</script>
<template>
<UserAvatar size="small" :showName="true" variant="default" />
</template>
| 特性 | 说明 |
|---|---|
| 数据来源 | userStore.userInfo.displayName 首字母 + 全名 |
| Props | size: 'small' | 'medium' | 'large'(28/32/40px) |
showName?: boolean | |
variant: 'default' | 'green' | |
onClick?: () => void | |
对比 UserProfile | UserAvatar 轻量无下拉,UserProfile 包含完整下拉菜单 |
import { useMenuStore, type TreeMenuItem } from 'cube-front/core/stores/menu';
import { useUserStore } from 'cube-front/core/stores/user';
| Store | 关键字段 | 类型 | 说明 |
|---|---|---|---|
useMenuStore | treeMenus | TreeMenuItem[] | 树形菜单列表(顶层节点 = 菜单分组) |
useMenuStore | activeMenu | TreeMenuItem | null | 当前选中的菜单项 |
useMenuStore | loading | boolean | 菜单是否正在加载 |
useMenuStore | hasMenus | boolean | 是否有菜单数据 |
useUserStore | userInfo | UserInfo | null | 当前用户信息 |
interface TreeMenuItem {
id: string; // 菜单项唯一标识
name: string; // 路由名称(用于 router-link 的 name)
title: string; // 显示标题
path: string; // 路由路径(用于 router-link 的 to)
icon?: string; // 图标(emoji 或图标类名)
parentId?: string; // 父菜单 ID
parentMenu?: TreeMenuItem; // 父菜单对象引用(面包屑用)
children?: TreeMenuItem[]; // 子菜单
}
数据来源:menuStore.treeMenus(顶层节点作为分组,children 作为组内菜单项)
推荐使用 MenuItem 组件(无需手写菜单逻辑):
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useMenuStore } from 'cube-front/core/stores/menu';
import MenuItem from 'cube-front/core/components/MenuItem.vue';
const menuStore = useMenuStore();
const { treeMenus, activeMenu } = storeToRefs(menuStore);
const menuGroups = computed(() => treeMenus.value ?? []);
</script>
<template>
<nav>
<template v-for="group in menuGroups" :key="group.id">
<div v-if="group.children?.length" class="menu-group">
<div class="menu-group-title">{{ group.title || group.name }}</div>
<MenuItem
v-for="item in group.children"
:key="item.id"
:menu="item"
:activeMenu="activeMenu"
/>
</div>
</template>
<div v-if="!hasMenus" class="menu-empty">
{{ loading ? '加载菜单中...' : '暂无菜单数据' }}
</div>
</nav>
</template>
MenuItem 自动处理:
openMenuTab() + setActiveMenu())如果不需要分组标题或需要自定义交互(如分组折叠),可参考 CyberLayout 的直接使用 MenuItem 方式:
<MenuItem v-for="menu in menuList" :key="menu.id" :menu="menu" :activeMenu="activeMenu" />
手写菜单(不推荐,仅当需要特殊交互时):
// Navbar.vue
const activeMenu = computed(() => menuStore.activeMenu);
const breadcrumbPath = computed(() => {
const menu = activeMenu.value;
if (!menu) return [];
const path: Array<{ name: string; title?: string }> = [];
let current: TreeMenuItem | undefined = menu;
while (current) {
path.unshift({ name: current.name, title: current.title });
current = current.parentMenu; // 沿 parentMenu 反向指针向上递推
}
return path;
});
<template>
<nav class="breadcrumb">
<template v-for="(item, idx) in breadcrumbPath" :key="idx">
<span v-if="idx < breadcrumbPath.length - 1" class="bc-item">{{ item.title }}</span>
<span v-if="idx < breadcrumbPath.length - 1" class="bc-sep">›</span>
<span v-else class="bc-current">{{ item.title }}</span>
</template>
</nav>
</template>
关键点:
parentMenu是框架在构建菜单树时自动填充的反向指针,无需手动维护。
/* 框架全局定义,布局直接引用 */
--blue-600: #2563eb;
--green-500: #22c55e;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* ... 更多原语 */
--bg-primary: #ffffff; /* 亮色主背景 */
--bg-secondary: #f9fafb; /* 亮色次背景 */
--text-primary: #111827;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--border-default: #e5e7eb;
variables.css 中定义)/* layouts/AuroraLayout/styles/variables.css */
:root,
[data-theme='aurora'] {
/* 尺寸 */
--aurora-sidebar-width: 220px;
--aurora-nav-height: 60px;
--aurora-content-padding: 24px;
/* 颜色(基于 Layer 1 派生,禁止硬编码) */
--aurora-bg: var(--bg-primary);
--aurora-sidebar-bg: var(--bg-secondary);
--aurora-nav-bg: var(--bg-secondary);
--aurora-text-primary: var(--text-primary);
--aurora-text-secondary: var(--text-secondary);
--aurora-text-muted: var(--text-muted);
--aurora-border: var(--border-default);
--aurora-sidebar-border: var(--border-default);
/* 品牌渐变 */
--aurora-logo-gradient: linear-gradient(135deg, var(--blue-600), var(--green-500));
--aurora-primary-gradient: linear-gradient(135deg, var(--blue-600), var(--green-500));
/* 圆角 */
--aurora-radius-sm: 8px;
--aurora-radius-md: 12px;
/* 动画 */
--aurora-transition: all 0.2s ease;
}
| 规则 | 示例 |
|---|---|
| 布局专属变量必须以布局名做前缀 | --aurora-sidebar-width |
| 禁止使用框架布局的前缀 | 不要用 --cyber-xxx,避免冲突 |
| 颜色优先引用 Layer 1 语义变量 | background: var(--bg-primary) |
| 禁止硬编码色值(除非是 Layer 0 原语且注释说明) | — |
src/layouts/{LayoutName}/
├── index.vue # 布局入口(组合 Sidebar + Navbar + slot)
├── Sidebar.vue # 侧边栏(必须对接 menuStore)
├── Navbar.vue # 顶部导航(必须对接 userStore + 框架组件)
├── styles/
│ └── variables.css # 布局专属 CSS Token(Layer 2)
└── README.md # 可选:布局说明文档
注意:
variables.css使用@import导入(非 scoped),确保变量定义在:root作用域,供所有布局子组件引用。
// src/main.ts
import { initApp } from 'cube-front/core/initApp';
import 'cube-front/core/global.css';
import { registerLayout } from 'cube-front/core/composables/useLayout';
import AuroraLayout from './layouts/AuroraLayout/index.vue';
registerLayout({
id: 'aurora',
label: '极光蓝绿',
icon: '◉',
description: '极光蓝绿风格布局',
component: AuroraLayout,
}, true); // true = 立即切换为当前布局
initApp();
| 参数 | 说明 |
|---|---|
id | 布局唯一标识,用于 data-theme 属性 |
label | 布局切换器中显示的中文名称 |
icon | 布局切换器中显示的图标(emoji 或 SVG) |
description | 布局描述(可选) |
component | 布局 Vue 组件 |
第二个参数 true | 注册后立即设为当前布局(首次注册建议传 true) |
⚠️ 踩坑记录:早期尝试通过
provide(LayoutKey, ...)注册布局,但RootLayout.vue只读取useLayout().currentComponent(模块级状态),不读取 injected 值。正确方式是通过registerLayout()函数修改registeredLayouts数组。
职责:组合 Sidebar + Navbar + 内容区,导入 CSS 变量。
<script setup lang="ts">
import Sidebar from './Sidebar.vue';
import Navbar from './Navbar.vue';
</script>
<template>
<div class="aurora-layout">
<Sidebar />
<main class="aurora-main">
<Navbar />
<div class="aurora-content">
<slot></slot>
</div>
</main>
</div>
</template>
<style>
@import './styles/variables.css'; /* 非 scoped,定义 :root 变量 */
</style>
<style scoped lang="scss">
.aurora-layout { display: flex; min-height: 100vh; }
.aurora-main { margin-left: var(--aurora-sidebar-width); flex: 1; }
.aurora-content { flex: 1; padding: var(--aurora-content-padding); }
</style>
职责:展示 Logo + 搜索框 + 动态菜单树 + 活跃状态高亮。
关键逻辑:
<LogoBrand />,自动从 configs/config.ts 的 base.logo 和 base.title 读取<SearchBar mode="box" placeholder="搜索菜单..." />,支持菜单模糊搜索<MenuItem> 组件(menuStore.treeMenus 动态数据),自动处理递归渲染、图标、高亮、跳转<UserProfile variant="sidebar" dropup>,含头像/用户名/退出登录职责:面包屑 + 布局/主题/模式切换 + 通知 + 用户头像下拉。
关键逻辑:
activeMenu.parentMenu 链向上递推LayoutSwitcher + ThemeSwitcher + ModeSwitcher + NotificationBell + UserProfileuserStore,用户信息由 UserProfile 组件内部处理| 坑 | 表现 | 原因 | 解决方案 |
|---|---|---|---|
provide(LayoutKey) 不生效 | 布局切换器显示但切换无反应 | RootLayout.vue 不读取 injected 值 | 使用 registerLayout() 函数 |
NavbarUserProfile 导入失败 | 编译错误:模块不存在 | 组件实际名为 UserProfile.vue | 改为 import UserProfile from '.../UserProfile.vue' |
| 侧边栏菜单不更新 | 路由切换后高亮状态不变 | 使用了静态数据而非 store | 使用 <MenuItem> 组件或改为 computed(() => menuStore.treeMenus) |
| CSS 变量不生效 | 样式无变化 | variables.css 用了 scoped style | @import 必须在非 scoped 的 <style> 中 |
| 布局切换器不显示 | 只看到一个布局选项 | 只注册了一个布局 | 注册多个布局后自动显示 |
| 面包屑为空 | 导航栏无面包屑 | activeMenu 为 null | 确保路由跳转后 menuStore 已更新 activeMenu |
| 手写图标/菜单/用户信息 | 需要自行处理各种边缘情况 | 未使用框架内置组件 | 使用 MenuItem/LogoBrand/UserProfile 等现成组件 |
| 用户头像不显示 | 头像图片 404 或加载失败 | baseUrl 未正确配置 | UserProfile 自动拼接 ${baseUrl}/Cube/Avatar/${userId} |
| 菜单图标不显示 | 只有文字无图标 | 未导入 Element Plus icons 或 icon 字段不匹配 | MenuItem 已内置 EP 图标解析,设置正确的 icon 字段值即可 |
构建通过后,按以下步骤在浏览器中验证:
如果已有设计原型(HTML/CSS),按以下步骤转换:
index.vue(骨架)+ Sidebar.vue + Navbar.vuemenuStore.treeMenusLayoutSwitcher / ThemeSwitcher 等