NewLife/cube-front

feat(MainLayout): 更新主布局菜单导航组件

- Navbar/CascaderMenu 增强级联菜单交互
- Sider 更新侧边栏,新增 SideMenuItem 组件
- 修复菜单激活状态逻辑
笑笑 authored at 2026-05-05 22:32:58
08bbee4
Tree
1 Parent(s) 880d08c
Summary: 8 changed files with 862 additions and 582 deletions.
Modified +21 -69
Modified +36 -41
Modified +29 -39
Modified +15 -42
Modified +230 -110
Modified +106 -186
Modified +159 -95
Added +266 -0
Modified +21 -69
diff --git a/core/layouts/MainLayout/index.vue b/core/layouts/MainLayout/index.vue
index e18a366..5d5fb9f 100644
--- a/core/layouts/MainLayout/index.vue
+++ b/core/layouts/MainLayout/index.vue
@@ -1,4 +1,4 @@
-<script setup lang="ts">
+<script setup lang="ts">
 import { ref } from 'vue';
 import Navbar from './Navbar/index.vue';
 import Content from './Content/index.vue';
@@ -9,7 +9,7 @@ const userStore = useUserStore();
 const collapsed = ref(false);
 
 const navbarHeight = 56;
-const menuWidth = collapsed.value ? 48 : 120;
+const menuWidth = collapsed.value ? 64 : 240;
 const paddingLeft = { paddingLeft: menuWidth + 'px' };
 const paddingTop = { paddingTop: navbarHeight + 'px' };
 
@@ -19,7 +19,7 @@ const paddingStyle = { ...paddingLeft, ...paddingTop };
 <template>
   <div class="layout">
     <div class="layoutNavbar">
-      <Navbar :currentUser="userStore.userInfo" :logout="userStore.logout" :collapsed="false" />
+      <Navbar :currentUser="userStore.userInfo" :logout="userStore.logout" :collapsed="collapsed" />
     </div>
     <div class="layoutMain">
       <div class="layoutSidebar" :style="{ ...paddingTop, width: menuWidth + 'px' }">
@@ -27,9 +27,6 @@ const paddingStyle = { ...paddingLeft, ...paddingTop };
       </div>
       <div class="layoutContent" :style="paddingStyle">
         <div class="layoutContentWrapper">
-          <!-- <div v-if="breadcrumb.length > 0" class="layoutBreadcrumb">
-            <Breadcrumb></Breadcrumb>
-          </div> -->
           <Content>
             <slot></slot>
           </Content>
@@ -46,7 +43,11 @@ $layout-max-width: 1100px;
 .layout {
   width: 100%;
   height: 100%;
-  background-color: #e9f6fe;
+  min-height: 100vh;
+  background:
+    radial-gradient(ellipse 70% 50% at 15% 5%, rgba(219, 234, 254, 0.35) 0%, transparent 55%),
+    radial-gradient(ellipse 50% 40% at 85% 95%, rgba(239, 246, 255, 0.25) 0%, transparent 55%),
+    linear-gradient(180deg, #faf8f5 0%, #f5f0eb 50%, #faf8f5 100%);
 }
 
 .layoutNavbar {
@@ -57,10 +58,6 @@ $layout-max-width: 1100px;
   left: 0;
   height: $nav-size-height;
   z-index: 100;
-
-  &-hidden {
-    height: 0;
-  }
 }
 
 .layoutMain {
@@ -71,76 +68,31 @@ $layout-max-width: 1100px;
     left: 0;
     z-index: 99;
     box-sizing: border-box;
-
-    ::-webkit-scrollbar {
-      width: 12px;
-      height: 4px;
-    }
-
-    ::-webkit-scrollbar-thumb {
-      border: 4px solid transparent;
-      background-clip: padding-box;
-      border-radius: 7px;
-      background-color: var(--color-text-4);
-    }
-
-    ::-webkit-scrollbar-thumb:hover {
-      background-color: var(--color-text-3);
-    }
-
-    &::after {
-      content: '';
-      display: block;
-      position: absolute;
-      top: 0;
-      right: -1px;
-      width: 1px;
-      height: 100%;
-      background-color: var(--color-border);
-    }
-
-    .collapseBtn {
-      height: 24px;
-      width: 24px;
-      background-color: var(--color-fill-1);
-      color: var(--color-text-3);
-      border-radius: 2px;
-      cursor: pointer;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      // 位置
-      position: absolute;
-      bottom: 12px;
-      right: 12px;
-
-      &:hover {
-        background-color: var(--color-fill-3);
-      }
-    }
+    transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
   }
 
   .layoutContent {
-    background-color: #e9f6fe;
     min-width: $layout-max-width;
     min-height: 100vh;
     height: 100vh;
-    transition: padding-left 0.2s;
+    transition: padding-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     box-sizing: border-box;
-    padding-right: 5px;
-    padding-bottom: 5px;
+    padding-right: 12px;
+    padding-bottom: 12px;
   }
 
   .layoutContentWrapper {
     height: 100%;
-    background-color: #fff;
-    border-radius: 14px;
-    padding: 16px 6px 0 16px;
+    background: rgba(255, 255, 255, 0.78);
+    backdrop-filter: blur(12px);
+    -webkit-backdrop-filter: blur(12px);
+    border-radius: 16px;
+    padding: 24px 16px 0 24px;
     box-sizing: border-box;
-  }
-
-  .layoutBreadcrumb {
-    margin-bottom: 16px;
+    border: 1px solid rgba(255, 255, 255, 0.55);
+    box-shadow:
+      0 4px 24px rgba(0, 0, 0, 0.03),
+      0 1px 3px rgba(0, 0, 0, 0.02);
   }
 }
 </style>
Modified +36 -41
diff --git a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue
index 0be2f4e..7ef0e8d 100644
--- a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue
+++ b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenu.vue
@@ -1,69 +1,64 @@
-<template>
-  <div class="menu-cascader-second">
-    <div :class="['menu-cascader-second-title']" :style="{ width }">
-      {{ renderMenuTitle(menu) }}
-    </div>
-    <div class="menu-cascader-second-item-wrap" :style="{ width }">
-      <SecondCascaderMenuItem
-        v-for="leaf in menu.children || []"
-        :key="leaf.id"
-        :menu="leaf"
-        :active-menu="activeMenu"
-        :on-menu-click="onMenuClick || ((menu) => {})"
-      />
-    </div>
-  </div>
-</template>
-
 <script setup lang="ts">
 import { type TreeMenuItem } from '@core/stores/menu';
 import { renderMenuTitle } from '@core/utils/menuHelpers';
 import SecondCascaderMenuItem from './SecondCascaderMenuItem.vue';
 import { computed } from 'vue';
 
