feat(layout): 添加赛博风格布局及相关组件,支持主题切换功能何炳宏 authored at 2026-05-13 22:27:54
diff --git a/components.d.ts b/components.d.ts
index d694651..99c6524 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -8,32 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
- ElAlert: typeof import('element-plus/es')['ElAlert']
- ElAvatar: typeof import('element-plus/es')['ElAvatar']
- ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
- ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
- ElButton: typeof import('element-plus/es')['ElButton']
- ElCard: typeof import('element-plus/es')['ElCard']
- ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
- ElDialog: typeof import('element-plus/es')['ElDialog']
- ElForm: typeof import('element-plus/es')['ElForm']
- ElFormItem: typeof import('element-plus/es')['ElFormItem']
- ElIcon: typeof import('element-plus/es')['ElIcon']
- ElInput: typeof import('element-plus/es')['ElInput']
- ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
- ElOption: typeof import('element-plus/es')['ElOption']
- ElPagination: typeof import('element-plus/es')['ElPagination']
- ElRadio: typeof import('element-plus/es')['ElRadio']
- ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
- ElSelect: typeof import('element-plus/es')['ElSelect']
- ElSwitch: typeof import('element-plus/es')['ElSwitch']
- ElTable: typeof import('element-plus/es')['ElTable']
- ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
- ElTag: typeof import('element-plus/es')['ElTag']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
- export interface GlobalDirectives {
- vLoading: typeof import('element-plus/es')['ElLoadingDirective']
- }
}
diff --git a/core/composables/useLayout.ts b/core/composables/useLayout.ts
index 9b68b7e..b562c65 100644
--- a/core/composables/useLayout.ts
+++ b/core/composables/useLayout.ts
@@ -11,11 +11,20 @@ export interface LayoutOption {
component: Component;
}
-const STORAGE_KEY = 'cube-layout';
+export const STORAGE_KEY = 'cube-layout';
// 模块级响应式状态(全局单例)
-const registeredLayouts = ref<LayoutOption[]>([]);
-const currentLayoutId = ref<string>('');
+export const registeredLayouts = ref<LayoutOption[]>([]);
+export const currentLayoutId = ref<string>('');
+
+// 计算当前布局组件
+const currentLayout = computed<LayoutOption | undefined>(() =>
+ registeredLayouts.value.find((l) => l.id === currentLayoutId.value),
+);
+
+export const currentComponent = computed<Component | undefined>(
+ () => currentLayout.value?.component,
+);
/**
* 注册一个布局
@@ -40,12 +49,6 @@ export function registerLayout(option: LayoutOption): void {
* 在 Topnav / RootLayout 等组件中使用
*/
export function useLayout() {
- const currentLayout = computed<LayoutOption | undefined>(() =>
- registeredLayouts.value.find((l) => l.id === currentLayoutId.value),
- );
-
- const currentComponent = computed<Component | undefined>(() => currentLayout.value?.component);
-
function setLayout(id: string): void {
if (!registeredLayouts.value.some((l) => l.id === id)) return;
currentLayoutId.value = id;
diff --git a/core/initApp.ts b/core/initApp.ts
index c27878d..bcfd679 100644
--- a/core/initApp.ts
+++ b/core/initApp.ts
@@ -42,16 +42,42 @@ import App from './App.vue';
import router from './router';
import i18n from './i18n';
import './global.css';
-import MainLayout from './layouts/MainLayout/index.vue';
+import CyberLayout from './layouts/CyberLayout/index.vue';
+import TopMenuLayout from './layouts/TopMenu/index.vue';
import {
appProvide,
hasProvided,
LayoutKey,
getProvidedKeys,
} from './composables/useProvideInject';
+import { registerLayout, currentComponent, currentLayoutId, registeredLayouts, STORAGE_KEY } from './composables/useLayout';
import { registerPageSections } from './utils/pageSections';
import autoSectionModules from 'virtual:cube-front-sections';
+// 注册内置布局
+registerLayout({
+ id: 'top-menu',
+ label: '顶部菜单',
+ icon: '⊟',
+ description: '顶部导航栏 + 内容区布局',
+ component: TopMenuLayout,
+});
+
+registerLayout({
+ id: 'cyber',
+ label: '赛博风格',
+ icon: '◉',
+ description: '深色科技风格 + 霓虹发光效果',
+ component: CyberLayout,
+ isDefault: true,
+});
+
+// 所有布局注册完成后,从 localStorage 恢复用户选择
+const storedLayout = localStorage.getItem(STORAGE_KEY);
+if (storedLayout && registeredLayouts.value.some((l) => l.id === storedLayout)) {
+ currentLayoutId.value = storedLayout;
+}
+
/**
* 应用配置选项接口
*
@@ -99,7 +125,7 @@ export type ConfigureFunction = (
*
* @example 标准子应用入口(无需配置,插件自动处理)
* ```typescript
- * import { initApp } from '@core/initApp';
+ * import { initApp } from 'cube-front/core/initApp';
* initApp();
* ```
*
@@ -212,8 +238,10 @@ export const initApp = async (optionsOrConfigure?: InitAppOptions | ConfigureFun
// 提供默认的依赖注入值
// 只有在外部没有提供的情况下才提供默认值
+ // 使用 currentComponent(根据 localStorage 恢复的布局)而不是硬编码的 CyberLayout
if (!hasProvided(LayoutKey)) {
- appProvide(app, LayoutKey, MainLayout, { track: true });
+ const layoutComponent = currentComponent.value || CyberLayout;
+ appProvide(app, LayoutKey, layoutComponent, { track: true });
}
app.mount('#app');
diff --git a/core/layouts/CyberLayout.ts b/core/layouts/CyberLayout.ts
new file mode 100644
index 0000000..7df3df5
--- /dev/null
+++ b/core/layouts/CyberLayout.ts
@@ -0,0 +1,22 @@
+/**
+ * CyberLayout - 赛博风格布局
+ *
+ * 特点:
+ * - 默认深色主题,支持明暗切换
+ * - 绿色/青色霓虹发光效果
+ * - 左侧固定侧边栏 + 右侧内容区
+ *
+ * 使用方式:
+ * ```typescript
+ * import CyberLayout from 'cube-front/core/layouts/CyberLayout';
+ * import { LayoutKey } from 'cube-front/core/composables/useProvideInject';
+ *
+ * initApp((app, { provide }) => {
+ * provide(app, LayoutKey, CyberLayout, { override: true });
+ * });
+ * ```
+ */
+
+export { default as CyberLayout } from './index.vue';
+export { default as CyberSidebar } from './Sidebar/index.vue';
+export { default as CyberNavbar } from './Navbar/index.vue';
\ No newline at end of file
diff --git a/core/layouts/CyberLayout/index.vue b/core/layouts/CyberLayout/index.vue
new file mode 100644
index 0000000..fa37ab1
--- /dev/null
+++ b/core/layouts/CyberLayout/index.vue
@@ -0,0 +1,78 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+import Sidebar from './Sidebar/index.vue';
+import Navbar from './Navbar/index.vue';
+
+const isDark = ref(true);
+
+function toggleTheme() {
+ isDark.value = !isDark.value;
+ if (isDark.value) {
+ document.documentElement.removeAttribute('data-theme');
+ } else {
+ document.documentElement.setAttribute('data-theme', 'cyber-light');
+ }
+}
+</script>
+
+<template>
+ <div class="cyber-layout">
+ <!-- 侧边栏 -->
+ <Sidebar />
+
+ <!-- 主内容区 -->
+ <div class="cyber-main">
+ <!-- 顶部导航 -->
+ <Navbar @theme-toggle="toggleTheme" />
+
+ <!-- 页面内容 -->
+ <main class="cyber-content">
+ <slot></slot>
+ </main>
+ </div>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+.cyber-layout {
+ display: flex;
+ height: 100vh;
+ overflow: hidden;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+}
+
+.cyber-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.cyber-content {
+ flex: 1;
+ min-width: 1200px;
+ overflow: auto;
+ padding: 16px 20px;
+ background:
+ radial-gradient(ellipse at 10% 10%, var(--accent-muted) 0%, transparent 40%),
+ radial-gradient(ellipse at 90% 90%, var(--color-info-bg) 0%, transparent 40%),
+ var(--bg-primary);
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-subtle) transparent;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: var(--border-subtle);
+ border-radius: 4px;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/core/layouts/CyberLayout/Navbar/index.vue b/core/layouts/CyberLayout/Navbar/index.vue
new file mode 100644
index 0000000..0bb3485
--- /dev/null
+++ b/core/layouts/CyberLayout/Navbar/index.vue
@@ -0,0 +1,269 @@
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import LayoutSwitcher from 'cube-front/core/components/LayoutSwitcher.vue';
+import { useUserStore } from 'cube-front/core/stores/user';
+import { useMenuStore } from 'cube-front/core/stores/menu';
+
+const emit = defineEmits<{
+ (e: 'theme-toggle'): void;
+}>();
+
+const userStore = useUserStore();
+const menuStore = useMenuStore();
+
+// 用户信息
+const currentUser = computed(() => userStore.userInfo);
+const userName = computed(() => currentUser.value?.displayName || currentUser.value?.name || '管理员');
+
+// 当前页面标题
+const pageTitle = computed(() => menuStore.activeMenu?.title || menuStore.activeMenu?.name || '仪表盘');
+
+// 当前日期
+const currentDate = computed(() => {
+ const now = new Date();
+ const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
+ const year = now.getFullYear();
+ const month = now.getMonth() + 1;
+ const day = now.getDate();
+ const weekday = weekdays[now.getDay()];
+ return `${year}年${month}月${day}日 ${weekday}`;
+});
+
+function handleLogout() {
+ userStore.logout();
+}
+</script>
+
+<template>
+ <header class="cyber-navbar">
+ <!-- 左侧标题 -->
+ <div class="navbar-left">
+ <h2>{{ pageTitle }}</h2>
+ <p>欢迎回来,{{ userName }} · {{ currentDate }}</p>
+ </div>
+
+ <!-- 右侧操作 -->
+ <div class="navbar-right">
+ <!-- 搜索框 -->
+ <div class="search-box">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <circle cx="11" cy="11" r="8" />
+ <path d="M21 21l-4.35-4.35" />
+ </svg>
+ <input type="text" placeholder="搜索设备、告警..." />
+ </div>
+
+ <!-- 主题切换 -->
+ <button class="theme-toggle" @click="emit('theme-toggle')" title="切换主题">
+ <!-- 太阳图标(浅色模式) -->
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke-width="2">
+ <circle cx="12" cy="12" r="5" />
+ <line x1="12" y1="1" x2="12" y2="3" />
+ <line x1="12" y1="21" x2="12" y2="23" />
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
+ <line x1="1" y1="12" x2="3" y2="12" />
+ <line x1="21" y1="12" x2="23" y2="12" />
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
+ </svg>
+ <!-- 月亮图标(深色模式) -->
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke-width="2">
+ <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
+ </svg>
+ </button>
+
+ <!-- 布局切换 -->
+ <LayoutSwitcher>
+ <template #icon>
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <rect x="3" y="3" width="18" height="4" rx="1" />
+ <rect x="3" y="10" width="7" height="11" rx="1" />
+ <rect x="13" y="10" width="8" height="11" rx="1" />
+ </svg>
+ </template>
+ </LayoutSwitcher>
+
+ <!-- 通知按钮 -->
+ <button class="nav-btn">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9" />
+ <path d="M13.73 21a2 2 0 01-3.46 0" />
+ </svg>
+ <span>通知</span>
+ </button>
+
+ <!-- 退出登录 -->
+ <button class="nav-btn" @click="handleLogout">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" />
+ <polyline points="16,17 21,12 16,7" />
+ <line x1="21" y1="12" x2="9" y2="12" />
+ </svg>
+ <span>退出</span>
+ </button>
+ </div>
+ </header>
+</template>
+
+<style lang="scss" scoped>
+.cyber-navbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: var(--layout-nav-height, 64px);
+ padding: 0 32px;
+ background: var(--navbar-bg);
+ border-bottom: 1px solid var(--navbar-border);
+}
+
+.navbar-left {
+ h2 {
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.3px;
+ margin: 0 0 2px 0;
+ color: var(--navbar-text);
+ }
+
+ p {
+ font-size: 12px;
+ color: var(--navbar-text-muted);
+ margin: 0;
+ }
+}
+
+.navbar-right {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+// 搜索框
+.search-box {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ padding: 8px 12px;
+ width: 220px;
+ transition: border-color 0.2s;
+
+ &:focus-within {
+ border-color: var(--accent);
+ }
+
+ svg {
+ width: 14px;
+ height: 14px;
+ color: var(--text-muted);
+ flex-shrink: 0;
+ }
+
+ input {
+ background: none;
+ border: none;
+ outline: none;
+ color: var(--text-primary);
+ font-family: inherit;
+ font-size: 13px;
+ width: 100%;
+
+ &::placeholder {
+ color: var(--text-muted);
+ }
+ }
+}
+
+// 主题切换
+.theme-toggle {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-subtle);
+ border-radius: 6px;
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s;
+
+ &:hover {
+ border-color: var(--accent);
+ }
+
+ svg {
+ width: 14px;
+ height: 14px;
+ position: absolute;
+ transition: all 0.3s;
+ }
+
+ .icon-sun {
+ stroke: var(--color-warning);
+ opacity: 0;
+ transform: rotate(-90deg);
+ }
+
+ .icon-moon {
+ stroke: var(--color-warning);
+ transform: rotate(0deg);
+ }
+
+ // 深色主题时显示月亮
+ :global([data-theme='cyber-dark']) & .icon-sun,
+ :global([data-theme='forest-dark']) & .icon-sun,
+ :global(:root:not([data-theme])) & .icon-sun {
+ opacity: 0;
+ }
+
+ :global([data-theme='cyber-dark']) & .icon-moon,
+ :global([data-theme='forest-dark']) & .icon-moon,
+ :global(:root:not([data-theme])) & .icon-moon {
+ opacity: 1;
+ }
+
+ // 浅色主题时显示太阳
+ :global([data-theme='cyber-light']) & .icon-sun,
+ :global([data-theme='forest-light']) & .icon-sun {
+ opacity: 1;
+ transform: rotate(0deg);
+ }
+
+ :global([data-theme='cyber-light']) & .icon-moon,
+ :global([data-theme='forest-light']) & .icon-moon {
+ opacity: 0;
+ transform: rotate(90deg);
+ }
+}
+
+// 通用按钮
+.nav-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-subtle);
+ border-radius: 6px;
+ padding: 6px 10px;
+ color: var(--text-secondary);
+ font-family: inherit;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+
+ &:hover {
+ border-color: var(--accent);
+ color: var(--text-primary);
+ }
+}
+</style>
\ No newline at end of file
diff --git a/core/layouts/CyberLayout/Sidebar/index.vue b/core/layouts/CyberLayout/Sidebar/index.vue
new file mode 100644
index 0000000..4bf64f1
--- /dev/null
+++ b/core/layouts/CyberLayout/Sidebar/index.vue
@@ -0,0 +1,258 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { storeToRefs } from 'pinia';
+import { getConfig } from 'cube-front/core/configure';
+import { useMenuStore, type TreeMenuItem } from 'cube-front/core/stores/menu';
+import { useUserStore } from 'cube-front/core/stores/user';
+import MenuItem from 'cube-front/core/components/MenuItem.vue';
+
+const config = getConfig();
+const menuStore = useMenuStore();
+const userStore = useUserStore();
+
+const { treeMenus, activeMenu } = storeToRefs(menuStore);
+
+// Logo配置
+const logo = computed(() => config.base.logo);
+const title = computed(() => config.base.title);
+
+// 检测logo宽高比
+const logoAspectRatio = ref<number | null>(null);
+const logoImgRef = ref<HTMLImageElement | null>(null);
+
+const onLogoLoad = (e: Event) => {
+ const img = e.target as HTMLImageElement;
+ if (img.naturalWidth && img.naturalHeight) {
+ logoAspectRatio.value = img.naturalWidth / img.naturalHeight;
+ }
+};
+
+// 是否显示标题(logo为正方形时显示)
+const showLogoTitle = computed(() => {
+ if (!logo.value) return true;
+ if (logoAspectRatio.value === null) return true;
+ const ratio = logoAspectRatio.value;
+ return ratio > 0.9 && ratio < 1.1;
+});
+
+// 用户信息
+const currentUser = computed(() => userStore.userInfo);
+const userInitial = computed(() =>
+ (currentUser.value?.displayName || currentUser.value?.name || 'U').charAt(0).toUpperCase(),
+);
+const userName = computed(() => currentUser.value?.displayName || currentUser.value?.name || '');
+const userEmail = computed(() => currentUser.value?.mail || '');
+
+// 对菜单进行分组(按顶层菜单分组)
+const menuGroups = computed(() => {
+ const groups: { title: string; menus: TreeMenuItem[] }[] = [];
+
+ if (treeMenus.value && treeMenus.value.length > 0) {
+ treeMenus.value.forEach(menu => {
+ groups.push({
+ title: menu.title || menu.name,
+ menus: menu.children || [],
+ });
+ });
+ }
+
+ return groups;
+});
+</script>
+
+<template>
+ <aside class="cyber-sidebar">
+ <!-- 分割线发光效果 -->
+ <div class="sidebar-glow" />
+
+ <!-- Logo 区域 -->
+ <div class="sidebar-logo" :class="{ 'logo-only': !showLogoTitle }">
+ <div v-if="logo" class="logo-icon" :class="{ 'logo-large': !showLogoTitle }">
+ <img ref="logoImgRef" :src="logo" :alt="title" @load="onLogoLoad" />
+ </div>
+ <div v-if="showLogoTitle" class="logo-text">
+ <h1>{{ title }}</h1>
+ </div>
+ </div>
+
+ <!-- 导航菜单 -->
+ <nav class="sidebar-nav">
+ <template v-for="group in menuGroups" :key="group.title">
+ <div v-if="group.menus.length > 0" class="nav-group">
+ <div class="nav-label">{{ group.title }}</div>
+ <MenuItem
+ v-for="menu in group.menus"
+ :key="menu.id"
+ :menu="menu"
+ :activeMenu="activeMenu"
+ />
+ </div>
+ </template>
+ </nav>
+
+ <!-- 用户信息 -->
+ <div class="sidebar-user">
+ <div class="user-avatar">{{ userInitial }}</div>
+ <div class="user-info">
+ <h3>{{ userName }}</h3>
+ <p>{{ userEmail }}</p>
+ </div>
+ </div>
+ </aside>
+</template>
+
+<style lang="scss" scoped>
+.cyber-sidebar {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background: var(--sidebar-bg);
+ border-right: 1px solid var(--sidebar-border);
+}
+
+.sidebar-glow {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 1px;
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ var(--accent),
+ transparent 50%,
+ var(--accent-secondary)
+ );
+ opacity: 0.3;
+}
+
+// Logo 区域
+.sidebar-logo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 0 16px;
+ border-bottom: 1px solid var(--sidebar-border);
+ height: var(--layout-nav-height, 64px);
+
+ // 长条形 logo:高度固定,宽度自适应
+ &.logo-only {
+ justify-content: center;
+ padding: 0 12px;
+
+ .logo-icon {
+ height: 48px;
+ width: auto;
+ border-radius: var(--radius-sm);
+ }
+ }
+}
+
+.logo-icon {
+ width: 32px;
+ height: 32px;
+ background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+}
+
+.logo-text {
+ h1 {
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: -0.2px;
+ margin: 0;
+ color: var(--text-primary);
+ }
+}
+
+// 导航菜单
+.sidebar-nav {
+ flex: 1;
+ padding: 20px 12px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-subtle) transparent;
+
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: var(--border-subtle);
+ border-radius: 4px;
+ }
+}
+
+.nav-group {
+ margin-bottom: 24px;
+}
+
+.nav-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 1.5px;
+ color: var(--text-muted);
+ padding: 0 12px;
+ margin-bottom: 10px;
+}
+
+// 用户信息
+.sidebar-user {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px;
+ border-top: 1px solid var(--sidebar-border);
+}
+
+.user-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-sm);
+ background: linear-gradient(135deg, var(--violet-400), var(--rose-400));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ font-size: 12px;
+ flex-shrink: 0;
+ color: white;
+}
+
+.user-info {
+ flex: 1;
+ min-width: 0;
+
+ h3 {
+ font-size: 12px;
+ font-weight: 600;
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--text-primary);
+ }
+
+ p {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/core/layouts/MainLayout/index.vue b/core/layouts/MainLayout/index.vue
index 5d5fb9f..af057ea 100644
--- a/core/layouts/MainLayout/index.vue
+++ b/core/layouts/MainLayout/index.vue
@@ -3,7 +3,7 @@ import { ref } from 'vue';
import Navbar from './Navbar/index.vue';
import Content from './Content/index.vue';
import Sider from './Sider/index.vue';
-import { useUserStore } from '@core/stores/user';
+import { useUserStore } from 'cube-front/core/stores/user';
const userStore = useUserStore();
const collapsed = ref(false);
diff --git a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue
index 7ef0e8d..17e5a22 100644
--- a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue
+++ b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
-import { type TreeMenuItem } from '@core/stores/menu';
-import { renderMenuTitle } from '@core/utils/menuHelpers';
+import { type TreeMenuItem } from 'cube-front/core/stores/menu';
+import { renderMenuTitle } from 'cube-front/core/utils/menuHelpers';
import SecondCascaderMenuItem from './SecondCascaderMenuItem.vue';
import { computed } from 'vue';
diff --git a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue
index a807b69..1ab2fbb 100644
--- a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue
+++ b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue
@@ -1,7 +1,7 @@
<script setup lang="ts">
-import { type TreeMenuItem } from '@core/stores/menu';
-import { isChildMenu, renderMenuTitle } from '@core/utils/menuHelpers';
-import TextOverflow from '@core/components/TextOverflow.vue';
+import { type TreeMenuItem } from 'cube-front/core/stores/menu';
+import { isChildMenu, renderMenuTitle } from 'cube-front/core/utils/menuHelpers';
+import TextOverflow from 'cube-front/core/components/TextOverflow.vue';
interface SecondCascaderMenuItemProps {
menu: TreeMenuItem;
diff --git a/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue b/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue
index 90dda88..2184f61 100644
--- a/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue
+++ b/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
-import { type TreeMenuItem } from '@core/stores/menu';
+import { type TreeMenuItem } from 'cube-front/core/stores/menu';
import SecondCascaderMenu from './components/SecondCascaderMenu.vue';
-import { isChildMenu, hasChildren } from '@core/utils/menuHelpers';
+import { isChildMenu, hasChildren } from 'cube-front/core/utils/menuHelpers';
interface CascaderMenuProps {
menu: TreeMenuItem;
diff --git a/core/layouts/MainLayout/Navbar/index.vue b/core/layouts/MainLayout/Navbar/index.vue
index c452b91..93623f5 100644
--- a/core/layouts/MainLayout/Navbar/index.vue
+++ b/core/layouts/MainLayout/Navbar/index.vue
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import Menu from './Menu/index.vue';
-import { type UserInfo } from '@core/stores/user';
-import { useMenuStore } from '@core/stores/menu';
-import { getConfig } from '@core/configure';
-import { useTheme, THEMES } from '@core/composables/useTheme';
+import { type UserInfo } from 'cube-front/core/stores/user';
+import { useMenuStore } from 'cube-front/core/stores/menu';
+import { getConfig } from 'cube-front/core/configure';
+import { useTheme, THEMES } from 'cube-front/core/composables/useTheme';
interface NavbarProps {
currentUser?: Partial<UserInfo>;
diff --git a/core/layouts/MainLayout/Navbar/Menu/index.vue b/core/layouts/MainLayout/Navbar/Menu/index.vue
index 0ab7156..93d713a 100644
--- a/core/layouts/MainLayout/Navbar/Menu/index.vue
+++ b/core/layouts/MainLayout/Navbar/Menu/index.vue
@@ -1,11 +1,11 @@
<script setup lang="tsx">
import { computed, ref, onMounted, nextTick, watch } from 'vue';
import { useRouter } from 'vue-router';
-import { type TreeMenuItem, useMenuStore } from '@core/stores/menu';
+import { type TreeMenuItem, useMenuStore } from 'cube-front/core/stores/menu';
import CascaderMenu from '../CascaderMenu/index.vue';
import { ElScrollbar } from 'element-plus';
-import { hasChildren } from '@core/utils/menuHelpers';
-import { openMenuTab } from '@core/utils/menuTab';
+import { hasChildren } from 'cube-front/core/utils/menuHelpers';
+import { openMenuTab } from 'cube-front/core/utils/menuTab';
interface MenuProps {
tabPanes?: Array<TreeMenuItem>;
diff --git a/core/layouts/MainLayout/Sider/index.vue b/core/layouts/MainLayout/Sider/index.vue
index 77761ed..426165d 100644
--- a/core/layouts/MainLayout/Sider/index.vue
+++ b/core/layouts/MainLayout/Sider/index.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
-import { useMenuStore } from '@core/stores/menu';
+import { useMenuStore } from 'cube-front/core/stores/menu';
import SideMenuItem from './SideMenuItem.vue';
const menuStore = useMenuStore();
diff --git a/core/layouts/MainLayout/Sider/SideMenuItem.vue b/core/layouts/MainLayout/Sider/SideMenuItem.vue
index 606624a..5907a09 100644
--- a/core/layouts/MainLayout/Sider/SideMenuItem.vue
+++ b/core/layouts/MainLayout/Sider/SideMenuItem.vue
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
-import { type TreeMenuItem } from '@core/stores/menu';
-import { isChildMenu, renderMenuTitle, hasChildren } from '@core/utils/menuHelpers';
-import { openMenuTab } from '@core/utils/menuTab';
-import { useMenuStore } from '@core/stores/menu';
+import { type TreeMenuItem } from 'cube-front/core/stores/menu';
+import { isChildMenu, renderMenuTitle, hasChildren } from 'cube-front/core/utils/menuHelpers';
+import { openMenuTab } from 'cube-front/core/utils/menuTab';
+import { useMenuStore } from 'cube-front/core/stores/menu';
const props = withDefaults(
defineProps<{
diff --git a/core/layouts/TopMenu/IconRail/index.vue b/core/layouts/TopMenu/IconRail/index.vue
index fe510e1..3a97639 100644
--- a/core/layouts/TopMenu/IconRail/index.vue
+++ b/core/layouts/TopMenu/IconRail/index.vue
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
-import { useMenuStore, type TreeMenuItem } from '@core/stores/menu';
-import { openMenuTab } from '@core/utils/menuTab';
+import { useMenuStore, type TreeMenuItem } from 'cube-front/core/stores/menu';
+import { openMenuTab } from 'cube-front/core/utils/menuTab';
const menuStore = useMenuStore();
const { treeMenus, topLevelActiveMenu } = storeToRefs(menuStore);
diff --git a/core/layouts/TopMenu/index.vue b/core/layouts/TopMenu/index.vue
index 9241d04..1459ddd 100644
--- a/core/layouts/TopMenu/index.vue
+++ b/core/layouts/TopMenu/index.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
-import { useMenuStore, type TreeMenuItem } from '@core/stores/menu';
+import { useMenuStore, type TreeMenuItem } from 'cube-front/core/stores/menu';
import Topnav from './Topnav/index.vue';
import Content from './Content/index.vue';
diff --git a/core/layouts/TopMenu/Topnav/index.vue b/core/layouts/TopMenu/Topnav/index.vue
index f03541a..889d1b2 100644
--- a/core/layouts/TopMenu/Topnav/index.vue
+++ b/core/layouts/TopMenu/Topnav/index.vue
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
-import { useMenuStore, type TreeMenuItem } from '@core/stores/menu';
-import { useUserStore } from '@core/stores/user';
-import { getConfig } from '@core/configure';
-import { openMenuTab } from '@core/utils/menuTab';
-import ThemeSwitcher from '@core/components/ThemeSwitcher.vue';
-import LayoutSwitcher from '@core/components/LayoutSwitcher.vue';
+import { useMenuStore, type TreeMenuItem } from 'cube-front/core/stores/menu';
+import { useUserStore } from 'cube-front/core/stores/user';
+import { getConfig } from 'cube-front/core/configure';
+import { openMenuTab } from 'cube-front/core/utils/menuTab';
+import ThemeSwitcher from 'cube-front/core/components/ThemeSwitcher.vue';
+import LayoutSwitcher from 'cube-front/core/components/LayoutSwitcher.vue';
const menuStore = useMenuStore();
const userStore = useUserStore();
diff --git a/core/main.ts b/core/main.ts
index b9ea2df..b8c69f1 100644
--- a/core/main.ts
+++ b/core/main.ts
@@ -1,17 +1,3 @@
import { initApp } from './initApp';
-import TopMenuLayout from './layouts/TopMenu/index.vue';
-import { registerLayout } from './composables/useLayout';
-// 注册内置布局(可在应用层继续 registerLayout 添加更多)
-registerLayout({
- id: 'top-menu',
- label: '顶部菜单',
- icon: '⊟',
- description: '顶部导航栏 + 内容区布局',
- component: TopMenuLayout,
-});
-
-initApp((app) => {
- // 应用初始化回调,可在此注册更多布局或进行其他配置
- void app;
-});
+initApp();
\ No newline at end of file