zhuj123456 il y a 1 mois
Parent
commit
0563e46aef

+ 181 - 68
gw-ui/src/layout/components/Sidebar/SidebarItem.vue

@@ -1,80 +1,193 @@
 <!-- layout/components/Sidebar/SidebarItem.vue -->
 <template>
   <template v-if="!item.hidden">
-    <template
-      v-if="
-        hasOneShowingChild &&
-        (!onlyOneChild.children || onlyOneChild.noShowingChildren)
-      "
+    <!-- 有子菜单 -->
+    <div v-if="item.children && item.children.length > 0" class="sub-menu">
+      <div class="sub-menu-title" @click="toggleSubGroup">
+        <svg-icon
+          v-if="item.meta?.icon"
+          :icon-class="item.meta.icon"
+          class="menu-icon"
+        />
+        <span class="menu-text" v-if="!isCollapse">{{ item.meta?.title }}</span>
+        <span class="sub-arrow" v-if="!isCollapse">
+          {{ expandedSubGroup ? "▼" : "▶" }}
+        </span>
+      </div>
+      <div class="sub-menu-items" v-show="expandedSubGroup || isCollapse">
+        <sidebar-item
+          v-for="child in item.children"
+          :key="child.path"
+          :item="child"
+          :parent-path="getChildFullPath(child.path)"
+          :is-collapse="isCollapse"
+          :active-path="activePath"
+          @menu-click="handleMenuClick"
+        />
+      </div>
+    </div>
+
+    <!-- 没有子菜单 -->
+    <div
+      v-else
+      :class="[
+        'sub-menu-item',
+        {
+          active:
+            activePath === getFullPath ||
+            activePath.startsWith(getFullPath + '/'),
+        },
+      ]"
+      @click="handleMenuClick(getFullPath)"
     >
-      <el-menu-item :index="resolvePath(onlyOneChild.path)">
-        <el-icon>
-          <component
-            :is="onlyOneChild.meta?.icon || (item.meta && item.meta.icon)"
-          />
-        </el-icon>
-        <template #title>
-          <span>{{ onlyOneChild.meta?.title || item.meta?.title }}</span>
-        </template>
-      </el-menu-item>
-    </template>
-
-    <el-sub-menu v-else :index="resolvePath(item.path)" teleported>
-      <template #title>
-        <el-icon v-if="item.meta && item.meta.icon">
-          <component :is="item.meta.icon" />
-        </el-icon>
-        <span>{{ item.meta?.title }}</span>
-      </template>
-
-      <sidebar-item
-        v-for="child in item.children"
-        :key="child.path"
-        :item="child"
-        :base-path="resolvePath(child.path)"
+      <svg-icon
+        v-if="item.meta?.icon"
+        :icon-class="item.meta.icon"
+        class="menu-icon"
       />
-    </el-sub-menu>
+      <span class="menu-text">{{ item.meta?.title }}</span>
+    </div>
   </template>
 </template>
 
-<script setup>
-import { computed, ref } from "vue";
-import { isExternal } from "@/utils/validate";
+<script>
+import { ref, watch } from "vue";
 
