浏览代码

BottomDock 重构:抽公共菜单配置 + 修最顶端遮挡 + 修箭头切换不准

  - src/config/menus.js (新增): 全应用顶层菜单单一数据源, 含 allMenus
    (10 项: 6 径向 + 4 溢出) 和 toDockItems() 字段适配器, Main 与
    DashboardLayout 共享
  - views/Main.vue: 删除内联 menuList, 改用 allMenus.slice(0,6) 给径向左/右,
    slice(6) -> toDockItems() 给 BottomDock; <BottomDock> 仅在溢出非空时渲染
  - layouts/DashboardLayout.vue: <BottomDock> 从 .center-area 内提到
    .fluid-dashboard 根级, 脱离 .ui-layer (z-index:2) 子上下文, 让 dock 不再
    被地图图例 (.map-legend z:100) 盖住; 接入 dockMenus computed 传 allMenus
  - components/ui/BottomDock.vue:
    - 新增 items prop (默认空数组), 删内置 13 项硬编码 dockItems
    - 根 div v-if="items && items.length", 空数据时不渲染 (Main 0 项时整个隐藏)
    - z-index 9999 -> 500: 介于地图图例 (100) 与 SmartDialog (2000+) 之间,
      dock 盖图例, 弹窗盖 dock
    - scrollList(direction) 不再用 scrollBy(±130) 估算步长, 改 getBoundingClientRect
      算下一个 item 实际左边界后精确 scrollTo, 修复连续切换累计偏移
    - 加 scroll-snap-type: x mandatory + scroll-snap-align: start, 拖动 / 滚轮
      自动吸附到 item 边界, 与 JS scrollTo 双保险
画安 2 月之前
父节点
当前提交
b28a78a83e
共有 4 个文件被更改,包括 231 次插入201 次删除
  1. 87 126
      src/components/ui/BottomDock.vue
  2. 108 0
      src/config/menus.js
  3. 11 3
      src/layouts/DashboardLayout.vue
  4. 25 72
      src/views/Main.vue

+ 87 - 126
src/components/ui/BottomDock.vue

@@ -1,5 +1,6 @@
 <template>
     <div class="dock-wrapper"
+         v-if="items && items.length"
          ref="dockWrapper"
          :class="[
             autoHide ? 'is-auto-hide' : 'is-always-show',
@@ -8,31 +9,30 @@
          ]"
          :style="dockStyles"
          @mouseleave="handleDockLeave">
-         
-        <div class="nav-arrow left-arrow" :class="{ 'is-disabled': !canScrollLeft, 'is-active': canScrollLeft }"
-            @click="scrollList(-1)">
+
+        <div class="nav-arrow left-arrow"
+             :class="{ 'is-disabled': !canScrollLeft, 'is-active': canScrollLeft }"
+             @click="scrollList(-1)">
             <img v-if="canScrollLeft" src="@/assets/main/main-right.png" class="arrow-img left-facing" />
             <img v-else src="@/assets/main/main-left.png" class="arrow-img" />
         </div>
 
         <div class="dock-list-container" ref="listContainer" @scroll="checkScrollState">
             <div class="dock-list">
-                <div v-for="(item, index) in dockItems" :key="index" class="dock-item"
-                    :class="{ 
-                        'is-active': activeIndex === index, 
+                <div v-for="(item, index) in items" :key="index" class="dock-item"
+                    :class="{
+                        'is-active': activeIndex === index,
                         [`theme-${item.theme}`]: item.theme,
                         'is-breathing': waveAnimation
                     }"
                     :style="waveAnimation ? { animationDelay: `${index * 0.15}s` } : {}"
                     @click="handleSelect(index, item)"
                     @mouseenter="hoverIndex = index"
-                    @mouseleave="hoverIndex = null"
-                    >
+                    @mouseleave="hoverIndex = null">
                     <div class="item-icon">
-                        <img v-if="item.imgUrl" 
-                        :src="(activeIndex === index || hoverIndex === index) && item.activeImgUrl ? item.activeImgUrl : item.imgUrl" 
-                        class="custom-icon" />
-
+                        <img v-if="item.imgUrl"
+                            :src="(activeIndex === index || hoverIndex === index) && item.activeImgUrl ? item.activeImgUrl : item.imgUrl"
+                            class="custom-icon" />
                         <i v-else :class="item.iconClass"></i>
                     </div>
                     <div class="item-label">{{ item.label }}</div>
