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

* feat: 可配置首页显示与隐藏
This commit is contained in:
RealityBoy 2023-05-05 22:55:12 +08:00 committed by GitHub
parent d49b23e8f9
commit 9d0c3f305d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 4850 additions and 2053 deletions

3
.env
View File

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

View File

@ -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"
};

View File

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

View File

@ -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",

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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) {

View File

@ -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,

View File

@ -1,6 +1,9 @@
import type { IconifyIcon } from "@iconify/vue";
const { VITE_HIDE_HOME } = import.meta.env;
export const routerArrays: Array<RouteConfigs> = [
export const routerArrays: Array<RouteConfigs> =
VITE_HIDE_HOME === "false"
? [
{
path: "/welcome",
parentPath: "/",
@ -9,7 +12,8 @@ export const routerArrays: Array<RouteConfigs> = [
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 = {

View File

@ -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", {

View File

@ -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
}
}
]

View File

@ -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
},

View File

@ -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,

View File

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

View File

@ -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
types/global.d.ts vendored
View File

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