-const props = defineProps({
-  item: {
-    type: Object,
-    required: true,
+export default {
+  name: "SidebarItem",
+  props: {
+    item: {
+      type: Object,
+      required: true,
+    },
+    parentPath: {
+      type: String,
+      default: "",
+    },
+    isCollapse: {
+      type: Boolean,
+      default: false,
+    },
+    activePath: {
+      type: String,
+      default: "",
+    },
   },
-  basePath: {
-    type: String,
-    default: "",
+  emits: ["menu-click"],
+  setup(props, { emit }) {
+    const expandedSubGroup = ref(false);
+
+    const getFullPath = computed(() => {
+      let path = props.item.path;
+      if (!path.startsWith("/") && props.parentPath) {
+        path = props.parentPath + "/" + path;
+      }
+      return path;
+    });
+
+    const getChildFullPath = (childPath) => {
+      if (childPath.startsWith("/")) {
+        return childPath;
+      }
+      return getFullPath.value;
+    };
+
+    const toggleSubGroup = () => {
+      expandedSubGroup.value = !expandedSubGroup.value;
+    };
+
+    const handleMenuClick = (path) => {
+      emit("menu-click", path);
+    };
+
+    // 自动展开当前路由对应的父级菜单
+    watch(
+      () => props.activePath,
+      (newPath) => {
+        if (
+          newPath === getFullPath.value ||
+          newPath.startsWith(getFullPath.value + "/")
+        ) {
+          expandedSubGroup.value = true;
+        }
+      },
+      { immediate: true },
+    );
+
+    return {
+      expandedSubGroup,
+      getFullPath,
+      getChildFullPath,
+      toggleSubGroup,
+      handleMenuClick,
+    };
   },
-});
-
-const onlyOneChild = ref(null);
-
-const hasOneShowingChild = computed(() => {
-  const showingChildren =
-    props.item.children?.filter((child) => !child.hidden) || [];
-  if (showingChildren.length === 1) {
-    onlyOneChild.value = showingChildren[0];
-    return true;
-  }
-  if (showingChildren.length === 0) {
-    onlyOneChild.value = { ...props.item, path: "", noShowingChildren: true };
-    return true;
-  }
-  return false;
-});
-
-function resolvePath(routePath) {
-  if (isExternal(routePath)) {
-    return routePath;
-  }
-  if (isExternal(props.basePath)) {
-    return props.basePath;
-  }
-  return props.basePath + "/" + routePath;
-}
+};
 </script>
+
+<style scoped>
+.sub-menu {
+  margin-bottom: 2px;
+}
+
+.sub-menu-title {
+  padding: 0 20px 0 44px;
+  height: 40px;
+  line-height: 40px;
+  color: #bfcbd9;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  position: relative;
+}
+
+.sub-menu-title:hover {
+  background-color: #263445;
+  color: #fff;
+}
+
+.sub-arrow {
+  position: absolute;
+  right: 20px;
+  font-size: 12px;
+}
+
+.sub-menu-items {
+  overflow: hidden;
+}
+
+.sub-menu-item {
+  padding: 0 20px 0 68px;
+  height: 36px;
+  line-height: 36px;
+  color: #bfcbd9;
+  cursor: pointer;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.sub-menu-item:hover {
+  background-color: #263445;
+  color: #fff;
+}
+
+.sub-menu-item.active {
+  background-color: #409eff;
+  color: #fff;
+}
+
+/* 收起时的样式 */
+.sidebar-container.collapsed .sub-menu-title,
+.sidebar-container.collapsed .sub-menu-item {
+  justify-content: center;
+  padding: 0;
+}
+
+.sidebar-container.collapsed .menu-text,
+.sidebar-container.collapsed .sub-arrow {
+  display: none;
+}
+</style>

+ 160 - 66
gw-ui/src/layout/components/Sidebar/index.vue

@@ -1,22 +1,76 @@
 <!-- layout/components/Sidebar/index.vue -->
 <template>
   <div class="sidebar-container" :class="{ collapsed: isCollapse }">
-    <div class="menu-list">
-      <div
-        v-for="item in menuList"
-        :key="item.path"
-        :class="['menu-item', { active: isActive(item.path) }]"
-        @click="handleMenuClick(item)"
-      >
-        <!-- 使用原本的 svg 图标 -->
-        <svg-icon
-          v-if="item.meta?.icon"
-          :icon-class="item.meta.icon"
-          class="menu-icon"
-        />
-        <span class="menu-text">{{ item.meta?.title }}</span>
-      </div>
-    </div>
+    <el-menu
+      :default-active="activeMenu"
+      :collapse="isCollapse"
+      background-color="#304156"
+      text-color="#bfcbd9"
+      active-text-color="#409eff"
+      :collapse-transition="false"
+      unique-opened
+      class="sidebar-menu"
+    >
+      <template v-for="item in menuList" :key="item.path">
+        <!-- 有子菜单的项 -->
+        <el-sub-menu
+          v-if="item.children && item.children.length > 0"
+          :index="getFullPath(item.path)"
+        >
+          <template #title>
+            <svg-icon v-if="item.meta?.icon" :icon-class="item.meta.icon" />
+            <span>{{ item.meta?.title }}</span>
+          </template>
+          <!-- 二级菜单 -->
+          <template v-for="child in item.children" :key="child.path">
+            <el-sub-menu
+              v-if="child.children && child.children.length > 0"
+              :index="getFullPath(child.path, item.path)"
+            >
+              <template #title>
+                <svg-icon
+                  v-if="child.meta?.icon"
+                  :icon-class="child.meta.icon"
+                />
+                <span>{{ child.meta?.title }}</span>
+              </template>
+              <!-- 三级菜单 -->
+              <el-menu-item
+                v-for="subChild in child.children"
+                :key="subChild.path"
+                :index="getFullPath(subChild.path, child.path, item.path)"
+                @click="handleMenuClick(subChild, child, item)"
+              >
+                <svg-icon
+                  v-if="subChild.meta?.icon"
+                  :icon-class="subChild.meta.icon"
+                />
+                <span>{{ subChild.meta?.title }}</span>
+              </el-menu-item>
+            </el-sub-menu>
+            <!-- 二级菜单(无子菜单) -->
+            <el-menu-item
+              v-else
+              :index="getFullPath(child.path, item.path)"
+              @click="handleMenuClick(child, item)"
+            >
+              <svg-icon v-if="child.meta?.icon" :icon-class="child.meta.icon" />
+              <span>{{ child.meta?.title }}</span>
+            </el-menu-item>
+          </template>
+        </el-sub-menu>
+
+        <!-- 没有子菜单的一级菜单项 -->
+        <el-menu-item
+          v-else
+          :index="getFullPath(item.path)"
+          @click="handleMenuClick(item)"
+        >
+          <svg-icon v-if="item.meta?.icon" :icon-class="item.meta.icon" />
+          <span>{{ item.meta?.title }}</span>
+        </el-menu-item>
+      </template>
+    </el-menu>
 
     <div class="collapse-btn" @click="toggleCollapse">
       <span class="btn-text">{{ isCollapse ? "▶" : "◀" }}</span>
@@ -46,8 +100,17 @@ export default {
 
     const isCollapse = computed(() => appStore.sidebar.isCollapse);
 
+    // 当前激活的菜单
+    const activeMenu = computed(() => {
+      return route.path;
+    });
+
+    // 获取菜单列表
     const menuList = computed(() => {
       const routes = permissionStore.sidebarRouters || [];
+      console.log("sidebarRouters:", routes);
+      console.log("topMenuKey:", props.topMenuKey);
+
       if (!routes.length) return [];
 
       if (props.topMenuKey) {
@@ -65,18 +128,65 @@ export default {
       return [];
     });
 
-    const isActive = (path) => {
-      return route.path.includes(path);
+    // 获取完整路径
+    const getFullPath = (path, parentPath = "", grandParentPath = "") => {
+      if (path.startsWith("/")) {
+        return path;
+      }
+
+      // 三级菜单:topMenuKey + 一级/二级/三级
+      if (grandParentPath && parentPath) {
+        return (
+          props.topMenuKey +
+          "/" +
+          grandParentPath +
+          "/" +
+          parentPath +
+          "/" +
+          path
+        );
+      }
+
+      // 二级菜单:topMenuKey + 一级/二级
+      if (parentPath) {
+        return props.topMenuKey + "/" + parentPath + "/" + path;
+      }
+
+      // 一级菜单:topMenuKey + 一级
+      return props.topMenuKey + "/" + path;
     };
 
-    const handleMenuClick = (item) => {
-      let fullPath = item.path;
-      if (!fullPath.startsWith("/") && props.topMenuKey) {
-        fullPath = props.topMenuKey + "/" + fullPath;
+    // 点击菜单
+    const handleMenuClick = (
+      item,
+      parentItem = null,
+      grandParentItem = null,
+    ) => {
+      let fullPath = "";
+
+      if (grandParentItem && parentItem) {
+        // 三级菜单:topMenuKey/一级/二级/三级
+        fullPath =
+          props.topMenuKey +
+          "/" +
+          grandParentItem.path +
+          "/" +
+          parentItem.path +
+          "/" +
+          item.path;
+      } else if (parentItem) {
+        // 二级菜单:topMenuKey/一级/二级
+        fullPath = props.topMenuKey + "/" + parentItem.path + "/" + item.path;
+      } else {
+        // 一级菜单:topMenuKey/一级
+        fullPath = props.topMenuKey + "/" + item.path;
       }
+
+      console.log("跳转路径:", fullPath);
       router.push(fullPath);
     };
 
+    // 收起/展开
     const toggleCollapse = () => {
       appStore.sidebar.isCollapse = !appStore.sidebar.isCollapse;
     };
@@ -84,7 +194,8 @@ export default {
     return {
       menuList,
       isCollapse,
-      isActive,
+      activeMenu,
+      getFullPath,
       handleMenuClick,
       toggleCollapse,
     };
@@ -94,73 +205,55 @@ export default {
 
 <style scoped>
 .sidebar-container {
-  width: 210px;
-  background-color: #304156;
   height: 100%;
-  transition: width 0.2s;
   display: flex;
   flex-direction: column;
-  overflow: hidden;
-}
-
-.sidebar-container.collapsed {
-  width: 64px;
+  background-color: #304156;
 }
 
-.menu-list {
+.sidebar-menu {
   flex: 1;
+  border: none;
   overflow-y: auto;
-  padding: 10px 0;
+  overflow-x: hidden;
 }
 
-.menu-item {
-  padding: 0 20px;
-  height: 44px;
-  line-height: 44px;
-  color: #bfcbd9;
-  cursor: pointer;
-  white-space: nowrap;
-  display: flex;
-  align-items: center;
-  gap: 12px;
+:deep(.el-menu) {
+  border-right: none;
+  background-color: #304156;
 }
 
-/* 收起时的样式 */
-.sidebar-container.collapsed .menu-item {
-  justify-content: center;
-  padding: 0;
+:deep(.el-sub-menu__title) {
+  color: #bfcbd9;
+  background-color: #304156;
 }
 
-.menu-item:hover {
-  background-color: #263445;
-  color: #fff;
+:deep(.el-sub-menu__title:hover) {
+  background-color: #263445 !important;
+  color: #fff !important;
 }
 
-.menu-item.active {
-  background-color: #409eff;
-  color: #fff;
+:deep(.el-menu-item) {
+  color: #bfcbd9;
+  background-color: #304156;
 }
 
-/* 图标样式 */
-.menu-icon {
-  width: 20px;
-  height: 20px;
-  flex-shrink: 0;
+:deep(.el-menu-item:hover) {
+  background-color: #263445 !important;
+  color: #fff !important;
 }
 
-/* 文字样式 */
-.menu-text {
-  font-size: 14px;
+:deep(.el-menu-item.is-active) {
+  background-color: #409eff !important;
+  color: #fff !important;
 }
 
-/* 收起时隐藏文字 */
-.sidebar-container.collapsed .menu-text {
+.sidebar-container.collapsed :deep(.el-sub-menu__title span) {
   display: none;
 }
 
-/* 收起时图标居中 */
-.sidebar-container.collapsed .menu-icon {
-  margin: 0;
+.sidebar-container.collapsed :deep(.el-menu-item span) {
+  display: none;
 }
 
 .collapse-btn {
@@ -170,6 +263,7 @@ export default {
   color: #bfcbd9;
   cursor: pointer;
   border-top: 1px solid #1f2d3d;
+  background-color: #304156;
 }
 
 .collapse-btn:hover {