浏览代码

feat: 可配置首页菜单显示与隐藏 (#539)

* feat: 可配置首页显示与隐藏
RealityBoy 1 年之前
父节点
当前提交
9d0c3f305d

+ 3 - 0
.env

@@ -1,2 +1,5 @@
 # 平台本地运行端口号
 VITE_PORT = 8848
+
+# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
+VITE_HIDE_HOME = false

+ 1 - 0
build/index.ts

@@ -6,6 +6,7 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
     VITE_PUBLIC_PATH: "",
     VITE_ROUTER_HISTORY: "",
     VITE_CDN: false,
+    VITE_HIDE_HOME: "false",
     VITE_COMPRESSION: "none"
   };
 

+ 0 - 1
build/optimize.ts

@@ -12,7 +12,6 @@ const include = [
   "axios",
   "pinia",
   "swiper",
-  "echarts",
   "intro.js",
   "vue-i18n",
   "js-cookie",

+ 1 - 1
package.json

@@ -135,7 +135,7 @@
     "terser": "^5.17.1",
     "typescript": "^5.0.4",
     "unplugin-vue-define-options": "1.1.6",
-    "vite": "^4.3.4",
+    "vite": "^4.3.5",
     "vite-plugin-cdn-import": "^0.3.5",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-mock": "^2.9.6",

文件差异内容过多而无法显示
+ 471 - 307
pnpm-lock.yaml


+ 4 - 16
src/layout/components/sidebar/breadCrumb.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { isEqual } from "@pureadmin/utils";
+import { routerArrays } from "@/layout/types";
 import { transformI18n } from "@/plugins/i18n";
 import { ref, watch, onMounted, toRaw } from "vue";
 import { getParentPaths, findRouteByPath } from "@/router/utils";
@@ -10,14 +11,9 @@ const route = useRoute();
 const levelList = ref([]);
 const router = useRouter();
 const routes: any = router.options.routes;
+const { VITE_HIDE_HOME } = import.meta.env;
 const multiTags: any = useMultiTagsStoreHook().multiTags;
 
-const isDashboard = (route: RouteLocationMatched): boolean | string => {
-  const name = route && (route.name as string);
-  if (!name) return false;
-  return name.trim().toLocaleLowerCase() === "Welcome".toLocaleLowerCase();
-};
-
 const getBreadcrumb = (): void => {
   // 当前路由信息
   let currentRoute;
@@ -35,7 +31,7 @@ const getBreadcrumb = (): void => {
       }
     });
   } else {
-    currentRoute = findRouteByPath(router.currentRoute.value.path, multiTags);
+    currentRoute = findRouteByPath(router.currentRoute.value.path, routes);
   }
   // 当前路由的父级路径组成的数组
   const parentRoutes = getParentPaths(
@@ -53,15 +49,7 @@ const getBreadcrumb = (): void => {
 
   if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
 
-  if (!isDashboard(matched[0])) {
-    matched = [
-      {
-        path: "/welcome",
-        parentPath: "/",
-        meta: { title: "menus.hshome" }
-      } as unknown as RouteLocationMatched
-    ].concat(matched);
-  }
+  if (VITE_HIDE_HOME === "false") matched = routerArrays.concat(matched);
 
   matched.forEach((item, index) => {
     if (currentRoute?.query || currentRoute?.params) return;

+ 2 - 2
src/layout/components/sidebar/horizontal.vue

@@ -19,7 +19,7 @@ const {
   title,
   routers,
   logout,
-  backHome,
+  backTopMenu,
   onPanel,
   menuSelect,
   username,
@@ -45,7 +45,7 @@ watch(
     v-loading="usePermissionStoreHook().wholeMenus.length === 0"
     class="horizontal-header"
   >
-    <div class="horizontal-header-left" @click="backHome">
+    <div class="horizontal-header-left" @click="backTopMenu">
       <img src="/logo.svg" alt="logo" />
       <span>{{ title }}</span>
     </div>

+ 4 - 2
src/layout/components/sidebar/logo.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { getTopMenu } from "@/router/utils";
 import { useNav } from "@/layout/hooks/useNav";
 
 const props = defineProps({
@@ -6,6 +7,7 @@ const props = defineProps({
 });
 
 const { title } = useNav();
+const topPath = getTopMenu().path;
 </script>
 
 <template>
@@ -16,7 +18,7 @@ const { title } = useNav();
         key="props.collapse"
         :title="title"
         class="sidebar-logo-link"
-        to="/"
+        :to="topPath"
       >
         <img src="/logo.svg" alt="logo" />
         <span class="sidebar-title">{{ title }}</span>
@@ -26,7 +28,7 @@ const { title } = useNav();
         key="expand"
         :title="title"
         class="sidebar-logo-link"
-        to="/"
+        :to="topPath"
       >
         <img src="/logo.svg" alt="logo" />
         <span class="sidebar-title">{{ title }}</span>

+ 4 - 7
src/layout/components/sidebar/sidebarItem.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 import path from "path";
 import { getConfig } from "@/config";
+import { menuType } from "../../types";
 import extraIcon from "./extraIcon.vue";
-import { childrenType } from "../../types";
 import { useNav } from "@/layout/hooks/useNav";
 import { transformI18n } from "@/plugins/i18n";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
@@ -17,7 +17,7 @@ const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
 
 const props = defineProps({
   item: {
-    type: Object as PropType<childrenType>
+    type: Object as PropType<menuType>
   },
   isNest: {
     type: Boolean,
@@ -112,7 +112,7 @@ const expandCloseIcon = computed(() => {
   };
 });
 
-const onlyOneChild: childrenType = ref(null);
+const onlyOneChild: menuType = ref(null);
 // 存放菜单是否存在showTooltip属性标识
 const hoverMenuMap = new WeakMap();
 // 存储菜单文本dom元素
@@ -149,10 +149,7 @@ function overflowSlice(text, item?: any) {
   return newText;
 }
 
-function hasOneShowingChild(
-  children: childrenType[] = [],
-  parent: childrenType
-) {
+function hasOneShowingChild(children: menuType[] = [], parent: menuType) {
   const showingChildren = children.filter((item: any) => {
     onlyOneChild.value = item;
     return true;

+ 18 - 13
src/layout/components/tag/index.vue

@@ -4,11 +4,11 @@ import { emitter } from "@/utils/mitt";
 import { RouteConfigs } from "../../types";
 import { useTags } from "../../hooks/useTag";
 import { routerArrays } from "@/layout/types";
-import { handleAliveRoute } from "@/router/utils";
 import { isEqual, isAllEmpty } from "@pureadmin/utils";
+import { handleAliveRoute, getTopMenu } from "@/router/utils";
 import { useSettingStoreHook } from "@/store/modules/settings";
-import { ref, watch, unref, nextTick, onBeforeMount } from "vue";
 import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
+import { ref, watch, unref, toRaw, nextTick, onBeforeMount } from "vue";
 import { useResizeObserver, useDebounceFn, useFullscreen } from "@vueuse/core";
 
 import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
@@ -50,6 +50,8 @@ const tabDom = ref();
 const containerDom = ref();
 const scrollbarDom = ref();
 const isShowArrow = ref(false);
+const topPath = getTopMenu().path;
+const { VITE_HIDE_HOME } = import.meta.env;
 const { isFullscreen, toggle } = useFullscreen();
 
 const dynamicTagView = () => {
@@ -165,7 +167,7 @@ function onFresh() {
   const { fullPath, query } = unref(route);
   router.replace({
     path: "/redirect" + fullPath,
-    query: query
+    query
   });
 }
 
@@ -190,7 +192,10 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
     other?: boolean
   ): void => {
     if (other) {
-      useMultiTagsStoreHook().handleTags("equal", [routerArrays[0], obj]);
+      useMultiTagsStoreHook().handleTags("equal", [
+        VITE_HIDE_HOME === "false" ? routerArrays[0] : toRaw(getTopMenu()),
+        obj
+      ]);
     } else {
       useMultiTagsStoreHook().handleTags("splice", "", {
         startIndex,
@@ -283,7 +288,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
         startIndex: 1,
         length: multiTags.value.length
       });
-      router.push("/welcome");
+      router.push(topPath);
       handleAliveRoute(route as toRouteType);
       break;
     case 6:
@@ -340,7 +345,7 @@ function disabledMenus(value: boolean) {
   });
 }
 
-/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
+/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
 function showMenuModel(
   currentPath: string,
   query: object = {},
@@ -362,11 +367,11 @@ function showMenuModel(
   }
 
   /**
-   * currentIndex为1时,左侧的菜单是首页,则不显示关闭左侧标签页
+   * currentIndex为1时,左侧的菜单顶级菜单,则不显示关闭左侧标签页
    * 如果currentIndex等于routeLength-1,右侧没有菜单,则不显示关闭右侧标签页
    */
   if (currentIndex === 1 && routeLength !== 2) {
-    // 左侧的菜单是首页,右侧存在别的菜单
+    // 左侧的菜单是顶级菜单,右侧存在别的菜单
     tagsViews[2].show = false;
     Array.of(1, 3, 4, 5).forEach(v => {
       tagsViews[v].disabled = false;
@@ -374,7 +379,7 @@ function showMenuModel(
     tagsViews[2].disabled = true;
   } else if (currentIndex === 1 && routeLength === 2) {
     disabledMenus(false);
-    // 左侧的菜单是首页,右侧不存在别的菜单
+    // 左侧的菜单是顶级菜单,右侧不存在别的菜单
     Array.of(2, 3, 4).forEach(v => {
       tagsViews[v].show = false;
       tagsViews[v].disabled = true;
@@ -386,8 +391,8 @@ function showMenuModel(
       tagsViews[v].disabled = false;
     });
     tagsViews[3].disabled = true;
-  } else if (currentIndex === 0 || currentPath === "/redirect/welcome") {
-    // 当前路由为首页
+  } else if (currentIndex === 0 || currentPath === `/redirect${topPath}`) {
+    // 当前路由为顶级菜单
     disabledMenus(true);
   } else {
     disabledMenus(false);
@@ -396,8 +401,8 @@ function showMenuModel(
 
 function openMenu(tag, e) {
   closeMenu();
-  if (tag.path === "/welcome") {
-    // 右键菜单为首页,只显示刷新
+  if (tag.path === topPath) {
+    // 右键菜单为顶级菜单,只显示刷新
     showMenus(false);
     tagsViews[0].show = true;
   } else if (route.path !== tag.path && route.name !== tag.name) {

+ 4 - 3
src/layout/hooks/useNav.ts

@@ -3,6 +3,7 @@ import { getConfig } from "@/config";
 import { useRouter } from "vue-router";
 import { emitter } from "@/utils/mitt";
 import { routeMetaType } from "../types";
+import { getTopMenu } from "@/router/utils";
 import { useGlobal } from "@pureadmin/utils";
 import { transformI18n } from "@/plugins/i18n";
 import { router, remainingPaths } from "@/router";
@@ -85,8 +86,8 @@ export function useNav() {
     useUserStoreHook().logOut();
   }
 
-  function backHome() {
-    router.push("/welcome");
+  function backTopMenu() {
+    router.push(getTopMenu().path);
   }
 
   function onPanel() {
@@ -154,7 +155,7 @@ export function useNav() {
     logout,
     routers,
     $storage,
-    backHome,
+    backTopMenu,
     onPanel,
     getDivStyle,
     changeTitle,

+ 19 - 12
src/layout/types.ts

@@ -1,15 +1,19 @@
 import type { IconifyIcon } from "@iconify/vue";
+const { VITE_HIDE_HOME } = import.meta.env;
 
-export const routerArrays: Array<RouteConfigs> = [
-  {
-    path: "/welcome",
-    parentPath: "/",
-    meta: {
-      title: "menus.hshome",
-      icon: "homeFilled"
-    }
-  }
-];
+export const routerArrays: Array<RouteConfigs> =
+  VITE_HIDE_HOME === "false"
+    ? [
+        {
+          path: "/welcome",
+          parentPath: "/",
+          meta: {
+            title: "menus.hshome",
+            icon: "homeFilled"
+          }
+        }
+      ]
+    : [];
 
 export type routeMetaType = {
   title?: string;
@@ -58,20 +62,23 @@ export interface setType {
   hideTabs: boolean;
 }
 
-export type childrenType = {
+export type menuType = {
+  id?: number;
   path?: string;
   noShowingChildren?: boolean;
-  children?: childrenType[];
+  children?: menuType[];
   value: unknown;
   meta?: {
     icon?: string;
     title?: string;
+    rank?: number;
     showParent?: boolean;
     extraIcon?: string;
   };
   showTooltip?: boolean;
   parentId?: number;
   pathList?: number[];
+  redirect?: string;
 };
 
 export type themeColorsType = {

+ 8 - 0
src/router/index.ts

@@ -13,6 +13,7 @@ import {
 } from "vue-router";
 import {
   ascending,
+  getTopMenu,
   initRouter,
   isOneOfArray,
   getHistoryMode,
@@ -96,6 +97,8 @@ export function resetRouter() {
 /** 路由白名单 */
 const whiteList = ["/login"];
 
+const { VITE_HIDE_HOME } = import.meta.env;
+
 router.beforeEach((to: toRouteType, _from, next) => {
   if (to.meta?.keepAlive) {
     handleAliveRoute(to, "add");
@@ -125,6 +128,10 @@ router.beforeEach((to: toRouteType, _from, next) => {
     if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
       next({ path: "/error/403" });
     }
+    // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
+    if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
+      next({ path: "/error/404" });
+    }
     if (_from?.name) {
       // name为超链接
       if (externalLink) {
@@ -146,6 +153,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
               path,
               router.options.routes[0].children
             );
+            getTopMenu(true);
             // query、params模式路由传参数的标签页不在此处处理
             if (route && route.meta?.title) {
               useMultiTagsStoreHook().handleTags("push", {

+ 3 - 1
src/router/modules/home.ts

@@ -1,5 +1,6 @@
 import { $t } from "@/plugins/i18n";
 import { home } from "@/router/enums";
+const { VITE_HIDE_HOME } = import.meta.env;
 const Layout = () => import("@/layout/index.vue");
 
 export default {
@@ -18,7 +19,8 @@ export default {
       name: "Welcome",
       component: () => import("@/views/welcome/index.vue"),
       meta: {
-        title: $t("menus.hshome")
+        title: $t("menus.hshome"),
+        showLink: VITE_HIDE_HOME === "true" ? false : true
       }
     }
   ]

+ 1 - 2
src/router/modules/remaining.ts

@@ -16,8 +16,7 @@ export default [
     path: "/redirect",
     component: Layout,
     meta: {
-      icon: "homeFilled",
-      title: $t("menus.hshome"),
+      title: $t("status.hsLoad"),
       showLink: false,
       rank: 102
     },

+ 10 - 0
src/router/utils.ts

@@ -17,8 +17,10 @@ import {
   isIncludeAllChildren
 } from "@pureadmin/utils";
 import { getConfig } from "@/config";
+import { menuType } from "@/layout/types";
 import { buildHierarchyTree } from "@/utils/tree";
 import { sessionKey, type DataInfo } from "@/utils/auth";
+import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
 import { usePermissionStoreHook } from "@/store/modules/permission";
 const IFrame = () => import("@/layout/frameView.vue");
 // https://cn.vitejs.dev/guide/features.html#glob-import
@@ -351,12 +353,20 @@ function hasAuth(value: string | Array<string>): boolean {
   return isAuths ? true : false;
 }
 
+/** 获取所有菜单中的第一个菜单(顶级菜单)*/
+function getTopMenu(tag = false): menuType {
+  const topMenu = usePermissionStoreHook().wholeMenus[0]?.children[0];
+  tag && useMultiTagsStoreHook().handleTags("push", topMenu);
+  return topMenu;
+}
+
 export {
   hasAuth,
   getAuths,
   ascending,
   filterTree,
   initRouter,
+  getTopMenu,
   addPathMatch,
   isOneOfArray,
   getHistoryMode,

+ 1 - 1
src/utils/responsive.ts

@@ -31,7 +31,7 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
     },
     config.MultiTagsCache
       ? {
-          // 默认显示首页tag
+          // 默认显示顶级菜单tag
           tags: Storage.getData("tags", nameSpace) ?? routerArrays
         }
       : {}

+ 2 - 2
src/views/login/index.vue

@@ -18,13 +18,13 @@ import TypeIt from "@/components/ReTypeit";
 import qrCode from "./components/qrCode.vue";
 import regist from "./components/regist.vue";
 import update from "./components/update.vue";
-import { initRouter } from "@/router/utils";
 import { useNav } from "@/layout/hooks/useNav";
 import type { FormInstance } from "element-plus";
 import { $t, transformI18n } from "@/plugins/i18n";
 import { operates, thirdParty } from "./utils/enums";
 import { useLayout } from "@/layout/hooks/useLayout";
 import { useUserStoreHook } from "@/store/modules/user";
+import { initRouter, getTopMenu } from "@/router/utils";
 import { bg, avatar, illustration } from "./utils/static";
 import { ReImageVerify } from "@/components/ReImageVerify";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
@@ -76,7 +76,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
           if (res.success) {
             // 获取后端路由
             initRouter().then(() => {
-              router.push("/");
+              router.push(getTopMenu(true).path);
               message("登录成功", { type: "success" });
             });
           }

+ 1 - 0
types/global.d.ts

@@ -63,6 +63,7 @@ declare global {
     VITE_PUBLIC_PATH: string;
     VITE_ROUTER_HISTORY: string;
     VITE_CDN: boolean;
+    VITE_HIDE_HOME: string;
     VITE_COMPRESSION: ViteCompression;
   }
 

部分文件因为文件数量过多而无法显示