Parcourir la source

feat: 菜单支持`a`标签右键的所有浏览器行为(在新标签页中、新窗口中打开链接,拖拽到新标签页打开等) (#936)

* feat: 菜单支持a标签右键的所有浏览器行为(在新标签页中、新窗口中打开链接,拖拽到新标签页打开等)

* feat: 修复添加a标签样式问题

* feat: 修复windows下双滚动条问题

* feat: 修复添加a标签样式问题
Banana Energy il y a 1 an
Parent
commit
2b71e8bd54

+ 36 - 0
src/layout/components/sidebar/linkItem.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import { computed } from "vue";
+import { isUrl } from "@pureadmin/utils";
+import { menuType } from "@/layout/types";
+
+defineOptions({
+  name: "LinkItem"
+});
+
+const props = defineProps<{
+  to: menuType;
+}>();
+
+const isExternalLink = computed(() => isUrl(props.to.name));
+const getLinkProps = (item: menuType) => {
+  if (isExternalLink.value) {
+    return {
+      href: item.name,
+      target: "_blank",
+      rel: "noopener"
+    };
+  }
+  return {
+    to: item
+  };
+};
+</script>
+
+<template>
+  <component
+    :is="isExternalLink ? 'a' : 'router-link'"
+    v-bind="getLinkProps(to)"
+  >
+    <slot />
+  </component>
+</template>

+ 1 - 0
src/layout/components/sidebar/logo.vue

@@ -48,6 +48,7 @@ const { title, getLogo } = useNav();
     flex-wrap: nowrap;
     align-items: center;
     height: 100%;
+    padding-left: 10px;
 
     img {
       display: inline-block;

+ 63 - 48
src/layout/components/sidebar/sidebarItem.vue

@@ -1,19 +1,28 @@
 <script setup lang="ts">
 import path from "path";
 import { getConfig } from "@/config";
+import LinkItem from "./linkItem.vue";
 import { menuType } from "../../types";
 import extraIcon from "./extraIcon.vue";
 import { ReText } from "@/components/ReText";
 import { useNav } from "@/layout/hooks/useNav";
 import { transformI18n } from "@/plugins/i18n";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
-import { type CSSProperties, type PropType, computed, ref, toRaw } from "vue";
+import {
+  type PropType,
+  type CSSProperties,
+  ref,
+  toRaw,
+  computed,
+  useAttrs
+} from "vue";
 
 import ArrowUp from "@iconify-icons/ep/arrow-up-bold";
 import EpArrowDown from "@iconify-icons/ep/arrow-down-bold";
 import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
 import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
 
+const attrs = useAttrs();
 const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
 
 const props = defineProps({
@@ -32,6 +41,7 @@ const props = defineProps({
 
 const getNoDropdownStyle = computed((): CSSProperties => {
   return {
+    width: "100%",
     display: "flex",
     alignItems: "center"
   };
@@ -96,61 +106,66 @@ function resolvePath(routePath) {
 </script>
 
 <template>
-  <el-menu-item
+  <link-item
     v-if="
       hasOneShowingChild(props.item.children, props.item) &&
       (!onlyOneChild.children || onlyOneChild.noShowingChildren)
     "
-    :index="resolvePath(onlyOneChild.path)"
-    :class="{ 'submenu-title-noDropdown': !isNest }"
-    :style="getNoDropdownStyle"
+    :to="item"
   >
-    <div
-      v-if="toRaw(props.item.meta.icon)"
-      class="sub-menu-icon"
-      :style="getSubMenuIconStyle"
+    <el-menu-item
+      :index="resolvePath(onlyOneChild.path)"
+      :class="{ 'submenu-title-noDropdown': !isNest }"
+      :style="getNoDropdownStyle"
+      v-bind="attrs"
     >
-      <component
-        :is="
-          useRenderIcon(
-            toRaw(onlyOneChild.meta.icon) ||
-              (props.item.meta && toRaw(props.item.meta.icon))
-          )
+      <div
+        v-if="toRaw(props.item.meta.icon)"
+        class="sub-menu-icon"
+        :style="getSubMenuIconStyle"
+      >
+        <component
+          :is="
+            useRenderIcon(
+              toRaw(onlyOneChild.meta.icon) ||
+                (props.item.meta && toRaw(props.item.meta.icon))
+            )
+          "
+        />
+      </div>
+      <el-text
+        v-if="
+          (!props.item?.meta.icon &&
+            isCollapse &&
+            layout === 'vertical' &&
+            props.item?.pathList?.length === 1) ||
+          (!onlyOneChild.meta.icon &&
+            isCollapse &&
+            layout === 'mix' &&
+            props.item?.pathList?.length === 2)
         "
-      />
-    </div>
-    <el-text
-      v-if="
-        (!props.item?.meta.icon &&
-          isCollapse &&
-          layout === 'vertical' &&
-          props.item?.pathList?.length === 1) ||
-        (!onlyOneChild.meta.icon &&
-          isCollapse &&
-          layout === 'mix' &&
-          props.item?.pathList?.length === 2)
-      "
-      truncated
-      class="!px-4 !text-inherit"
-    >
-      {{ transformI18n(onlyOneChild.meta.title) }}
-    </el-text>
+        truncated
+        class="!px-4 !text-inherit"
+      >
+        {{ transformI18n(onlyOneChild.meta.title) }}
+      </el-text>
 
-    <template #title>
-      <div :style="getDivStyle">
-        <ReText
-          :tippyProps="{
-            offset: [0, -10],
-            theme: tooltipEffect
-          }"
-          class="!text-inherit"
-        >
-          {{ transformI18n(onlyOneChild.meta.title) }}
-        </ReText>
-        <extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
-      </div>
-    </template>
-  </el-menu-item>
+      <template #title>
+        <div :style="getDivStyle">
+          <ReText
+            :tippyProps="{
+              offset: [0, -10],
+              theme: tooltipEffect
+            }"
+            class="!text-inherit"
+          >
+            {{ transformI18n(onlyOneChild.meta.title) }}
+          </ReText>
+          <extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
+        </div>
+      </template>
+    </el-menu-item>
+  </link-item>
   <el-sub-menu
     v-else
     ref="subMenu"

+ 1 - 0
src/layout/types.ts

@@ -62,6 +62,7 @@ export interface setType {
 
 export type menuType = {
   id?: number;
+  name?: string;
   path?: string;
   noShowingChildren?: boolean;
   children?: menuType[];

+ 14 - 8
src/style/sidebar.scss

@@ -14,6 +14,11 @@
     }
   }
 
+  /* 修复 windows 下双滚动条问题 https://github.com/pure-admin/vue-pure-admin/pull/936#issuecomment-1968125992 */
+  .el-popper.pure-scrollbar {
+    overflow: hidden;
+  }
+
   /* popper menu 超出内容区可滚动 */
   .pure-scrollbar {
     max-height: calc(100vh - calc(50px * 2.5));
@@ -130,11 +135,9 @@
     }
 
     a {
-      display: inline-block;
       display: flex;
       flex-wrap: wrap;
       width: 100%;
-      padding-left: 10px;
     }
 
     .el-menu {
@@ -331,9 +334,18 @@
       margin-top: 0;
     }
 
+    /* 无子菜单时激活 border-bottom */
+    a > .is-active.submenu-title-noDropdown {
+      border-bottom: 2px solid var(--el-menu-active-color);
+    }
+
     .el-menu--popup {
       background-color: $subMenuBg !important;
 
+      a > .is-active.submenu-title-noDropdown {
+        border-bottom: none;
+      }
+
       .el-menu-item {
         color: $menuText;
         background-color: $subMenuBg;
@@ -348,12 +360,6 @@
       }
     }
 
-    /* 无子菜单时激活 border-bottom */
-    .router-link-exact-active > .submenu-title-noDropdown {
-      height: 60px;
-      border-bottom: 2px solid var(--el-menu-active-color);
-    }
-
     /* 子菜单中还有子菜单 */
     .el-menu .el-sub-menu__title {
       min-width: $sideBarWidth !important;