@@ -40,8 +40,9 @@
             </div>
         </div>
 
-        <div class="nav-arrow right-arrow" :class="{ 'is-disabled': !canScrollRight, 'is-active': canScrollRight }"
-            @click="scrollList(1)">
+        <div class="nav-arrow right-arrow"
+             :class="{ 'is-disabled': !canScrollRight, 'is-active': canScrollRight }"
+             @click="scrollList(1)">
             <img v-if="canScrollRight" src="@/assets/main/main-right.png" class="arrow-img" />
             <img v-else src="@/assets/main/main-left.png" class="arrow-img left-facing" />
         </div>
@@ -57,17 +58,17 @@ export default {
             type: Boolean,
             default: true
         },
-        // 距离屏幕底部的偏移量 (单位 px正数代表往上抬高)
+        // 距离屏幕底部的偏移量 (单位 px, 正数代表往上抬高)
         bottomOffset: {
             type: Number,
             default: 0
         },
-        // 允许外部传入的自定义容器样式 (如背景色、宽度等)
+        // 允许外部传入的自定义容器样式
         customStyle: {
             type: Object,
             default: () => ({})
         },
-        // 允许外部传入自定义 class (支持字符串、数组、对象)
+        // 允许外部传入自定义 class
         customClass: {
             type: [String, Array, Object],
             default: ''
@@ -76,6 +77,11 @@ export default {
         waveAnimation: {
             type: Boolean,
             default: false
+        },
+        // 菜单数据 (一般来自 src/config/menus.js)
+        items: {
+            type: Array,
+            default: () => []
         }
     },
     data() {
@@ -85,80 +91,22 @@ export default {
             dockExpanded: false,
             canScrollLeft: false,
             canScrollRight: true,
-            dockItems: [
-                {
-                    label: '首页',
-                    imgUrl: require('@/assets/main/dock-home.png'),
-                    activeImgUrl: require('@/assets/main/dock-home-hover.png'),
-                    route: '/home',
-                    theme: 'blue',
-                },
-                {
-                    label: '状态监控',
-                    imgUrl: require('@/assets/main/dock-surve.png'),
-                    activeImgUrl: require('@/assets/main/dock-surve-hover.png'),
-                    route: '/surve',
-                    theme: 'blue',
-                },
-                {
-                    label: '勤务管理',
-                    imgUrl: require('@/assets/main/dock-security.png'),
-                    activeImgUrl: require('@/assets/main/dock-security-hover.png'),
-                    route: '/security',
-                    theme: 'gold',
-                },
-                {
-                    label: '干线协调',
-                    imgUrl: require('@/assets/main/dock-coor.png'),
-                    activeImgUrl: require('@/assets/main/dock-coor-hover.png'),
-                    route: '/trunk',
-                    theme: 'blue',
-                },
-                {
-                    label: '数据分析',
-                    imgUrl: require('@/assets/main/dock-watch.png'),
-                    activeImgUrl: require('@/assets/main/dock-watch-hover.png'),
-                    route: '/watch',
-                    theme: 'blue',
-                },
-                {
-                    label: '系统设置',
-                    imgUrl: require('@/assets/main/dock-setting.png'),
-                    activeImgUrl: require('@/assets/main/dock-setting-hover.png'),
-                    route: '/setting',
-                    theme: 'blue',
-                },
-                {
-                    label: '测试1',
-                    imgUrl: require('@/assets/main/dock-home.png'),
-                    theme: 'blue',
-                },
-                {
-                    label: '测试2',
-                    imgUrl: require('@/assets/main/dock-surve.png'),
-                    theme: 'blue',
-                },
-                {
-                    label: '测试3',
-                    imgUrl: require('@/assets/main/dock-security.png'),
-                    theme: 'blue',
-                },
-            ]
         };
     },
     computed: {
-        // 动态计算 CSS 变量和合并自定义样式
         dockStyles() {
-            // 统一只传一个基础 bottom 偏移量,动画交由 CSS transform 处理
             return {
                 '--dock-bottom': `${this.bottomOffset}px`,
-                ...this.customStyle 
+                ...this.customStyle
             };
         }
     },
     watch: {
         $route() {
             this.updateActiveIndexByRoute();
+        },
+        items() {
+            this.$nextTick(this.checkScrollState);
         }
     },
     created() {
@@ -179,11 +127,10 @@ export default {
     },
     methods: {
         updateActiveIndexByRoute() {
-            const currentPath = this.$route?.path || ''; 
-            const matchIndex = this.dockItems.findIndex(item => {
+            const currentPath = this.$route?.path || '';
+            const matchIndex = this.items.findIndex(item => {
                 return item.route && currentPath.startsWith(item.route);
             });
-
             if (matchIndex !== -1) {
                 this.activeIndex = matchIndex;
             }
@@ -214,12 +161,12 @@ export default {
             const el = this.$refs.dockWrapper;
             if (!el) return;
             const rect = el.getBoundingClientRect();
-            // 发光线区域:居中,宽度与 CSS clamp(150px, 20vw, 250px) 一致
+            // 发光线区域: 居中, 宽度与 CSS clamp(150px, 20vw, 250px) 一致
             const lineWidth = Math.min(250, Math.max(150, window.innerWidth * 0.2));
             const centerX = window.innerWidth / 2;
             const left = centerX - lineWidth / 2;
             const right = centerX + lineWidth / 2;
-            // 垂直:dock 可见顶部往上延伸 30px(与 ::after 热区一致)
+            // 垂直: dock 可见顶部往上延伸 30px (与 ::after 热区一致)
             if (e.clientX >= left && e.clientX <= right && e.clientY >= rect.top - 30) {
                 this.dockExpanded = true;
             }
@@ -232,14 +179,36 @@ export default {
             if (direction === 1 && !this.canScrollRight) return;
 
             const container = this.$refs.listContainer;
-            const scrollAmount = 130; 
+            if (!container) return;
 
-            if (container) {
-                container.scrollBy({
-                    left: direction * scrollAmount,
-                    behavior: 'smooth'
-                });
+            const items = container.querySelectorAll('.dock-item');
+            if (!items.length) return;
+
+            // 每个 item 在 container scroll 坐标系中的 left 边界
+            const containerRect = container.getBoundingClientRect();
+            const currentScroll = container.scrollLeft;
+            const itemLefts = Array.from(items).map(el =>
+                el.getBoundingClientRect().left - containerRect.left + currentScroll
+            );
+
+            const EPS = 2; // 浮点容差
+            let target = null;
+            if (direction === 1) {
+                // 下一个 left > currentScroll 的 item
+                target = itemLefts.find(left => left > currentScroll + EPS);
+                if (target == null) target = container.scrollWidth - container.clientWidth;
+            } else {
+                // 上一个 left < currentScroll 的 item (倒序找第一个)
+                for (let i = itemLefts.length - 1; i >= 0; i--) {
+                    if (itemLefts[i] < currentScroll - EPS) {
+                        target = itemLefts[i];
+                        break;
+                    }
+                }
+                if (target == null) target = 0;
             }
+
+            container.scrollTo({ left: target, behavior: 'smooth' });
         }
     }
 };
@@ -254,16 +223,18 @@ export default {
     justify-content: center;
     width: 100%;
     height: 160px;
-    position: fixed; 
+    position: fixed;
     left: 0;
     bottom: var(--dock-bottom, 0px);
-    z-index: 9999;
+    /* z-index 必须 > 地图图例 (.map-legend z:100) 但 < SmartDialog 起始 (z:2000),
+       否则要么被图例盖, 要么反过来盖住弹窗 */
+    z-index: 500;
     transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
     background: linear-gradient(to top, rgba(0, 20, 30, 0.8) 0%, rgba(0, 20, 30, 0.01) 100%);
     pointer-events: auto !important;
 }
 
-/* === 状态 A自动隐藏 === */
+/* === 状态 A: 自动隐藏 === */
 .dock-wrapper.is-auto-hide {
     transform: translateY(calc(100% - clamp(15px, 4vh, 20px)));
     pointer-events: none;
@@ -273,7 +244,7 @@ export default {
     pointer-events: auto;
 }
 
-/* 中间触发热区:只覆盖发光线范围,pointer-events 始终开启 */
+/* 中间触发热区: 只覆盖发光线范围, pointer-events 始终开启 */
 .dock-wrapper.is-auto-hide::after {
     content: '';
     position: absolute;
@@ -293,9 +264,8 @@ export default {
     top: 0;
     left: 50%;
     transform: translateX(-50%);
-    /* 宽度和厚度也做一点自适应,小屏自动变短点 */
     width: clamp(150px, 20vw, 250px);
-    height: clamp(3px, 0.5vh, 5px); 
+    height: clamp(3px, 0.5vh, 5px);
     background: rgba(0, 229, 255, 0.6);
     box-shadow: 0 0 10px rgba(0, 229, 255, 0.8);
     border-radius: 4px;
@@ -307,7 +277,7 @@ export default {
     opacity: 0;
 }
 
-/* === 状态 B常驻显示 === */
+/* === 状态 B: 常驻显示 === */
 .dock-wrapper.is-always-show {
     transform: translateY(0);
 }
@@ -325,6 +295,9 @@ export default {
     white-space: nowrap;
     -ms-overflow-style: none;
     scrollbar-width: none;
+    /* 滑动 / 滚轮拖动时按 item 边界自动吸附, 配合左右箭头 scrollTo 双保险定位 */
+    scroll-snap-type: x mandatory;
+    scroll-padding-left: 0;
 }
 
 .dock-list-container::-webkit-scrollbar {
@@ -358,30 +331,30 @@ export default {
 }
 
 .arrow-img {
-  width: 100%;
-  height: 100%;
-  object-fit: contain;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
 }
 
 .left-facing {
-  transform: rotate(180deg);
+    transform: rotate(180deg);
 }
 
 .nav-arrow.is-active {
-  cursor: pointer;
+    cursor: pointer;
 }
 .nav-arrow.is-active:hover .arrow-img {
-  transform: scale(1.1); 
-  filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.8));
+    transform: scale(1.1);
+    filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.8));
 }
 
 .nav-arrow.left-arrow.is-active:hover .arrow-img.left-facing {
-  transform: rotate(180deg) scale(1.1);
+    transform: rotate(180deg) scale(1.1);
 }
 
 .nav-arrow.is-disabled {
-  cursor: not-allowed;
-  opacity: 0.6; 
+    cursor: not-allowed;
+    opacity: 0.6;
 }
 
 /* ================= 单个导航项 ================= */
@@ -396,6 +369,8 @@ export default {
     width: 100px;
     height: 90px;
     transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+    /* 配合 .dock-list-container 的 scroll-snap-type: 每个 item 左边界为吸附点 */
+    scroll-snap-align: start;
 }
 
 .item-icon {
@@ -403,7 +378,7 @@ export default {
     color: #a0cfff;
     z-index: 3;
     margin-bottom: 5px;
-    transition: all 0.3s;
+    transition: transform 0.3s ease, filter 0.3s ease;
 }
 
 .item-label {
@@ -414,7 +389,7 @@ export default {
     letter-spacing: 1px;
 }
 
-/* ================= 交互状态悬浮与选中 ================= */
+/* ================= 交互状态: 悬浮与选中 ================= */
 .dock-item:hover {
     transform: translateY(-15px) scale(1.15);
 }
@@ -432,12 +407,6 @@ export default {
     text-shadow: 0 0 8px #00e5ff;
 }
 
-.dock-item:hover .top-solid,
-.dock-item.is-active .top-solid {
-    background: linear-gradient(135deg, rgba(0, 229, 255, 0.9), rgba(0, 115, 255, 0.9));
-    box-shadow: 0 0 20px rgba(0, 229, 255, 0.6);
-}
-
 .dock-item.theme-gold.is-active .item-icon {
     color: #ffd700;
     text-shadow: 0 0 15px #ffaa00;
@@ -448,12 +417,6 @@ export default {
     text-shadow: 0 0 8px #ffaa00;
 }
 
-.item-icon {
-    z-index: 3;
-    margin-bottom: 5px;
-    transition: transform 0.3s ease, filter 0.3s ease;
-}
-
 .custom-icon {
     width: 88px;
     height: 64px;
@@ -471,19 +434,17 @@ export default {
         transform: translateY(0) scale(1);
     }
     50% {
-        transform: translateY(-8px) scale(1.08); /* 呼吸时轻微上浮和放大 */
+        transform: translateY(-8px) scale(1.08);
     }
 }
 
 .dock-item.is-breathing {
-    /* 动画周期设为 2.5s,无限循环,平滑过渡 */
     animation: breathing-wave 2.5s infinite ease-in-out;
 }
 
-/* 覆盖动画冲突:当鼠标悬浮或该项处于激活状态时,停止呼吸动画,采用原有的高亮放大效果 */
 .dock-item:hover,
 .dock-item.is-active {
-    animation: none !important; /* 强制停止动画 */
+    animation: none !important;
     transform: translateY(-15px) scale(1.15) !important;
 }
-</style>
+</style>

+ 108 - 0
src/config/menus.js

@@ -0,0 +1,108 @@
+// =============================================================================
+// 全应用顶层菜单的单一数据源.
+//
+// 字段约定:
+//   name        显示名 (径向上的"name", BottomDock 上对应 label)
+//   path        路由路径 (BottomDock 对应 route)
+//   side        径向布局位置: 'left' | 'right'. 仅前 6 项生效;
+//               第 7 项起一律落到 BottomDock 溢出.
+//   theme       BottomDock 主题色: 'blue' | 'gold' (默认 'blue')
+//   icon, hoverIcon            径向布局上的主图标 (Main.vue 用)
+//   halo, hoverHalo            径向布局的光圈底座 (Main.vue 用)
+//   dockIcon, dockHoverIcon    BottomDock 上的图标
+//
+// 添加新菜单:
+//   1) 列入下面的 allMenus 数组
+//   2) 第 1~6 项要带 side; 第 7 项起 side 可省略 (不参与径向)
+//   3) Main 页面径向只取前 6, 多出的自动落到 BottomDock; DashboardLayout
+//      所在的子页面看到的是全部 (含前 6).
+// =============================================================================
+
+const _halo = require('@/assets/main/main-menu-bg.png');
+const _haloHover = require('@/assets/main/main-menu-bg-hover.png');
+
+export const allMenus = [
+  // ========== 前 6 项: 径向布局 (3 left + 3 right) ==========
+  {
+    name: '首页', path: '/home', side: 'left', theme: 'blue',
+    icon: require('@/assets/main/main-home.png'),
+    hoverIcon: require('@/assets/main/main-home-hover.png'),
+    halo: _halo, hoverHalo: _haloHover,
+    dockIcon: require('@/assets/main/dock-home.png'),
+    dockHoverIcon: require('@/assets/main/dock-home-hover.png'),
+  },
+  {
+    name: '状态监测', path: '/surve', side: 'left', theme: 'blue',
+    icon: require('@/assets/main/main-surve.png'),
+    hoverIcon: require('@/assets/main/main-surve-hover.png'),
+    halo: _halo, hoverHalo: _haloHover,
+    dockIcon: require('@/assets/main/dock-surve.png'),
+    dockHoverIcon: require('@/assets/main/dock-surve-hover.png'),
+  },
+  {
+    name: '勤务管理', path: '/security', side: 'left', theme: 'gold',
+    icon: require('@/assets/main/main-security.png'),
+    hoverIcon: require('@/assets/main/main-security-hover.png'),
+    halo: _halo, hoverHalo: _haloHover,
+    dockIcon: require('@/assets/main/dock-security.png'),
+    dockHoverIcon: require('@/assets/main/dock-security-hover.png'),
+  },
+  {
+    name: '干线协调', path: '/trunk', side: 'right', theme: 'blue',
+    icon: require('@/assets/main/main-coor.png'),
+    hoverIcon: require('@/assets/main/main-coor-hover.png'),
+    halo: _halo, hoverHalo: _haloHover,
+    dockIcon: require('@/assets/main/dock-coor.png'),
+    dockHoverIcon: require('@/assets/main/dock-coor-hover.png'),
+  },
+  {
+    name: '数据分析', path: '/watch', side: 'right', theme: 'blue',
+    icon: require('@/assets/main/main-watch.png'),
+    hoverIcon: require('@/assets/main/main-watch-hover.png'),
+    halo: _halo, hoverHalo: _haloHover,
+    dockIcon: require('@/assets/main/dock-watch.png'),
+    dockHoverIcon: require('@/assets/main/dock-watch-hover.png'),
+  },
+  {
+    name: '系统设置', path: '/setting', side: 'right', theme: 'blue',
+    icon: require('@/assets/main/main-setting.png'),
+    hoverIcon: require('@/assets/main/main-setting-hover.png'),
+    halo: _halo, hoverHalo: _haloHover,
+    dockIcon: require('@/assets/main/dock-setting.png'),
+    dockHoverIcon: require('@/assets/main/dock-setting-hover.png'),
+  },
+
+  // ========== 第 7 项起: 径向放不下, 自动落到 BottomDock ==========
+  {
+    name: '通知中心', path: '/notify', theme: 'blue',
+    dockIcon: require('@/assets/main/dock-watch.png'),
+    dockHoverIcon: require('@/assets/main/dock-watch-hover.png'),
+  },
+  {
+    name: '工单', path: '/ticket', theme: 'blue',
+    dockIcon: require('@/assets/main/dock-coor.png'),
+    dockHoverIcon: require('@/assets/main/dock-coor-hover.png'),
+  },
+  {
+    name: '帮助', path: '/help', theme: 'gold',
+    dockIcon: require('@/assets/main/dock-home.png'),
+    dockHoverIcon: require('@/assets/main/dock-home-hover.png'),
+  },
+  {
+    name: '关于', path: '/about', theme: 'blue',
+    dockIcon: require('@/assets/main/dock-setting.png'),
+    dockHoverIcon: require('@/assets/main/dock-setting-hover.png'),
+  },
+];
+
+// 把 allMenus 适配成 BottomDock 期望的字段名:
+//   name -> label, path -> route, dockIcon/dockHoverIcon 优先, 兜底 icon/hoverIcon
+export function toDockItems(list) {
+  return list.map(m => ({
+    label: m.label || m.name,
+    imgUrl: m.imgUrl || m.dockIcon || m.icon,
+    activeImgUrl: m.activeImgUrl || m.dockHoverIcon || m.hoverIcon,
+    route: m.route || m.path,
+    theme: m.theme || 'blue',
+  }));
+}

+ 11 - 3
src/layouts/DashboardLayout.vue

@@ -37,9 +37,6 @@
 
                 <section class="center-area">
                     <slot name="center"></slot>
-
-                    <!-- 底部dock菜单-->
-                    <BottomDock />
                 </section>
 
                 <aside class="right-sidebar">
@@ -49,6 +46,10 @@
             </main>
         </div>
 
+        <!-- 底部dock菜单: 挂在 .fluid-dashboard 根级 (脱离 .ui-layer 的 z-index:2 子上下文),
+             这样 z-index:9999 直接在根上下文里参与排序, 不会被地图图例 (z-index:100) 盖住 -->
+        <BottomDock :items="dockMenus" />
+
         <!-- 弹窗层:由 Layout 统一渲染 -->
         <SmartDialog
             ref="dialogs"
@@ -115,6 +116,7 @@ import DetectorTable from '@/components/ui/DetectorTable.vue';
 import CameraVideoDialog from '@/components/ui/CameraVideoDialog.vue';
 import SchemeStageEditDialog from '@/components/ui/SchemeStageEditDialog.vue';
 import brand from '@/utils/brand';
+import { allMenus, toDockItems } from '@/config/menus';
 
 export default {
     name: 'DashboardLayout',
@@ -172,6 +174,12 @@ export default {
             brand,
         }
     },
+    computed: {
+        // 与 Main 共享同一份菜单配置 (src/config/menus.js)
+        dockMenus() {
+            return toDockItems(allMenus);
+        },
+    },
     methods: {
         handleDialogExpand(dialog) {
             if (dialog.data && typeof dialog.data.onExpand === 'function') {

+ 25 - 72
src/views/Main.vue

@@ -34,116 +34,69 @@
         </div>
 
         <div class="menu-side right-side">
-          <div 
-            v-for="(item, index) in rightMenus" 
-            :key="'right-'+index" 
+          <div
+            v-for="(item, index) in rightMenus"
+            :key="'right-'+index"
             class="menu-item"
             @click="handleMenuClick(item)"
           >
             <div class="icon-container">
               <img :src="item.halo" class="halo-img halo-normal" v-if="item.halo" />
               <img :src="item.hoverHalo" class="halo-img halo-hover" v-if="item.hoverHalo" />
-              
+
               <img :src="item.icon" class="icon-img img-normal" v-if="item.icon" />
               <div class="icon-placeholder img-normal" v-else></div>
-              
+
               <img :src="item.hoverIcon || item.icon" class="icon-img img-hover" v-if="item.icon" />
               <div class="icon-placeholder img-hover" v-else></div>
             </div>
-            
+
             <span class="menu-text">{{ item.name }}</span>
           </div>
         </div>
 
       </div>
+
+      <!-- 底部 dock: 仅显示 menuList 前 6 个之外的溢出菜单 (空时不渲染) -->
+      <BottomDock v-if="dockOverflow.length" :items="dockOverflow" />
+
     </template>
   </LoginLayout>
 </template>
 
 <script>
 import LoginLayout from "@/layouts/LoginLayout.vue";
+import BottomDock from "@/components/ui/BottomDock.vue";
+import { allMenus, toDockItems } from "@/config/menus";
 
 export default {
   name: "MainPage",
-  components: { 
-    LoginLayout, 
+  components: {
+    LoginLayout,
+    BottomDock,
   },
   data() {
-    // 假设公共的光圈图片路径(按需修改为你实际的文件名)
-    const defaultHalo = require('@/assets/main/main-menu-bg.png');
-    const defaultHoverHalo = require('@/assets/main/main-menu-bg-hover.png');
-
     return {
       baseW: 1920,
       baseH: 1080,
       scale: 1,
       dots: [],
-      // 菜单配置数据
-      menuList: [
-        { 
-          name: '首页', 
-          icon: require('@/assets/main/main-home.png'),
-          hoverIcon: require('@/assets/main/main-home-hover.png'),
-          halo: defaultHalo,             // 引入光圈图片
-          hoverHalo: defaultHoverHalo,   // 引入高亮光圈图片
-          side: 'left', 
-          path: '/home' 
-        },
-        { 
-          name: '状态监测', 
-          icon: require('@/assets/main/main-surve.png'),
-          hoverIcon: require('@/assets/main/main-surve-hover.png'),
-          halo: defaultHalo,
-          hoverHalo: defaultHoverHalo,
-          side: 'left', 
-          path: '/surve' 
-        },
-        { 
-          name: '勤务管理', 
-          icon: require('@/assets/main/main-security.png'),
-          hoverIcon: require('@/assets/main/main-security-hover.png'),
-          halo: defaultHalo,
-          hoverHalo: defaultHoverHalo,
-          side: 'left', 
-          path: '/security' 
-        },
-        { 
-          name: '干线协调', 
-          icon: require('@/assets/main/main-coor.png'),
-          hoverIcon: require('@/assets/main/main-coor-hover.png'),
-          halo: defaultHalo,
-          hoverHalo: defaultHoverHalo,
-          side: 'right',
-          path: '/trunk'
-        },
-        { 
-          name: '数据分析', 
-          icon: require('@/assets/main/main-watch.png'),
-          hoverIcon: require('@/assets/main/main-watch-hover.png'),
-          halo: defaultHalo,
-          hoverHalo: defaultHoverHalo,
-          side: 'right', 
-          path: '/watch' 
-        },
-        { 
-          name: '系统设置', 
-          icon: require('@/assets/main/main-setting.png'),
-          hoverIcon: require('@/assets/main/main-setting-hover.png'),
-          halo: defaultHalo,
-          hoverHalo: defaultHoverHalo,
-          side: 'right', 
-          path: '/setting' 
-        },
-      ]
+      // 菜单配置: 单一数据源在 src/config/menus.js, 与 DashboardLayout 共享
+      menuList: allMenus,
     };
   },
   computed: {
+    // 前 6 项才参与径向布局; 多余的全部落到 dockOverflow
     leftMenus() {
-      return this.menuList.filter(item => item.side === 'left');
+      return this.menuList.slice(0, 6).filter(item => item.side === 'left');
     },
     rightMenus() {
-      return this.menuList.filter(item => item.side === 'right');
-    }
+      return this.menuList.slice(0, 6).filter(item => item.side === 'right');
+    },
+    // 第 7 项起 -> BottomDock 字段映射
+    dockOverflow() {
+      return toDockItems(this.menuList.slice(6));
+    },
   },
   mounted() {
     this.updateScale();