Browse Source

菜单更新

Lin Qilong 5 ngày trước cách đây
mục cha
commit
9da1f273bf

+ 19 - 0
gw-ui/src/layout/components/Sidebar/index.vue

@@ -171,6 +171,25 @@ export default {
   border: none;
   overflow-y: auto;
   overflow-x: hidden;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
+}
+
+.sidebar-menu::-webkit-scrollbar {
+  width: 4px;
+}
+
+.sidebar-menu::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.sidebar-menu::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.25);
+  border-radius: 2px;
+}
+
+.sidebar-menu::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.45);
 }
 
 :deep(.el-menu) {

+ 256 - 19
gw-ui/src/layout/index.vue

@@ -1,25 +1,34 @@
 <!-- layout/index.vue -->
 <template>
-  <div
-    class="app-wrapper"
-    :style="{
-      '--current-color': theme,
-      '--current-color-light': theme + '1a',
-      '--current-color-dark-bg': theme + '33',
-    }"
-  >
-    <!-- 顶部栏(系统名 + 用户信息) -->
+  <div class="app-wrapper" :style="{
+    '--current-color': theme,
+    '--current-color-light': theme + '1a',
+    '--current-color-dark-bg': theme + '33',
+  }">
+    <!-- 顶部栏(系统名 + 一级菜单 + 用户信息) -->
     <div class="top-bar">
       <div class="logo-area">
         <img :src="logoImg" class="logo-img" />
-        <span class="logo-text" style="cursor:pointer" @click="router.push('/index')">水利安全生产监管信息系统</span>
+        <span class="logo-text" style="cursor:pointer" @click="router.push('/')">水利安全生产监管信息系统</span>
       </div>
+
+      <!-- 一级菜单(顶部水平导航) -->
+      <div class="top-nav-menu">
+        <div v-for="item in topMenus" :key="item.path"
+          :class="['top-nav-item', { active: activeTopMenu === item.path }]" @click="handleTopMenuClick(item)">
+          <svg-icon v-if="item.meta?.icon" :icon-class="item.meta.icon" class="top-nav-icon" />
+          <span>{{ item.meta?.title }}</span>
+        </div>
+      </div>
+
       <div class="top-right-menu">
         <el-dropdown @command="handleCommand" class="avatar-container" trigger="hover">
           <div class="avatar-wrapper">
             <svg-icon icon-class="user" class="user-icon" />
             <span class="user-realName">{{ userStore.realName || userStore.name }}</span>
-            <el-icon class="dropdown-icon"><ArrowDown /></el-icon>
+            <el-icon class="dropdown-icon">
+              <ArrowDown />
+            </el-icon>
           </div>
           <template #dropdown>
             <el-dropdown-menu>
@@ -35,13 +44,13 @@
 
     <!-- 下方区域:左右结构 -->
     <div class="main-layout">
-      <!-- 左侧二三级菜单 -->
-      <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }">
+      <!-- 左侧菜单:有子菜单时才显示 -->
+      <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }" v-show="!sidebar.hide">
         <sidebar v-if="!sidebar.hide" />
       </div>
 
       <!-- 右侧内容区域 -->
-      <div class="content-wrapper">
+      <div class="content-wrapper" :class="{ 'full-width': sidebar.hide }">
         <app-main />
         <settings ref="settingRef" />
       </div>
@@ -50,8 +59,8 @@
 </template>
 
 <script setup>
-import { computed, ref } from "vue";
-import { useRouter } from "vue-router";
+import { computed, ref, watch } from "vue";
+import { useRouter, useRoute } from "vue-router";
 import { ElMessageBox } from "element-plus";
 import { ArrowDown } from "@element-plus/icons-vue";
 import Sidebar from "./components/Sidebar/index.vue";
@@ -59,12 +68,15 @@ import { AppMain, Settings } from "./components";
 import useAppStore from "@/store/modules/app";
 import useSettingsStore from "@/store/modules/settings";
 import useUserStore from "@/store/modules/user";
+import usePermissionStore from "@/store/modules/permission";
 import logoImg from "@/assets/logo/logo.png";
 
 const settingsStore = useSettingsStore();
 const appStore = useAppStore();
 const userStore = useUserStore();
+const permissionStore = usePermissionStore();
 const router = useRouter();
+const route = useRoute();
 
 const theme = computed(() => settingsStore.theme);
 const sidebar = computed(() => appStore.sidebar);
@@ -72,6 +84,162 @@ const isCollapse = computed(() => appStore.sidebar.isCollapse);
 
 const settingRef = ref(null);
 
