From 296db3789b99d7ffa8fdda5bcd6cb1694bcce395 Mon Sep 17 00:00:00 2001 From: xiaoming <1923740402@qq.com> Date: Tue, 9 Apr 2024 17:07:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B7=AF=E7=94=B1=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AE=E7=9A=84`fixedTag`=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=EF=BC=8C=E4=BD=9C=E7=94=A8=E4=B8=BA=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E5=90=8D=E7=A7=B0=E6=98=AF=E5=90=A6=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E6=98=BE=E7=A4=BA=E5=9C=A8=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=E4=B8=94=E4=B8=8D=E5=8F=AF=E5=85=B3=E9=97=AD=20(#1047)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/components/tag/index.scss | 4 ++ src/layout/components/tag/index.vue | 61 +++++++++++++++++------ src/router/utils.ts | 10 +++- src/store/modules/multiTags.ts | 8 +++- src/store/modules/permission.ts | 12 ++++- src/views/system/menu/README.md | 1 + src/views/system/menu/form.vue | 72 +++++++++++++++++----------- src/views/system/menu/utils/enums.ts | 14 ++++++ src/views/system/menu/utils/hook.tsx | 1 + src/views/system/menu/utils/types.ts | 1 + types/router.d.ts | 4 +- 11 files changed, 142 insertions(+), 46 deletions(-) diff --git a/src/layout/components/tag/index.scss b/src/layout/components/tag/index.scss index 1c0d9ab99..cc122dd2e 100644 --- a/src/layout/components/tag/index.scss +++ b/src/layout/components/tag/index.scss @@ -90,6 +90,10 @@ padding: 0 12px; } } + + .fixed-tag { + padding: 0 12px; + } } } diff --git a/src/layout/components/tag/index.vue b/src/layout/components/tag/index.vue index e9a76a6ba..943366030 100644 --- a/src/layout/components/tag/index.vue +++ b/src/layout/components/tag/index.vue @@ -8,6 +8,7 @@ import { onClickOutside } from "@vueuse/core"; import { handleAliveRoute, getTopMenu } from "@/router/utils"; import { useSettingStoreHook } from "@/store/modules/settings"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; +import { usePermissionStoreHook } from "@/store/modules/permission"; import { ref, watch, unref, toRaw, nextTick, onBeforeUnmount } from "vue"; import { delay, @@ -59,6 +60,10 @@ const contextmenuRef = ref(); const isShowArrow = ref(false); const topPath = getTopMenu()?.path; const { VITE_HIDE_HOME } = import.meta.env; +const fixedTags = [ + ...routerArrays, + ...usePermissionStoreHook().flatteningRoutes.filter(v => v?.meta?.fixedTag) +]; const dynamicTagView = async () => { await nextTick(); @@ -228,10 +233,13 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) { other?: boolean ): void => { if (other) { - useMultiTagsStoreHook().handleTags("equal", [ - VITE_HIDE_HOME === "false" ? routerArrays[0] : toRaw(getTopMenu()), - obj - ]); + useMultiTagsStoreHook().handleTags( + "equal", + [ + VITE_HIDE_HOME === "false" ? fixedTags : toRaw(getTopMenu()), + obj + ].flat() + ); } else { useMultiTagsStoreHook().handleTags("splice", "", { startIndex, @@ -244,7 +252,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) { if (tag === "other") { spliceRoute(1, 1, true); } else if (tag === "left") { - spliceRoute(1, valueIndex - 1); + spliceRoute(fixedTags.length, valueIndex - 1, true); } else if (tag === "right") { spliceRoute(valueIndex + 1, multiTags.value.length); } else { @@ -321,10 +329,11 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) { case 5: // 关闭全部标签页 useMultiTagsStoreHook().handleTags("splice", "", { - startIndex: 1, + startIndex: fixedTags.length, length: multiTags.value.length }); router.push(topPath); + // router.push(fixedTags[fixedTags.length - 1]?.path); handleAliveRoute(route as ToRouteType); break; case 6: @@ -363,10 +372,14 @@ function showMenus(value: boolean) { }); } -function disabledMenus(value: boolean) { +function disabledMenus(value: boolean, fixedTag = false) { Array.of(1, 2, 3, 4, 5).forEach(v => { tagsViews[v].disabled = value; }); + if (fixedTag) { + tagsViews[2].show = false; + tagsViews[2].disabled = true; + } } /** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */ @@ -383,6 +396,13 @@ function showMenuModel( } else { currentIndex = allRoute.findIndex(v => isEqual(v.query, query)); } + function fixedTagDisabled() { + if (allRoute[currentIndex]?.meta?.fixedTag) { + Array.of(1, 2, 3, 4, 5).forEach(v => { + tagsViews[v].disabled = true; + }); + } + } showMenus(true); @@ -401,6 +421,7 @@ function showMenuModel( tagsViews[v].disabled = false; }); tagsViews[2].disabled = true; + fixedTagDisabled(); } else if (currentIndex === 1 && routeLength === 2) { disabledMenus(false); // 左侧的菜单是顶级菜单,右侧不存在别的菜单 @@ -408,6 +429,7 @@ function showMenuModel( tagsViews[v].show = false; tagsViews[v].disabled = true; }); + fixedTagDisabled(); } else if (routeLength - 1 === currentIndex && currentIndex !== 0) { // 当前路由是所有路由中的最后一个 tagsViews[3].show = false; @@ -415,18 +437,24 @@ function showMenuModel( tagsViews[v].disabled = false; }); tagsViews[3].disabled = true; + if (allRoute[currentIndex - 1]?.meta?.fixedTag) { + tagsViews[2].show = false; + tagsViews[2].disabled = true; + } + fixedTagDisabled(); } else if (currentIndex === 0 || currentPath === `/redirect${topPath}`) { // 当前路由为顶级菜单 disabledMenus(true); } else { - disabledMenus(false); + disabledMenus(false, allRoute[currentIndex - 1]?.meta?.fixedTag); + fixedTagDisabled(); } } function openMenu(tag, e) { closeMenu(); - if (tag.path === topPath) { - // 右键菜单为顶级菜单,只显示刷新 + if (tag.path === topPath || tag?.meta?.fixedTag) { + // 右键菜单为顶级菜单或拥有 fixedTag 属性,只显示刷新 showMenus(false); tagsViews[0].show = true; } else if (route.path !== tag.path && route.name !== tag.name) { @@ -485,7 +513,6 @@ function tagOnClick(item) { } else { router.push({ path }); } - // showMenuModel(item?.path, item?.query); } onClickOutside(contextmenuRef, closeMenu, { @@ -549,7 +576,11 @@ onBeforeUnmount(() => { v-for="(item, index) in multiTags" :ref="'dynamic' + index" :key="index" - :class="['scroll-item is-closable', linkIsActive(item)]" + :class="[ + 'scroll-item is-closable', + linkIsActive(item), + !isAllEmpty(item?.meta?.fixedTag) && 'fixed-tag' + ]" @contextmenu.prevent="openMenu(item, $event)" @mouseenter.prevent="onMouseenter(index)" @mouseleave.prevent="onMouseleave(index)" @@ -562,8 +593,10 @@ onBeforeUnmount(() => { import("@/layout/frameView.vue"); @@ -178,6 +178,14 @@ function handleAsyncRoutes(routeList) { ); usePermissionStoreHook().handleWholeMenus(routeList); } + if (!useMultiTagsStoreHook().getMultiTagsCache) { + useMultiTagsStoreHook().handleTags("equal", [ + ...routerArrays, + ...usePermissionStoreHook().flatteningRoutes.filter( + v => v?.meta?.fixedTag + ) + ]); + } addPathMatch(); } diff --git a/src/store/modules/multiTags.ts b/src/store/modules/multiTags.ts index cad8b3184..1081f9340 100644 --- a/src/store/modules/multiTags.ts +++ b/src/store/modules/multiTags.ts @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import { store } from "@/store"; import { routerArrays } from "@/layout/types"; +import { usePermissionStoreHook } from "./permission"; import { responsiveStorageNameSpace } from "@/config"; import type { multiType, positionType } from "./types"; import { isEqual, isBoolean, isUrl, storageLocal } from "@pureadmin/utils"; @@ -15,7 +16,12 @@ export const useMultiTagsStore = defineStore({ ? storageLocal().getItem( `${responsiveStorageNameSpace()}tags` ) - : [...routerArrays], + : [ + ...routerArrays, + ...usePermissionStoreHook().flatteningRoutes.filter( + v => v?.meta?.fixedTag + ) + ], multiTagsCache: storageLocal().getItem( `${responsiveStorageNameSpace()}configure` )?.multiTagsCache diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index a285f68df..76551ca14 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -4,7 +4,12 @@ import type { cacheType } from "./types"; import { constantMenus } from "@/router"; import { useMultiTagsStoreHook } from "./multiTags"; import { debounce, getKeyList } from "@pureadmin/utils"; -import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils"; +import { + ascending, + filterTree, + filterNoPermissionTree, + formatFlatteningRoutes +} from "@/router/utils"; export const usePermissionStore = defineStore({ id: "pure-permission", @@ -13,6 +18,8 @@ export const usePermissionStore = defineStore({ constantMenus, // 整体路由生成的菜单(静态、动态) wholeMenus: [], + // 整体路由(一维数组格式) + flatteningRoutes: [], // 缓存页面keepAlive cachePageList: [] }), @@ -22,6 +29,9 @@ export const usePermissionStore = defineStore({ this.wholeMenus = filterNoPermissionTree( filterTree(ascending(this.constantMenus.concat(routes))) ); + this.flatteningRoutes = formatFlatteningRoutes( + this.constantMenus.concat(routes) + ); }, cacheOperate({ mode, name }: cacheType) { const delIndex = this.cachePageList.findIndex(v => v === name); diff --git a/src/views/system/menu/README.md b/src/views/system/menu/README.md index 9a01a9707..10eb4a8c5 100644 --- a/src/views/system/menu/README.md +++ b/src/views/system/menu/README.md @@ -20,6 +20,7 @@ | `frameLoading` | 加载动画(内嵌的`iframe`页面是否开启首次加载动画) | | `keepAlive` | 缓存页面(是否缓存该路由页面,开启后会保存该页面的整体状态,刷新后会清空状态) | | `hiddenTag` | 标签页(当前菜单名称或自定义信息禁止添加到标签页) | +| `fixedTag` | 固定标签页(当前菜单名称是否固定显示在标签页且不可关闭) | | `showLink` | 菜单(是否显示该菜单) | | `showParent` | 父级菜单(是否显示父级菜单 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E7%AC%AC%E4%B8%80%E7%A7%8D-%E8%AF%A5%E6%A8%A1%E5%BC%8F%E9%92%88%E5%AF%B9%E7%88%B6%E7%BA%A7%E8%8F%9C%E5%8D%95%E4%B8%8B%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84%E6%83%85%E5%86%B5-%E5%9C%A8%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84-meta-%E5%B1%9E%E6%80%A7%E4%B8%AD%E5%8A%A0%E4%B8%8A-showparent-true-%E5%8D%B3%E5%8F%AF)) | diff --git a/src/views/system/menu/form.vue b/src/views/system/menu/form.vue index 46892f2f0..711e297f7 100644 --- a/src/views/system/menu/form.vue +++ b/src/views/system/menu/form.vue @@ -10,6 +10,7 @@ import ReAnimateSelector from "@/components/ReAnimateSelector"; import { menuTypeOptions, showLinkOptions, + fixedTagOptions, keepAliveOptions, hiddenTagOptions, showParentOptions, @@ -37,6 +38,7 @@ const props = withDefaults(defineProps(), { frameLoading: true, keepAlive: false, hiddenTag: false, + fixedTag: false, showLink: true, showParent: false }) @@ -258,33 +260,6 @@ defineExpose({ getRef }); - - - - - - - - - - - @@ -321,6 +296,47 @@ defineExpose({ getRef }); /> + + + + + + + + + + + + + + + + + diff --git a/src/views/system/menu/utils/enums.ts b/src/views/system/menu/utils/enums.ts index 6eb3d5d6f..4466b61d7 100644 --- a/src/views/system/menu/utils/enums.ts +++ b/src/views/system/menu/utils/enums.ts @@ -32,6 +32,19 @@ const showLinkOptions: Array = [ } ]; +const fixedTagOptions: Array = [ + { + label: "固定", + tip: "当前菜单名称固定显示在标签页且不可关闭", + value: true + }, + { + label: "不固定", + tip: "当前菜单名称不固定显示在标签页且可关闭", + value: false + } +]; + const keepAliveOptions: Array = [ { label: "缓存", @@ -87,6 +100,7 @@ const frameLoadingOptions: Array = [ export { menuTypeOptions, showLinkOptions, + fixedTagOptions, keepAliveOptions, hiddenTagOptions, showParentOptions, diff --git a/src/views/system/menu/utils/hook.tsx b/src/views/system/menu/utils/hook.tsx index af3023ed7..6aa8e8127 100644 --- a/src/views/system/menu/utils/hook.tsx +++ b/src/views/system/menu/utils/hook.tsx @@ -155,6 +155,7 @@ export function useMenu() { frameLoading: row?.frameLoading ?? true, keepAlive: row?.keepAlive ?? false, hiddenTag: row?.hiddenTag ?? false, + fixedTag: row?.fixedTag ?? false, showLink: row?.showLink ?? true, showParent: row?.showParent ?? false } diff --git a/src/views/system/menu/utils/types.ts b/src/views/system/menu/utils/types.ts index 7259825c5..f07d1f71b 100644 --- a/src/views/system/menu/utils/types.ts +++ b/src/views/system/menu/utils/types.ts @@ -19,6 +19,7 @@ interface FormItemProps { frameLoading: boolean; keepAlive: boolean; hiddenTag: boolean; + fixedTag: boolean; showLink: boolean; showParent: boolean; } diff --git a/types/router.d.ts b/types/router.d.ts index 752377d10..a03ba0e35 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -45,8 +45,10 @@ declare global { /** 离场动画 */ leaveTransition?: string; }; - // 是否不添加信息到标签页,(默认`false`) + /** 当前菜单名称或自定义信息禁止添加到标签页(默认`false`) */ hiddenTag?: boolean; + /** 当前菜单名称是否固定显示在标签页且不可关闭(默认`false`) */ + fixedTag?: boolean; /** 动态路由可打开的最大数量 `可选` */ dynamicLevel?: number; /** 将某个菜单激活