-/**
- * 二级层叠菜单组件Props接口定义
- */
 interface SecondCascaderMenuProps {
-  /** 菜单项 */
   menu: TreeMenuItem;
-  /** 宽度 */
   width: string;
-  /** 单行菜单项数量 */
   menuItemColumns: number;
-  /** 激活菜单项 */
   activeMenu?: TreeMenuItem;
-  /** 菜单点击回调函数 */
   onMenuClick?: (menu: TreeMenuItem) => void;
 }
 
 const props = defineProps<SecondCascaderMenuProps>();
-
 const menuItemColumns = computed(() => props.menuItemColumns);
 </script>
 
+<template>
+  <div class="cascader-second">
+    <div class="cascader-second-title" :style="{ width }">
+      <span class="title-text">{{ renderMenuTitle(menu) }}</span>
+    </div>
+    <div class="cascader-second-items" :style="{ width }">
+      <SecondCascaderMenuItem
+        v-for="leaf in menu.children || []"
+        :key="leaf.id"
+        :menu="leaf"
+        :active-menu="activeMenu"
+        :on-menu-click="onMenuClick || ((menu) => {})"
+      />
+    </div>
+  </div>
+</template>
+
 <style lang="scss" scoped>
-.menu-cascader-second {
-  padding-bottom: 16px;
+.cascader-second {
+  padding-bottom: 20px;
 
-  .menu-cascader-second-title {
-    letter-spacing: 0;
-    height: 40px;
-    margin-left: 16px;
-    padding-left: 20px;
-    color: #333;
+  .cascader-second-title {
+    display: flex;
+    align-items: center;
+    height: 36px;
+    margin-left: 12px;
+    padding-left: 16px;
+    border-bottom: 1px solid #f0ece6;
+    overflow: hidden;
+  }
+
+  .title-text {
+    font-size: 14px;
     font-weight: 700;
-    border-bottom: 1px solid #f1f1f1;
-    font-size: 16px;
-    line-height: 40px;
+    color: #1e293b;
+    letter-spacing: -0.01em;
+    white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
-    word-break: break-all;
-    white-space: nowrap;
   }
 
-  .menu-cascader-second-item-wrap {
-    margin-top: 8px;
+  .cascader-second-items {
+    margin-top: 6px;
     display: grid;
     grid-template-columns: repeat(v-bind(menuItemColumns), 1fr);
     width: 100%;
Modified +29 -39
diff --git a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue
index 0aa77c2..a807b69 100644
--- a/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue
+++ b/core/layouts/MainLayout/Navbar/CascaderMenu/components/SecondCascaderMenuItem.vue
@@ -1,40 +1,16 @@
-<template>
-  <div
-    :class="[
-      'menu-cascader-second-item',
-      {
-        'menu-cascader-second-item-active': isChildMenu(activeMenu, menu),
-      },
-    ]"
-    @click="handleMenuClick"
-  >
-    <TextOverflow :text="renderMenuTitle(menu)" tooltip-placement="right" class="menu-title-text" />
-  </div>
-</template>
-
 <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';
 
-/**
- * 二级层叠菜单项组件Props接口定义
- */
 interface SecondCascaderMenuItemProps {
-  /** 菜单项 */
   menu: TreeMenuItem;
-  /** 激活菜单项 */
   activeMenu?: TreeMenuItem;
-  /** 菜单点击回调函数 */
   onMenuClick: (menu: TreeMenuItem) => void;
 }
 
 const props = defineProps<SecondCascaderMenuItemProps>();
 
-/**
- * 处理菜单项点击
- * @param event 鼠标事件
- */
 const handleMenuClick = (event: MouseEvent) => {
   event.preventDefault();
   event.stopPropagation();
@@ -42,31 +18,45 @@ const handleMenuClick = (event: MouseEvent) => {
 };
 </script>
 
+<template>
+  <div
+    :class="['cascader-item', { 'cascader-item--active': isChildMenu(activeMenu, menu) }]"
+    @click="handleMenuClick"
+  >
+    <TextOverflow :text="renderMenuTitle(menu)" tooltip-placement="right" class="menu-title-text" />
+  </div>
+</template>
+
 <style lang="scss" scoped>
-.menu-cascader-second-item {
-  font-size: 14px;
-  letter-spacing: 0;
+.cascader-item {
+  font-size: 13px;
   width: 100%;
-  height: 36px;
-  margin-top: 0;
-  margin-bottom: 0;
-  margin-left: 16px;
-  padding-left: 20px;
-  padding-right: 10px;
-  color: #6a6a6a;
-  line-height: 36px;
+  height: 34px;
+  margin-left: 12px;
+  padding-left: 16px;
+  padding-right: 8px;
+  color: #64748b;
+  line-height: 34px;
   cursor: pointer;
+  border-radius: 8px;
+  box-sizing: border-box;
+  transition:
+    background-color 0.15s ease,
+    color 0.15s ease;
 
   .menu-title-text {
     width: 100%;
     display: block;
   }
-  box-sizing: border-box;
 
-  &-active,
+  &--active,
   &:hover {
-    color: var(--primary-color);
-    background: #e9f6fe;
+    color: #1e40af;
+    background: #eff6ff;
+  }
+
+  &--active {
+    font-weight: 600;
   }
 }
 </style>
Modified +15 -42
diff --git a/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue b/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue
index a81b3fa..90dda88 100644
--- a/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue
+++ b/core/layouts/MainLayout/Navbar/CascaderMenu/index.vue
@@ -4,57 +4,30 @@ import { type TreeMenuItem } from '@core/stores/menu';
 import SecondCascaderMenu from './components/SecondCascaderMenu.vue';
 import { isChildMenu, hasChildren } from '@core/utils/menuHelpers';
 
-/**
- * 层叠菜单组件Props接口定义
- */
 interface CascaderMenuProps {
-  /** 菜单项 */
   menu: TreeMenuItem;
-  /** 激活菜单项 */
   activeMenu?: TreeMenuItem;
-  /** 当前鼠标悬浮菜单项 */
   currentMenu?: TreeMenuItem;
-  /** 菜单点击回调函数 */
   onMenuClick: (menu: TreeMenuItem) => void;
 }
 
 const props = defineProps<CascaderMenuProps>();
-
-/** 一行显示菜单项数量 */
 const menuItemColumns = 3;
-/** 子菜单宽度 */
 const subMenuWidth = 225;
-/** 层叠菜单宽度 */
 const subWidth = `${subMenuWidth * menuItemColumns}px`;
-/** 导航栏宽度 */
-const navWidth = '200px';
-/** 是否折叠 */
+const navWidth = '220px';
 const collapsed = false;
 
-/**
- * 计算处理后的子菜单列表
- * 将没有子菜单的项目归类到一起,有子菜单的单独列出
- */
 const children = computed(() => {
-  /**  无子菜单的菜单项  */
   const noChildrenList = props.menu.children?.filter((item) => !hasChildren(item));
-
-  /** 有子菜单的菜单项 */
   const childrenList: TreeMenuItem[] =
     noChildrenList && noChildrenList.length > 0
-      ? [
-          {
-            ...props.menu,
-            children: noChildrenList,
-          },
-        ]
+      ? [{ ...props.menu, children: noChildrenList }]
       : [];
-
   props.menu.children?.forEach((item) => {
     if (noChildrenList?.includes(item)) return;
     childrenList.push(item);
   });
-
   return childrenList;
 });
 </script>
@@ -63,13 +36,9 @@ const children = computed(() => {
   <div
     :class="[
       'menu-cascader',
-      {
-        'menu-cascader-current': currentMenu && isChildMenu(currentMenu, menu),
-      },
+      { 'menu-cascader--visible': currentMenu && isChildMenu(currentMenu, menu) },
     ]"
-    :style="{
-      maxWidth: collapsed ? 'calc(100vw - 50px)' : `calc(100vw - ${navWidth})`,
-    }"
+    :style="{ maxWidth: collapsed ? 'calc(100vw - 50px)' : `calc(100vw - ${navWidth})` }"
   >
     <SecondCascaderMenu
       v-for="second in children"
@@ -90,17 +59,21 @@ const children = computed(() => {
   display: none;
   height: 100%;
   width: 675px;
-  max-width: calc(100vw - 200px);
+  max-width: calc(100vw - 220px);
   overflow: hidden auto;
-  padding: 32px 16px 0;
+  padding: 28px 20px 0;
   z-index: 1100;
-  background: #fff;
-  border-left: 1px solid #f0f0f0;
+  background: rgba(255, 255, 255, 0.95);
+  backdrop-filter: blur(16px);
+  -webkit-backdrop-filter: blur(16px);
+  border-left: 1px solid rgba(226, 224, 220, 0.5);
+  border-radius: 0 14px 14px 0;
+  box-shadow:
+    4px 0 24px rgba(0, 0, 0, 0.05),
+    0 4px 24px rgba(0, 0, 0, 0.04);
 
-  /* 完全隐藏滚动条,但保留滚动功能 */
   scrollbar-width: none;
   -ms-overflow-style: none;
-
   &::-webkit-scrollbar {
     width: 0;
     height: 0;
@@ -108,7 +81,7 @@ const children = computed(() => {
   }
 }
 
-.menu-cascader-current {
+.menu-cascader--visible {
   display: block;
 }
 </style>
Modified +230 -110
diff --git a/core/layouts/MainLayout/Navbar/index.vue b/core/layouts/MainLayout/Navbar/index.vue
index 786f2c4..c452b91 100644
--- a/core/layouts/MainLayout/Navbar/index.vue
+++ b/core/layouts/MainLayout/Navbar/index.vue
@@ -1,9 +1,10 @@
-<script setup lang="ts">
+<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';
 
 interface NavbarProps {
   currentUser?: Partial<UserInfo>;
@@ -15,137 +16,256 @@ defineProps<NavbarProps>();
 
 const menuStore = useMenuStore();
 const baseConfig = getConfig().base;
-
-// 使用storeToRefs保持解构属性的响应性
 const { treeMenus: mainMenus } = storeToRefs(menuStore);
-
 const title = baseConfig.title;
+
+const { currentTheme, setTheme } = useTheme();
 </script>
 
 <template>
-  <div class="navbar">
-    <div class="left">
-      <div class="logo">
-        <!-- <Login /> -->
-        <div class="logoName">{{ title }}</div>
+  <header class="navbar">
+    <!-- 左侧品牌 -->
+    <div class="navbar-left">
+      <div class="brand">
+        <div class="brand-icon">
+          <svg
+            width="20"
+            height="20"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="2.2"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          >
+            <path
+              d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
+            />
+            <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
+            <line x1="12" y1="22.08" x2="12" y2="12" />
+          </svg>
+        </div>
+        <span class="brand-name">{{ title }}</span>
       </div>
     </div>
 
-    <div class="right">
-      <!-- 顶部导航信息 -->
-      <div class="menu-wrapper">
-        <Menu :tabPanes="mainMenus" />
+    <!-- 中间菜单 -->
+    <div class="navbar-center">
+      <Menu :tabPanes="mainMenus" />
+    </div>
+
+    <!-- 右侧用户 -->
+    <div class="navbar-right">
+      <!-- 主题切换 -->
+      <div class="theme-switcher">
+        <button
+          v-for="theme in THEMES"
+          :key="theme.id"
+          class="theme-btn"
+          :class="{ 'theme-btn--active': currentTheme === theme.id }"
+          :title="theme.description"
+          @click="setTheme(theme.id)"
+        >
+          <span class="theme-btn-icon">{{ theme.icon }}</span>
+          <span class="theme-btn-label">{{ theme.label }}</span>
+        </button>
       </div>
 
-      <!-- 顶部导航操作栏 -->
-      <div class="actions-wrapper">
-        <!-- 头像 -->
-        <div class="user-info">
-          <span class="userName">
-            {{ currentUser?.displayName || currentUser?.name || '' }}
-          </span>
+      <div class="user-info">
+        <div class="user-avatar">
+          {{ (currentUser?.displayName || currentUser?.name || 'U').charAt(0).toUpperCase() }}
         </div>
-        <div class="actionItem" @click="logout">退出</div>
+        <span class="user-name">{{ currentUser?.displayName || currentUser?.name || '' }}</span>
       </div>
+      <button class="logout-btn" @click="logout">
+        <svg
+          width="15"
+          height="15"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          stroke-width="2"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+        >
+          <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
+          <polyline points="16 17 21 12 16 7" />
+          <line x1="21" y1="12" x2="9" y2="12" />
+        </svg>
+        <span>退出</span>
+      </button>
     </div>
-  </div>
+  </header>
 </template>
 
 <style lang="scss" scoped>
-$left-width: 200px;
+$brand-width: 220px;
 
 .navbar {
   display: flex;
-  box-sizing: border-box;
-  background-color: #e9f6fe;
+  align-items: center;
+  height: 56px;
+  background: rgba(255, 255, 255, 0.82);
+  backdrop-filter: blur(14px);
+  -webkit-backdrop-filter: blur(14px);
+  border-bottom: 1px solid rgba(226, 224, 220, 0.6);
+  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.03);
+}
+
+// ---- 左侧品牌 ----
+.navbar-left {
+  width: $brand-width;
+  height: 56px;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+}
+
+.brand {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding-left: 20px;
+}
+
+.brand-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 34px;
+  height: 34px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
+  color: #ffffff;
+  flex-shrink: 0;
+  box-shadow: 0 2px 8px rgba(30, 64, 175, 0.2);
+}
+
+.brand-name {
+  font-weight: 700;
+  font-size: 18px;
+  color: #1e293b;
+  letter-spacing: -0.02em;
+  white-space: nowrap;
+}
+
+// ---- 中间菜单 ----
+.navbar-center {
+  flex: 1;
+  height: 56px;
+  overflow: hidden;
+  min-width: 0;
+}
+
+// ---- 右侧用户 ----
+.navbar-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding-right: 20px;
+  flex-shrink: 0;
   height: 56px;
-  /* 固定导航栏高度为56px */
-  position: relative;
-
-  .left {
-    display: flex;
-    align-items: center;
-    width: 200px;
-    height: 56px;
-    /* 确保左侧内容也是56px高 */
-
-    .logo {
-      display: flex;
-      align-items: center;
-      width: $left-width;
-      padding-left: 20px;
-      box-sizing: border-box;
-    }
-
-    .logoName {
-      font-weight: 500;
-      font-size: 20px;
-      margin-left: 10px;
-      font-family:
-        Alimama ShuHeiTi,
-        Alimama ShuHeiTi;
-      font-weight: bold;
-      font-size: 20px;
-      color: #1d2129;
-      line-height: 28px;
-      text-align: left;
-      font-style: normal;
-      text-transform: none;
-    }
+}
+
+// ---- 主题切换 ----
+.theme-switcher {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  background: rgba(0, 0, 0, 0.04);
+  border-radius: 8px;
+  padding: 3px;
+}
+
+.theme-btn {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  padding: 4px 10px;
+  border: none;
+  border-radius: 6px;
+  background: transparent;
+  cursor: pointer;
+  font-size: 12px;
+  font-family: 'Inter', sans-serif;
+  font-weight: 500;
+  color: var(--color-text-tertiary, #64748b);
+  transition:
+    background 0.15s,
+    color 0.15s;
+  white-space: nowrap;
+
+  &:hover {
+    background: rgba(0, 0, 0, 0.06);
+    color: var(--color-text-primary, #1e293b);
+  }
+
+  &--active {
+    background: #ffffff;
+    color: var(--color-text-primary, #1e293b);
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  }
+}
+
+.theme-btn-icon {
+  font-size: 13px;
+  line-height: 1;
+}
+
+.theme-btn-label {
+  line-height: 1;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.user-avatar {
+  width: 32px;
+  height: 32px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+  color: #1e40af;
+  font-weight: 700;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.user-name {
+  font-size: 14px;
+  font-weight: 500;
+  color: #475569;
+  white-space: nowrap;
+}
+
+.logout-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 14px;
+  border: 1px solid #e2e0dc;
+  border-radius: 10px;
+  background: #ffffff;
+  color: #64748b;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  white-space: nowrap;
+
+  &:hover {
+    background: #fef2f2;
+    color: #dc2626;
+    border-color: #fecaca;
   }
 
-  .right {
-    display: flex;
-    justify-content: space-between;
-    list-style: none;
-    padding-right: 20px;
-    width: calc(100% - #{$left-width});
-    height: 56px;
-    /* 确保右侧区域高度也是56px */
-
-    .menu-wrapper {
-      flex: 1;
-      overflow: hidden;
-      /* 确保菜单不会超出其容器 */
-      margin-right: 20px;
-      /* 与操作区域保持间距 */
-    }
-
-    .actions-wrapper {
-      display: flex;
-      align-items: center;
-      justify-content: flex-end;
-      white-space: nowrap;
-      /* 防止文本换行 */
-      min-width: 150px;
-      /* 确保操作区域有最小宽度 */
-      padding-left: 15px;
-      /* 与菜单保持一定距离 */
-      z-index: 15;
-      /* 确保显示在菜单之上 */
-      background-color: #e9f6fe;
-      /* 与导航栏背景色一致 */
-
-      .user-info {
-        display: flex;
-        align-items: center;
-      }
-
-      .userName {
-        margin-left: 5px;
-        font-weight: 500;
-      }
-
-      .actionItem {
-        cursor: pointer;
-        margin-left: 15px;
-        color: #1890ff;
-
-        &:hover {
-          text-decoration: underline;
-        }
-      }
-    }
+  &:active {
+    background: #fee2e2;
   }
 }
 </style>
Modified +106 -186
diff --git a/core/layouts/MainLayout/Navbar/Menu/index.vue b/core/layouts/MainLayout/Navbar/Menu/index.vue
index c9f7a8b..0ab7156 100644
--- a/core/layouts/MainLayout/Navbar/Menu/index.vue
+++ b/core/layouts/MainLayout/Navbar/Menu/index.vue
@@ -7,11 +7,7 @@ import { ElScrollbar } from 'element-plus';
 import { hasChildren } from '@core/utils/menuHelpers';
 import { openMenuTab } from '@core/utils/menuTab';
 
-/**
- * Menu组件Props接口定义
- */
 interface MenuProps {
-  /** 菜单项数组 */
   tabPanes?: Array<TreeMenuItem>;
 }
 
@@ -22,11 +18,8 @@ const menuCascaderHeight = ref<number>(500);
 const router = useRouter();
 const menuStore = useMenuStore();
 const activeMenu = computed(() => menuStore.activeMenu);
-
-/** 当前鼠标悬浮位置菜单,层叠菜单组件需要 */
 const currentMenu = ref<TreeMenuItem | undefined>();
 
-// 滚动相关引用和状态
 const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null);
 const menuRef = ref<HTMLElement | null>(null);
 const showLeftArrow = ref(false);
@@ -35,90 +28,58 @@ const scrollPosition = ref(0);
 const scrollWidth = ref(0);
 const containerWidth = ref(0);
 
-/**
- * 更新箭头显示状态
- */
 const updateArrowVisibility = () => {
   if (!menuRef.value || !scrollbarRef.value) return;
-
   const wrapperElement = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
   if (!wrapperElement) return;
-
   scrollPosition.value = wrapperElement.scrollLeft;
   scrollWidth.value = menuRef.value.scrollWidth;
   containerWidth.value = wrapperElement.clientWidth;
-
-  // 显示/隐藏左右箭头
   showLeftArrow.value = scrollPosition.value > 0;
-
-  // 添加2px的容差值,解决右侧边缘判断精度问题
-  const scrollRightTolerance = 2; // 2像素的容差值
+  const tolerance = 2;
   showRightArrow.value =
     scrollWidth.value > containerWidth.value &&
-    scrollPosition.value < scrollWidth.value - containerWidth.value - scrollRightTolerance;
+    scrollPosition.value < scrollWidth.value - containerWidth.value - tolerance;
 };
 
-/**
- * 向左滚动
- */
 const scrollLeft = () => {
   if (!scrollbarRef.value?.$el) return;
-  const wrapperElement = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
-  if (!wrapperElement) return;
-
-  wrapperElement.scrollLeft -= 200; // 滚动200px
+  const w = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
+  if (!w) return;
+  w.scrollLeft -= 200;
   updateArrowVisibility();
 };
 
-/**
- * 向右滚动
- */
 const scrollRight = () => {
   if (!scrollbarRef.value?.$el) return;
-  const wrapperElement = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
-  if (!wrapperElement) return;
-
-  wrapperElement.scrollLeft += 200; // 滚动200px
+  const w = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
+  if (!w) return;
+  w.scrollLeft += 200;
   updateArrowVisibility();
 };
 
-/**
- * 处理鼠标滚轮事件
- */
 const handleWheel = (e: WheelEvent) => {
   if (!scrollbarRef.value?.$el) return;
-  const wrapperElement = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
-  if (!wrapperElement) return;
-
-  // 阻止默认的垂直滚动
+  const w = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
+  if (!w) return;
   e.preventDefault();
-
-  // 水平滚动距离
-  const delta = e.deltaY || e.deltaX;
-  wrapperElement.scrollLeft += delta;
+  w.scrollLeft += e.deltaY || e.deltaX;
   updateArrowVisibility();
 };
 
-// 组件挂载后初始化
 onMounted(async () => {
   await nextTick();
-
-  // 初始化箭头状态
   updateArrowVisibility();
-
-  // 添加滚动事件监听
   window.addEventListener('resize', updateArrowVisibility);
   if (scrollbarRef.value?.$el) {
-    const wrapperElement = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
-    if (wrapperElement) {
-      wrapperElement.addEventListener('scroll', updateArrowVisibility);
-      // 添加鼠标滚轮事件
+    const w = scrollbarRef.value.$el.querySelector('.el-scrollbar__wrap');
+    if (w) {
+      w.addEventListener('scroll', updateArrowVisibility);
       scrollbarRef.value.$el.addEventListener('wheel', handleWheel, { passive: false });
     }
   }
 });
 
-// 监听菜单项变化,更新箭头态
 watch(
   () => props.tabPanes,
   async () => {
@@ -128,113 +89,51 @@ watch(
   { deep: true },
 );
 
-/**
- * 计算是否显示层叠菜单
- */
 const showCascaderMenu = computed(() => {
-  // 如果是顶级菜单,但是没有子菜单,不显示
-  if (currentMenu.value && !currentMenu.value.parentMenu) {
-    return hasChildren(currentMenu.value);
-  }
-
-  // 子菜单,显示
+  if (currentMenu.value && !currentMenu.value.parentMenu) return hasChildren(currentMenu.value);
   return currentMenu.value !== undefined;
 });
 
-/**
- * 检查菜单项是否应该高亮显示
- * @param menu 要检查的菜单项
- * @returns 是否应该高亮显示
- */
 const isMenuActive = (menu: TreeMenuItem) => {
-  // 直接判断顶级活动菜单路径匹配
-  if (menuStore.topLevelActiveMenu?.path === menu.path) {
-    return true;
-  }
-
-  // 通过ID匹配顶级活动菜单
-  if (menuStore.topLevelActiveMenu?.id === menu.id) {
-    return true;
-  }
-
-  // 判断当前激活菜单是否是该菜单的子菜单
-  if (activeMenu.value && isChildOf(activeMenu.value, menu)) {
-    return true;
-  }
-
-  // 当前路由路径匹配菜单路径
-  if (router.currentRoute.value.path === menu.path) {
-    return true;
-  }
-
+  if (menuStore.topLevelActiveMenu?.path === menu.path) return true;
+  if (menuStore.topLevelActiveMenu?.id === menu.id) return true;
+  if (activeMenu.value && isChildOf(activeMenu.value, menu)) return true;
+  if (router.currentRoute.value.path === menu.path) return true;
   return false;
 };
 
-/**
- * 检查一个菜单是否是另一个菜单的子菜单(包括多级子菜单)
- * @param child 可能的子菜单
- * @param parent 可能的父菜单
- * @returns 是否是子菜单关系
- */
 const isChildOf = (child: TreeMenuItem, parent: TreeMenuItem) => {
   if (!child || !parent) return false;
   if (child.id === parent.id) return true;
-
-  // 向上遍历查找父菜单
-  let currentParent = child.parentMenu;
-  while (currentParent) {
-    if (currentParent.id === parent.id) return true;
-    currentParent = currentParent.parentMenu;
+  let p = child.parentMenu;
+  while (p) {
+    if (p.id === parent.id) return true;
+    p = p.parentMenu;
   }
-
   return false;
 };
 
-/**
- * 鼠标悬浮在菜单处处理
- * @param event 鼠标事件
- * @param menuItem 菜单项
- */
 const handleMouseEnter = (event: MouseEvent, menuItem: TreeMenuItem) => {
   event.preventDefault();
   event.stopPropagation();
-
   const target = event.target as HTMLElement;
-
-  /** 层叠菜单展示的左边距 */
   let x = event.clientX - (target?.clientWidth || 0);
-
-  // 菜单宽度大于屏幕宽度时,计算偏移量
-  if (x + cascaderMenuWidth > window.innerWidth) {
-    x = window.innerWidth - cascaderMenuWidth;
-  }
-
+  if (x + cascaderMenuWidth > window.innerWidth) x = window.innerWidth - cascaderMenuWidth;
   clientX.value = x;
   currentMenu.value = menuItem;
 };
 
-/**
- * 鼠标离开菜单项处理
- */
 const handleMouseLeave = (event: MouseEvent) => {
   event.preventDefault();
   event.stopPropagation();
-
   currentMenu.value = undefined;
 };
 
-/**
- * 遮罩触发处理
- */
 const handleMaskTrigger = (e: MouseEvent) => {
-  // 只有当鼠标移动到遮罩区域,而不是导航栏或级联菜单时才隐藏
   const target = e.target as HTMLElement;
   const relatedTarget = e.relatedTarget as HTMLElement;
-
-  //  如果鼠标从菜单区域移出到遮罩区域,才隐藏菜单
   if (
-    target &&
-    target.classList.contains('side-mask') &&
+    target?.classList.contains('side-mask') &&
     !relatedTarget?.closest('.menu-cascader-parent') &&
     !relatedTarget?.closest('.menu-container')
   ) {
@@ -242,27 +141,29 @@ const handleMaskTrigger = (e: MouseEvent) => {
   }
 };
 
-/**
- * 菜单点击处理
- * @param menu 菜单项
- */
 const onMenuClick = (menu: TreeMenuItem) => {
-  openMenuTab({
-    url: menu.path,
-    title: menu.name,
-  });
-
+  openMenuTab({ url: menu.path, title: menu.name });
   menuStore.setActiveMenu(menu);
-
   currentMenu.value = undefined;
 };
 </script>
 
 <template>
   <div class="menu-container" @mouseleave="handleMouseLeave">
-    <!-- 左箭头按钮 -->
-    <div class="scroll-arrow left-arrow" @click="scrollLeft" v-show="showLeftArrow">
-      <i class="el-icon-arrow-left">&lt;</i>
+    <!-- 左箭头 -->
+    <div class="scroll-arrow scroll-arrow--left" @click="scrollLeft" v-show="showLeftArrow">
+      <svg
+        width="14"
+        height="14"
+        viewBox="0 0 24 24"
+        fill="none"
+        stroke="currentColor"
+        stroke-width="2.5"
+        stroke-linecap="round"
+        stroke-linejoin="round"
+      >
+        <polyline points="15 18 9 12 15 6" />
+      </svg>
     </div>
 
     <ElScrollbar class="menu-scrollbar" ref="scrollbarRef">
@@ -270,7 +171,7 @@ const onMenuClick = (menu: TreeMenuItem) => {
         <li v-for="(tabPane, index) in props.tabPanes" :key="index">
           <div
             class="menu-item"
-            :class="{ 'menu-item-active': isMenuActive(tabPane) }"
+            :class="{ 'menu-item--active': isMenuActive(tabPane) }"
             @mouseenter="(e) => handleMouseEnter(e, tabPane)"
           >
             {{ tabPane.title || tabPane.name }}
@@ -279,11 +180,23 @@ const onMenuClick = (menu: TreeMenuItem) => {
       </div>
     </ElScrollbar>
 
-    <!-- 右箭头按钮 -->
-    <div class="scroll-arrow right-arrow" @click="scrollRight" v-show="showRightArrow">
-      <i class="el-icon-arrow-right">&gt;</i>
+    <!-- 右箭头 -->
+    <div class="scroll-arrow scroll-arrow--right" @click="scrollRight" v-show="showRightArrow">
+      <svg
+        width="14"
+        height="14"
+        viewBox="0 0 24 24"
+        fill="none"
+        stroke="currentColor"
+        stroke-width="2.5"
+        stroke-linecap="round"
+        stroke-linejoin="round"
+      >
+        <polyline points="9 18 15 12 9 6" />
+      </svg>
     </div>
-    <!-- 只渲染当前鼠标悬停的一级菜单的级联菜单 -->
+
+    <!-- 层叠菜单 -->
     <div
       v-if="currentMenu"
       :style="{ left: `${clientX}px`, height: `${menuCascaderHeight}px` }"
@@ -297,6 +210,8 @@ const onMenuClick = (menu: TreeMenuItem) => {
         :onMenuClick="onMenuClick"
       />
     </div>
+
+    <!-- 遮罩 -->
     <div
       v-if="showCascaderMenu"
       class="side-mask"
@@ -313,7 +228,7 @@ const onMenuClick = (menu: TreeMenuItem) => {
   height: 56px;
   display: flex;
   align-items: center;
-  overflow: hidden; /* 确保内容不会溢出 */
+  overflow: hidden;
 }
 
 .scroll-arrow {
@@ -321,50 +236,50 @@ const onMenuClick = (menu: TreeMenuItem) => {
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 24px;
-  height: 56px;
-  background-color: rgba(233, 246, 254, 0.9);
-  color: #1890ff;
+  width: 28px;
+  height: 36px;
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.9);
+  color: #64748b;
   cursor: pointer;
-  z-index: 20; /* 提高层级确保显示在其他元素之上 */
+  z-index: 20;
+  backdrop-filter: blur(6px);
+  border: 1px solid #e2e0dc;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+  transition: all 0.18s ease;
 
   &:hover {
-    background-color: rgba(233, 246, 254, 1);
+    background: #ffffff;
+    color: #1e40af;
+    border-color: #bfdbfe;
+    box-shadow: 0 2px 8px rgba(30, 64, 175, 0.1);
   }
 
-  &.left-arrow {
-    left: 0;
-    box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
+  &--left {
+    left: 4px;
   }
-
-  &.right-arrow {
-    right: 0;
-    box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
+  &--right {
+    right: 4px;
   }
 }
 
 .menu-scrollbar {
-  flex: 1; /* 使用flex布局占据所有可用空间 */
+  flex: 1;
   width: 100%;
   height: 63px;
-  overflow: hidden; /* 确保不会出现额外的滚动条 */
+  overflow: hidden;
 
   :deep(.el-scrollbar__wrap) {
     overflow-x: auto;
     overflow-y: hidden;
-
-    /* 完全隐藏滚动条,但保留滚动功能 */
     scrollbar-width: none;
     -ms-overflow-style: none;
-
     &::-webkit-scrollbar {
       width: 0;
       height: 0;
       display: none;
     }
   }
-
-  /* 覆盖Element Plus默认滚动条样式 */
   :deep(.el-scrollbar__bar) {
     display: none !important;
   }
@@ -376,40 +291,45 @@ const onMenuClick = (menu: TreeMenuItem) => {
   margin: 0;
   padding: 0 10px;
   list-style: none;
-  white-space: nowrap; /* 防止菜单项换行 */
-  min-width: max-content; /* 确保内容不会被压缩 */
-  box-sizing: border-box;
-  width: fit-content; /* 适应内容宽度 */
+  white-space: nowrap;
+  min-width: max-content;
+  width: fit-content;
 
   .menu-item {
     display: flex;
     align-items: center;
     height: 100%;
-    margin: auto 32px auto 5px;
+    margin: auto 28px auto 4px;
     cursor: pointer;
     font-weight: 600;
-    color: #6d6f72;
-    padding: 0 5px;
+    font-size: 14px;
+    color: #64748b;
+    padding: 0 6px;
+    position: relative;
     transition: color 0.2s ease;
 
-    &-active {
-      color: #1890ff;
-      font-weight: 600;
-      position: relative;
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 0;
+      height: 2.5px;
+      background: linear-gradient(90deg, #1e40af, #2563eb);
+      border-radius: 2px 2px 0 0;
+      transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+    }
 
+    &--active {
+      color: #1e40af;
       &::after {
-        content: '';
-        position: absolute;
-        bottom: 0;
-        left: 0;
         width: 100%;
-        height: 2px;
-        background-color: #1890ff;
       }
     }
 
     &:hover {
-      color: #1890ff;
+      color: #1e40af;
     }
   }
 }
@@ -427,8 +347,8 @@ const onMenuClick = (menu: TreeMenuItem) => {
   position: fixed;
   inset: 56px 0 0;
   z-index: 1080;
-  background-color: #000;
-  cursor: pointer;
-  opacity: 0.1;
+  background: #0f172a;
+  opacity: 0.12;
+  backdrop-filter: blur(1px);
 }
 </style>
Modified +159 -95
diff --git a/core/layouts/MainLayout/Sider/index.vue b/core/layouts/MainLayout/Sider/index.vue
index 2903c43..77761ed 100644
--- a/core/layouts/MainLayout/Sider/index.vue
+++ b/core/layouts/MainLayout/Sider/index.vue
@@ -1,124 +1,188 @@
 <script setup lang="ts">
 import { computed } from 'vue';
-import { openMenuTab } from '@core/utils/menuTab';
-import { useMenuStore, type TreeMenuItem } from '@core/stores/menu';
-import { renderMenuTitle } from '@core/utils/menuHelpers';
-import TextOverflow from '@core/components/TextOverflow.vue';
+import { useMenuStore } from '@core/stores/menu';
+import SideMenuItem from './SideMenuItem.vue';
 
 const menuStore = useMenuStore();
 
-const parentMenu = computed(() => {
-  return menuStore.activeMenu?.parentMenu;
-});
-
-/** 点击菜单处理 */
-const handleMenuClick = (menu: TreeMenuItem) => {
-  openMenuTab({
-    url: menu.path,
-    title: menu.name,
-  });
-
-  menuStore.setActiveMenu(menu);
-};
-
-function isMenuActive(menu: TreeMenuItem) {
-  return menu.path === menuStore.activeMenu?.path;
-}
+const parentMenu = computed(() => menuStore.activeMenu?.parentMenu);
+const secondLevelMenus = computed(() => parentMenu.value?.children || []);
 </script>
 
 <template>
-  <div class="sider">
-    <!-- 菜单容器 -->
-    <div class="menuContainer">
-      <!-- 一级菜单名,带图标 -->
-      <div class="title">
-        <!-- <Icon v-if="parentMenu?.icon" :type="parentMenu.icon" /> -->
-        <span>{{ parentMenu?.title }}</span>
+  <aside class="sider">
+    <!-- 顶部标题区 -->
+    <div class="sider-header">
+      <div class="sider-header-icon">
+        <svg
+          width="18"
+          height="18"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          stroke-width="2"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+        >
+          <rect x="3" y="3" width="7" height="7" rx="1.2" />
+          <rect x="14" y="3" width="7" height="7" rx="1.2" />
+          <rect x="3" y="14" width="7" height="7" rx="1.2" />
+          <rect x="14" y="14" width="7" height="7" rx="1.2" />
+        </svg>
       </div>
-      <!-- 二级菜单名 -->
-      <div
-        v-for="item in parentMenu?.children"
-        :key="item.name"
-        :class="['menuName', isMenuActive(item) && 'active']"
-        @click="() => handleMenuClick(item)"
-      >
-        <TextOverflow
-          :text="renderMenuTitle(item)"
-          tooltip-placement="right"
-          class="menu-title-text"
+      <span class="sider-header-title">
+        {{ parentMenu?.title || parentMenu?.name || '导航菜单' }}
+      </span>
+    </div>
+
+    <!-- 分割线 -->
+    <div class="sider-divider" />
+
+    <!-- 菜单树 -->
+    <nav class="sider-nav">
+      <div class="sider-nav-inner">
+        <SideMenuItem
+          v-for="menu in secondLevelMenus"
+          :key="menu.id"
+          :menu="menu"
+          :depth="0"
+          :activeMenu="menuStore.activeMenu"
         />
       </div>
+    </nav>
+
+    <!-- 底部 -->
+    <div class="sider-footer">
+      <button class="sider-collapse-btn" title="折叠菜单">
+        <svg
+          width="16"
+          height="16"
+          viewBox="0 0 16 16"
+          fill="none"
+          stroke="currentColor"
+          stroke-width="1.5"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+        >
+          <path d="M10 3L5 8L10 13" />
+        </svg>
+      </button>
     </div>
-  </div>
+  </aside>
 </template>
 
 <style lang="scss" scoped>
 .sider {
+  display: flex;
+  flex-direction: column;
   width: 100%;
   height: 100%;
-  background-color: #e9f6fe;
+  background: rgba(255, 255, 255, 0.78);
+  backdrop-filter: blur(14px);
+  -webkit-backdrop-filter: blur(14px);
+  border-right: 1px solid rgba(226, 224, 220, 0.5);
+  box-shadow: 1px 0 12px rgba(0, 0, 0, 0.02);
+}
 
-  .menuContainer {
-    height: 100%;
-    overflow-y: auto;
+// ---- 顶部标题 ----
+.sider-header {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 20px 16px 12px;
+  flex-shrink: 0;
+}
 
-    /* 完全隐藏滚动条 */
-    scrollbar-width: none;
-    -ms-overflow-style: none;
+.sider-header-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+  color: #1e40af;
+  flex-shrink: 0;
+}
 
-    &::-webkit-scrollbar {
-      width: 0;
-      height: 0;
-      display: none;
-    }
+.sider-header-title {
+  font-size: 15px;
+  font-weight: 700;
+  color: #1e293b;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  letter-spacing: -0.01em;
+}
+
+// ---- 分割线 ----
+.sider-divider {
+  height: 1px;
+  margin: 0 16px;
+  background: linear-gradient(to right, transparent 0%, #e2e0dc 20%, #e2e0dc 80%, transparent 100%);
+  flex-shrink: 0;
+}
+
+// ---- 菜单导航 ----
+.sider-nav {
+  flex: 1;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 8px 0;
 
-    /* 悬浮时可使用滚动,但保持滚动条隐藏 */
+  scrollbar-width: thin;
+  scrollbar-color: #d5cec4 transparent;
+
+  &::-webkit-scrollbar {
+    width: 4px;
+  }
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #d5cec4;
+    border-radius: 4px;
     &:hover {
-      overflow-y: auto;
-    }
-    .title {
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      font-weight: 600;
-      font-size: 14px;
-      color: #86909c;
-      margin-bottom: 16px;
-
-      span {
-        margin-left: 4px;
-      }
+      background: #b8b3bf;
     }
+  }
+}
 
-    .menuName {
-      height: 32px;
-      font-weight: 600;
-      font-size: 14px;
-      // color: #1D2129;
-      color: #6d6f72;
-      line-height: 20px;
-      margin: 0 8px 4px 8px;
-      padding: 6px 0 6px 20px;
-      text-align: left;
-      user-select: none;
-      cursor: pointer;
-
-      .menu-title-text {
-        /* 继承菜单项样式 */
-        width: 100%;
-      }
-
-      &:hover {
-        color: #007fff;
-      }
-    }
+.sider-nav-inner {
+  padding-bottom: 8px;
+}
 
-    .active {
-      background: rgba(255, 255, 255, 0.7);
-      border-radius: 8px;
-      color: #007fff;
-      box-shadow: 0px 2px 8px 0px rgba(92, 173, 255, 0.1);
-    }
+// ---- 底部 ----
+.sider-footer {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 8px 16px 12px;
+  flex-shrink: 0;
+  border-top: 1px solid #f0ece6;
+}
+
+.sider-collapse-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border: 1px solid #e2e0dc;
+  border-radius: 10px;
+  background: #ffffff;
+  color: #94a3b8;
+  cursor: pointer;
+  transition: all 0.18s ease;
+
+  &:hover {
+    background: #f8fafc;
+    color: #64748b;
+    border-color: #cbd5e1;
+  }
+
+  &:active {
+    background: #f1f5f9;
   }
 }
 </style>
Added +266 -0
diff --git a/core/layouts/MainLayout/Sider/SideMenuItem.vue b/core/layouts/MainLayout/Sider/SideMenuItem.vue
new file mode 100644
index 0000000..606624a
--- /dev/null
+++ b/core/layouts/MainLayout/Sider/SideMenuItem.vue
@@ -0,0 +1,266 @@
+<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';
+
+const props = withDefaults(
+  defineProps<{
+    menu: TreeMenuItem;
+    depth?: number;
+    activeMenu?: TreeMenuItem;
+  }>(),
+  { depth: 0 },
+);
+
+const menuStore = useMenuStore();
+const isExpanded = ref(false);
+
+const hasChildrenMenu = computed(() => hasChildren(props.menu));
+
+const isActive = computed(
+  () => !hasChildrenMenu.value && isChildMenu(props.activeMenu, props.menu),
+);
+
+const isAncestorOfActive = computed(() => {
+  if (!props.activeMenu || !hasChildrenMenu.value) return false;
+  let current: TreeMenuItem | undefined = props.activeMenu.parentMenu;
+  while (current) {
+    if (current.id === props.menu.id) return true;
+    current = current.parentMenu;
+  }
+  return false;
+});
+
+// 自动展开 activeMenu 的祖先链路
+watch(
+  () => props.activeMenu,
+  (newActive) => {
+    if (newActive && hasChildrenMenu.value) {
+      let current: TreeMenuItem | undefined = newActive.parentMenu;
+      while (current) {
+        if (current.id === props.menu.id) {
+          isExpanded.value = true;
+          return;
+        }
+        current = current.parentMenu;
+      }
+    }
+  },
+  { immediate: true },
+);
+
+const handleClick = () => {
+  if (hasChildrenMenu.value) {
+    isExpanded.value = !isExpanded.value;
+  } else {
+    openMenuTab({ url: props.menu.path, title: props.menu.name });
+    menuStore.setActiveMenu(props.menu);
+  }
+};
+</script>
+
+<template>
+  <div class="side-menu-item">
+    <!-- 菜单行 -->
+    <div
+      :class="[
+        'menu-row',
+        {
+          'menu-row--active': isActive,
+          'menu-row--ancestor': isAncestorOfActive && isExpanded,
+        },
+      ]"
+      :style="{ paddingLeft: `${depth * 16 + 16}px` }"
+      @click="handleClick"
+    >
+      <!-- 展开箭头 -->
+      <span
+        v-if="hasChildrenMenu"
+        class="menu-row-arrow"
+        :class="{ 'menu-row-arrow--expanded': isExpanded }"
+      >
+        <svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="arrow-svg">
+          <path
+            d="M4.5 2.5L8 6L4.5 9.5"
+            stroke="currentColor"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+        </svg>
+      </span>
+      <span v-else class="menu-row-arrow-placeholder" />
+
+      <!-- 图标 -->
+      <span v-if="menu.icon" class="menu-row-icon">
+        <i :class="menu.icon"></i>
+      </span>
+
+      <!-- 标题 -->
+      <span class="menu-row-title">{{ renderMenuTitle(menu) }}</span>
+
+      <!-- 子菜单计数 -->
+      <span v-if="hasChildrenMenu" class="menu-row-badge">
+        {{ menu.children?.length }}
+      </span>
+    </div>
+
+    <!-- 子菜单容器 -->
+    <div
+      v-if="hasChildrenMenu"
+      class="menu-children"
+      :class="{ 'menu-children--expanded': isExpanded }"
+    >
+      <div class="menu-children-inner">
+        <SideMenuItem
+          v-for="child in menu.children"
+          :key="child.id"
+          :menu="child"
+          :depth="depth + 1"
+          :activeMenu="activeMenu"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.menu-row {
+  display: flex;
+  align-items: center;
+  height: 40px;
+  padding-right: 12px;
+  cursor: pointer;
+  user-select: none;
+  position: relative;
+  transition:
+    background-color 0.15s ease,
+    color 0.15s ease;
+  border-radius: 0 10px 10px 0;
+  margin: 1px 8px 1px 0;
+
+  // 左侧激活指示条
+  &::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 3px;
+    height: 0;
+    background: linear-gradient(180deg, #1e40af 0%, #2563eb 100%);
+    border-radius: 0 3px 3px 0;
+    transition: height 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+  }
+
+  &:hover {
+    background: #f3f0ed;
+  }
+
+  // 激活态(叶子节点)
+  &--active {
+    background: linear-gradient(90deg, #eff6ff 0%, rgba(239, 246, 255, 0.4) 100%);
+    color: #1e40af;
+    font-weight: 600;
+
+    &::before {
+      height: 24px;
+    }
+
+    .menu-row-title {
+      color: #1e40af;
+    }
+    .menu-row-icon {
+      color: #1e40af;
+    }
+  }
+
+  // 展开的祖先节点
+  &--ancestor {
+    .menu-row-title {
+      color: #1e3a8a;
+      font-weight: 600;
+    }
+  }
+}
+
+.menu-row-arrow {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 20px;
+  height: 20px;
+  margin-right: 2px;
+  color: #94a3b8;
+  transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+  flex-shrink: 0;
+
+  &--expanded {
+    transform: rotate(90deg);
+    color: #64748b;
+  }
+
+  .arrow-svg {
+    display: block;
+  }
+}
+
+.menu-row-arrow-placeholder {
+  width: 20px;
+  height: 20px;
+  margin-right: 2px;
+  flex-shrink: 0;
+}
+
+.menu-row-icon {
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  font-size: 16px;
+  color: #94a3b8;
+  flex-shrink: 0;
+  transition: color 0.15s ease;
+}
+
+.menu-row-title {
+  font-size: 14px;
+  color: #475569;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.4;
+  flex: 1;
+  transition: color 0.15s ease;
+}
+
+.menu-row-badge {
+  font-size: 11px;
+  color: #64748b;
+  background: #f1f5f9;
+  padding: 1px 7px;
+  border-radius: 10px;
+  margin-left: 8px;
+  flex-shrink: 0;
+  font-weight: 500;
+  transition:
+    background 0.15s ease,
+    color 0.15s ease;
+}
+
+// 子菜单展开动画
+.menu-children {
+  display: grid;
+  grid-template-rows: 0fr;
+  transition: grid-template-rows 0.28s cubic-bezier(0.4, 0, 0.2, 1);
+
+  &--expanded {
+    grid-template-rows: 1fr;
+  }
+}
+
+.menu-children-inner {
+  overflow: hidden;
+}
+</style>