+// ========== 顶部一级菜单 ==========
+
+// 从 topbarRouters 提取一级菜单
+const topMenus = computed(() => {
+  const routes = permissionStore.topbarRouters || [];
+  const menus = [];
+  for (const menu of routes) {
+    if (menu.hidden) continue;
+    // 根路径:取其第一个子路由(首页)
+    if ((menu.path === "/" || menu.path === "") && menu.children && menu.children.length > 0) {
+      const firstChild = menu.children.find(c => !c.hidden) || menu.children[0];
+      menus.push({
+        ...firstChild,
+        path: firstChild.path.startsWith("/") ? firstChild.path : "/" + firstChild.path,
+      });
+    } else if (menu.meta?.title) {
+      menus.push(menu);
+    }
+  }
+  return menus;
+});
+
+// 当前激活的一级菜单
+const activeTopMenu = ref("/index");
+
+// 根据当前路由路径找到归属的一级菜单
+function findActiveTopMenu(path) {
+  if (path === "/index" || path === "/") return "/index";
+
+  // 优先检查 meta.activeMenu(隐藏路由通过此属性指定归属菜单)
+  if (route.meta?.activeMenu) {
+    const activePath = route.meta.activeMenu;
+    const found = topMenus.value.find(m => activePath === m.path || activePath.startsWith(m.path + "/"));
+    if (found) return found.path;
+  }
+
+  // 查找 path 属于哪个一级菜单
+  for (const menu of topMenus.value) {
+    if (path === menu.path || path.startsWith(menu.path + "/")) {
+      return menu.path;
+    }
+  }
+  return "/index";
+}
+
+// 同步左侧菜单:根据当前激活的一级菜单,设置侧边栏内容
+function syncSidebar(topMenuPath) {
+  // 首页永远不显示侧边栏
+  if (topMenuPath === "/index") {
+    permissionStore.setSidebarRouters([]);
+    appStore.toggleSideBarHide(true);
+    return;
+  }
+
+  const topRoutes = permissionStore.topbarRouters || [];
+  const fullRoute = topRoutes.find(r => r.path === topMenuPath);
+
+  if (fullRoute && fullRoute.children && fullRoute.children.length > 0) {
+    const visibleChildren = fullRoute.children.filter(c => !c.hidden && c.meta?.title);
+    if (visibleChildren.length > 0) {
+      // 构建侧边栏路由:子路径拼成绝对路径
+      const basePath = (!fullRoute.path || fullRoute.path === "/") ? "" : fullRoute.path;
+      const sidebarRoutes = visibleChildren.map(c => ({
+        ...c,
+        path: c.path.startsWith("/") ? c.path : (basePath + "/" + c.path),
+      }));
+      permissionStore.setSidebarRouters(sidebarRoutes);
+      appStore.toggleSideBarHide(false);
+      return;
+    }
+  }
+
+  // 没有子菜单:隐藏侧边栏
+  permissionStore.setSidebarRouters([]);
+  appStore.toggleSideBarHide(true);
+}
+
+// 点击顶部一级菜单
+function handleTopMenuClick(menu) {
+  const menuPath = menu.path;
+
+  // 首页直接导航,不显示侧边栏
+  if (menuPath === "/index") {
+    appStore.toggleSideBarHide(true);
+    permissionStore.setSidebarRouters([]);
+    router.push("/index");
+    return;
+  }
+
+  // 查找该菜单在 topbarRouters 中的完整路由
+  const topRoutes = permissionStore.topbarRouters || [];
+  const fullRoute = topRoutes.find(r => r.path === menuPath);
+
+  if (fullRoute && fullRoute.children && fullRoute.children.length > 0) {
+    const visibleChildren = fullRoute.children.filter(c => !c.hidden && c.meta?.title);
+    if (visibleChildren.length > 0) {
+      // 有子菜单:更新侧边栏,导航到第一个可见子菜单
+      const basePath = (!fullRoute.path || fullRoute.path === "/") ? "" : fullRoute.path;
+      const sidebarRoutes = visibleChildren.map(c => ({
+        ...c,
+        path: c.path.startsWith("/") ? c.path : (basePath + "/" + c.path),
+      }));
+      permissionStore.setSidebarRouters(sidebarRoutes);
+      appStore.toggleSideBarHide(false);
+
+      const firstChild = sidebarRoutes[0];
+      let navigatePath;
+      if (firstChild.children && firstChild.children.length > 0) {
+        const firstGrandchild = firstChild.children.find(cc => !cc.hidden) || firstChild.children[0];
+        navigatePath = firstGrandchild.path.startsWith("/")
+          ? firstGrandchild.path
+          : firstChild.path + "/" + firstGrandchild.path;
+      } else {
+        navigatePath = firstChild.path;
+      }
+      router.push(navigatePath);
+      return;
+    }
+  }
+
+  // 没有子菜单直接导航,隐藏侧边栏
+  appStore.toggleSideBarHide(true);
+  permissionStore.setSidebarRouters([]);
+  router.push(menuPath);
+}
+
+// 初始化:setup 阶段同步执行,确保首帧渲染前侧边栏已正确
+if (permissionStore.topbarRouters && permissionStore.topbarRouters.length > 0) {
+  const initTop = findActiveTopMenu(route.path);
+  activeTopMenu.value = initTop;
+  syncSidebar(initTop);
+}
+
+// 菜单数据就绪时(首次加载异步返回后)补充同步一次
+watch(topMenus, (menus) => {
+  if (menus.length === 0) return;
+  const top = findActiveTopMenu(route.path);
+  if (top !== activeTopMenu.value) {
+    activeTopMenu.value = top;
+    syncSidebar(top);
+  }
+});
+
+// 路由变化时同步侧边栏
+watch(
+  () => route.path,
+  (newPath) => {
+    if (topMenus.value.length === 0) return;
+    const top = findActiveTopMenu(newPath);
+    if (top !== activeTopMenu.value) {
+      activeTopMenu.value = top;
+      syncSidebar(top);
+    }
+  }
+);
+
 function handleCommand(command) {
   switch (command) {
     case "logout":
@@ -83,7 +251,7 @@ function handleCommand(command) {
         userStore.logOut().then(() => {
           location.href = "/index";
         });
-      }).catch(() => {});
+      }).catch(() => { });
       break;
   }
 }
