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 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_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "", VITE_ROUTER_HISTORY: "",
VITE_CDN: false, VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none" VITE_COMPRESSION: "none"
}; };

View File

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

View File

@ -135,7 +135,7 @@
"terser": "^5.17.1", "terser": "^5.17.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"unplugin-vue-define-options": "1.1.6", "unplugin-vue-define-options": "1.1.6",
"vite": "^4.3.4", "vite": "^4.3.5",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6", "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"> <script setup lang="ts">
import { isEqual } from "@pureadmin/utils"; import { isEqual } from "@pureadmin/utils";
import { routerArrays } from "@/layout/types";
import { transformI18n } from "@/plugins/i18n"; import { transformI18n } from "@/plugins/i18n";
import { ref, watch, onMounted, toRaw } from "vue"; import { ref, watch, onMounted, toRaw } from "vue";
import { getParentPaths, findRouteByPath } from "@/router/utils"; import { getParentPaths, findRouteByPath } from "@/router/utils";
@ -10,14 +11,9 @@ const route = useRoute();
const levelList = ref([]); const levelList = ref([]);
const router = useRouter(); const router = useRouter();
const routes: any = router.options.routes; const routes: any = router.options.routes;
const { VITE_HIDE_HOME } = import.meta.env;
const multiTags: any = useMultiTagsStoreHook().multiTags; 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 => { const getBreadcrumb = (): void => {
// //
let currentRoute; let currentRoute;
@ -35,7 +31,7 @@ const getBreadcrumb = (): void => {
} }
}); });
} else { } else {
currentRoute = findRouteByPath(router.currentRoute.value.path, multiTags); currentRoute = findRouteByPath(router.currentRoute.value.path, routes);
} }
// //
const parentRoutes = getParentPaths( const parentRoutes = getParentPaths(
@ -53,15 +49,7 @@ const getBreadcrumb = (): void => {
if (currentRoute?.path !== "/welcome") matched.push(currentRoute); if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
if (!isDashboard(matched[0])) { if (VITE_HIDE_HOME === "false") matched = routerArrays.concat(matched);
matched = [
{
path: "/welcome",
parentPath: "/",
meta: { title: "menus.hshome" }
} as unknown as RouteLocationMatched
].concat(matched);
}
matched.forEach((item, index) => { matched.forEach((item, index) => {
if (currentRoute?.query || currentRoute?.params) return; if (currentRoute?.query || currentRoute?.params) return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { $t } from "@/plugins/i18n"; import { $t } from "@/plugins/i18n";
import { home } from "@/router/enums"; import { home } from "@/router/enums";
const { VITE_HIDE_HOME } = import.meta.env;
const Layout = () => import("@/layout/index.vue"); const Layout = () => import("@/layout/index.vue");
export default { export default {
@ -18,7 +19,8 @@ export default {
name: "Welcome", name: "Welcome",
component: () => import("@/views/welcome/index.vue"), component: () => import("@/views/welcome/index.vue"),
meta: { 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", path: "/redirect",
component: Layout, component: Layout,
meta: { meta: {
icon: "homeFilled", title: $t("status.hsLoad"),
title: $t("menus.hshome"),
showLink: false, showLink: false,
rank: 102 rank: 102
}, },

View File

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

View File

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

View File

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

1
types/global.d.ts vendored
View File

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