@@ -96,13 +264,14 @@ function handleCommand(command) {
   flex-direction: column;
 }
 
+/* ===== 顶部系统栏 ===== */
 .top-bar {
   height: 60px;
   background: #3c68cb;
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 0 30px;
+  padding: 0 20px;
   box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
   flex-shrink: 0;
 }
@@ -110,6 +279,8 @@ function handleCommand(command) {
 .logo-area {
   display: flex;
   align-items: center;
+  flex-shrink: 0;
+  margin-right: 80px;
 }
 
 .logo-img {
@@ -122,11 +293,73 @@ function handleCommand(command) {
   color: #fff;
   font-size: 20px;
   font-weight: bold;
+  white-space: nowrap;
+}
+
+/* 顶部一级菜单 */
+.top-nav-menu {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  flex: 1;
+  overflow-x: auto;
+  overflow-y: hidden;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
+}
+
+.top-nav-menu::-webkit-scrollbar {
+  height: 4px;
+}
+
+.top-nav-menu::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.top-nav-menu::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.25);
+  border-radius: 2px;
+}
+
+.top-nav-menu::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.45);
+}
+
+.top-nav-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 0 18px;
+  height: 100%;
+  color: rgba(255, 255, 255, 0.85);
+  cursor: pointer;
+  font-size: 15px;
+  white-space: nowrap;
+  transition: background 0.2s, color 0.2s;
+  flex-shrink: 0;
+}
+
+.top-nav-item:hover {
+  background: rgba(255, 255, 255, 0.12);
+  color: #fff;
+}
+
+.top-nav-item.active {
+  background: rgba(255, 255, 255, 0.22);
+  color: #fff;
+  font-weight: 600;
+}
+
+.top-nav-icon {
+  width: 16px;
+  height: 16px;
 }
 
 .top-right-menu {
   display: flex;
   align-items: center;
+  flex-shrink: 0;
+  margin-left: 12px;
 }
 
 .avatar-wrapper {
@@ -170,6 +403,7 @@ function handleCommand(command) {
   transform: rotate(180deg);
 }
 
+/* ===== 下方主布局 ===== */
 .main-layout {
   flex: 1;
   display: flex;
@@ -189,6 +423,9 @@ function handleCommand(command) {
 .content-wrapper {
   flex: 1;
   overflow: auto;
-  //background: #f0f2f6;
+}
+
+.content-wrapper.full-width {
+  margin-left: 0;
 }
 </style>

+ 1 - 1
gw-ui/src/router/index.js

@@ -60,7 +60,7 @@ export const constantRoutes = [
   {
     path: '',
     component: Layout,
-    redirect: '/index',
+    redirect: '/onemap',
     children: [
       {
         path: '/index',