feat(projects): 添加多级菜单页面
This commit is contained in:
parent
2a7709ec04
commit
3f49d6db30
@ -10,6 +10,9 @@ export enum EnumRoutePath {
|
|||||||
'dashboard' = '/dashboard',
|
'dashboard' = '/dashboard',
|
||||||
'dashboard-analysis' = '/dashboard/analysis',
|
'dashboard-analysis' = '/dashboard/analysis',
|
||||||
'dashboard-workbench' = '/dashboard/workbench',
|
'dashboard-workbench' = '/dashboard/workbench',
|
||||||
|
'multimenu' = '/multimenu',
|
||||||
|
'multimenu-first' = '/multimenu/first',
|
||||||
|
'multimenu-first-second' = '/multimenu/first/second',
|
||||||
'exception' = '/exception',
|
'exception' = '/exception',
|
||||||
'exception-403' = '/exception/403',
|
'exception-403' = '/exception/403',
|
||||||
'exception-404' = '/exception/404',
|
'exception-404' = '/exception/404',
|
||||||
@ -28,6 +31,9 @@ export enum EnumRouteTitle {
|
|||||||
'dashboard' = '仪表盘',
|
'dashboard' = '仪表盘',
|
||||||
'dashboard-analysis' = '分析页',
|
'dashboard-analysis' = '分析页',
|
||||||
'dashboard-workbench' = '工作台',
|
'dashboard-workbench' = '工作台',
|
||||||
|
'multimenu' = '多级菜单',
|
||||||
|
'multimenu-first' = '一级菜单',
|
||||||
|
'multimenu-first-second' = '二级菜单',
|
||||||
'exception' = '异常页',
|
'exception' = '异常页',
|
||||||
'exception-403' = '异常页-403',
|
'exception-403' = '异常页-403',
|
||||||
'exception-404' = '异常页-404',
|
'exception-404' = '异常页-404',
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="mb-6px px-4px cursor-pointer" @mouseenter="setTrue" @mouseleave="setFalse">
|
||||||
class="mb-6px px-4px cursor-pointer"
|
|
||||||
@click="handleRouter"
|
|
||||||
@mouseenter="handleMouseEvent('enter')"
|
|
||||||
@mouseleave="handleMouseEvent('leave')"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="flex-center flex-col py-12px rounded-2px"
|
class="flex-center flex-col py-12px rounded-2px"
|
||||||
:class="{ 'text-primary bg-primary bg-opacity-10': isActive, 'text-primary': isHover }"
|
:class="{ 'text-primary bg-primary bg-opacity-10': isActive, 'text-primary': isHover }"
|
||||||
@ -23,10 +18,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { PropType, VNodeChild } from 'vue';
|
import type { PropType, VNodeChild } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
import { useBoolean } from '@/hooks';
|
import { useBoolean } from '@/hooks';
|
||||||
import { useVerticalMixSiderContext } from '@/context';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
routeName: {
|
routeName: {
|
||||||
@ -51,40 +43,8 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const { useVerticalMixSiderInject } = useVerticalMixSiderContext();
|
|
||||||
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||||
const { bool: isMouseEnterMenu, setTrue: setMouseEnterMenu, setFalse: setMouseLeaveMenu } = useBoolean();
|
|
||||||
|
|
||||||
const { setHoverRouteName, showChildMenu, hideChildMenu, isMouseEnterChildMenu } = useVerticalMixSiderInject();
|
|
||||||
|
|
||||||
const isActive = computed(() => props.routeName === props.activeRouteName);
|
const isActive = computed(() => props.routeName === props.activeRouteName);
|
||||||
|
|
||||||
function handleRouter() {
|
|
||||||
router.push({ name: props.routeName });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setActiveHoverRouteName() {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (app.menu.fixedMix && !isMouseEnterChildMenu.value && !isMouseEnterMenu.value) {
|
|
||||||
setHoverRouteName(props.activeRouteName);
|
|
||||||
}
|
|
||||||
setMouseLeaveMenu();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseEvent(type: 'enter' | 'leave') {
|
|
||||||
if (type === 'enter') {
|
|
||||||
setMouseEnterMenu();
|
|
||||||
setTrue();
|
|
||||||
setHoverRouteName(props.routeName);
|
|
||||||
showChildMenu();
|
|
||||||
} else {
|
|
||||||
setFalse();
|
|
||||||
hideChildMenu();
|
|
||||||
setActiveHoverRouteName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -13,13 +13,10 @@
|
|||||||
dark:bg-[#18181c]
|
dark:bg-[#18181c]
|
||||||
"
|
"
|
||||||
:style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
|
:style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
@mouseenter="handleMouseEvent('enter')"
|
|
||||||
@mouseleave="handleMouseEvent('leave')"
|
|
||||||
>
|
>
|
||||||
<header class="header-height flex-y-center justify-between">
|
<header class="header-height flex-y-center justify-between">
|
||||||
<h2 class="pl-8px text-16px text-primary font-bold">{{ title }}</h2>
|
<h2 class="pl-8px text-16px text-primary font-bold">{{ title }}</h2>
|
||||||
|
<div class="px-8px text-16px text-gray-600 cursor-pointer" @click="toggleFixedMixMenu">
|
||||||
<div class="px-8px text-16px cursor-pointer" @click="toggleFixedMixMenu">
|
|
||||||
<icon-mdi:pin-off v-if="app.menu.fixedMix" />
|
<icon-mdi:pin-off v-if="app.menu.fixedMix" />
|
||||||
<icon-mdi:pin v-else />
|
<icon-mdi:pin v-else />
|
||||||
</div>
|
</div>
|
||||||
@ -39,11 +36,14 @@ import { NScrollbar, NMenu } from 'naive-ui';
|
|||||||
import type { MenuOption } from 'naive-ui';
|
import type { MenuOption } from 'naive-ui';
|
||||||
import { useThemeStore, useAppStore } from '@/store';
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
import { useAppTitle } from '@/hooks';
|
import { useAppTitle } from '@/hooks';
|
||||||
import { useVerticalMixSiderContext } from '@/context';
|
|
||||||
import { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
import type { GlobalMenuOption } from '@/interface';
|
import type { GlobalMenuOption } from '@/interface';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
activeRouteName: {
|
activeRouteName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@ -55,23 +55,12 @@ const route = useRoute();
|
|||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { toggleFixedMixMenu } = useAppStore();
|
const { toggleFixedMixMenu } = useAppStore();
|
||||||
const { useVerticalMixSiderInject } = useVerticalMixSiderContext();
|
|
||||||
const title = useAppTitle();
|
const title = useAppTitle();
|
||||||
|
|
||||||
const {
|
|
||||||
childMenuVisible,
|
|
||||||
hoverRouteName,
|
|
||||||
setHoverRouteName,
|
|
||||||
showChildMenu,
|
|
||||||
hideChildMenu,
|
|
||||||
setMouseEnterChildMenu,
|
|
||||||
setMouseLeaveChildMenu
|
|
||||||
} = useVerticalMixSiderInject();
|
|
||||||
|
|
||||||
const childMenus = computed(() => {
|
const childMenus = computed(() => {
|
||||||
const children: MenuOption[] = [];
|
const children: MenuOption[] = [];
|
||||||
menus.some(item => {
|
menus.some(item => {
|
||||||
const flag = item.routeName === hoverRouteName.value && Boolean(item.children?.length);
|
const flag = item.routeName === props.activeRouteName && Boolean(item.children?.length);
|
||||||
if (flag) {
|
if (flag) {
|
||||||
children.push(...item.children!);
|
children.push(...item.children!);
|
||||||
}
|
}
|
||||||
@ -80,7 +69,7 @@ const childMenus = computed(() => {
|
|||||||
return children;
|
return children;
|
||||||
});
|
});
|
||||||
|
|
||||||
const showDrawer = computed(() => (childMenuVisible.value && childMenus.value.length) || app.menu.fixedMix);
|
const showDrawer = computed(() => (props.visible && childMenus.value.length) || app.menu.fixedMix);
|
||||||
|
|
||||||
const activeKey = computed(() => route.name as string);
|
const activeKey = computed(() => route.name as string);
|
||||||
|
|
||||||
@ -93,17 +82,6 @@ const headerHeight = computed(() => {
|
|||||||
const { height } = theme.headerStyle;
|
const { height } = theme.headerStyle;
|
||||||
return `${height}px`;
|
return `${height}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMouseEvent(type: 'enter' | 'leave') {
|
|
||||||
if (type === 'enter') {
|
|
||||||
showChildMenu();
|
|
||||||
setMouseEnterChildMenu();
|
|
||||||
} else {
|
|
||||||
hideChildMenu();
|
|
||||||
setMouseLeaveChildMenu();
|
|
||||||
setHoverRouteName(props.activeRouteName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.drawer-shadow {
|
.drawer-shadow {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full bg-white dark:bg-[#18181c]">
|
<div class="flex h-full bg-white dark:bg-[#18181c]" @mouseleave="handleMouseLeaveMenu">
|
||||||
<div
|
<div
|
||||||
class="flex-col-stretch flex-1 h-full transition-width duration-300 ease-in-out"
|
class="flex-col-stretch flex-1 h-full transition-width duration-300 ease-in-out"
|
||||||
:class="[app.menu.collapsed ? 'mix-menu-collapsed-width' : 'mix-menu-width']"
|
:class="[app.menu.collapsed ? 'mix-menu-collapsed-width' : 'mix-menu-width']"
|
||||||
@ -15,6 +15,7 @@
|
|||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
:active-route-name="activeParentRouteName"
|
:active-route-name="activeParentRouteName"
|
||||||
:is-mini="app.menu.collapsed"
|
:is-mini="app.menu.collapsed"
|
||||||
|
@click="handleMixMenu(item.routeName)"
|
||||||
/>
|
/>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
@ -24,26 +25,26 @@
|
|||||||
class="relative h-full transition-width duration-300 ease-in-out"
|
class="relative h-full transition-width duration-300 ease-in-out"
|
||||||
:style="{ width: app.menu.fixedMix ? theme.menuStyle.width + 'px' : '0px' }"
|
:style="{ width: app.menu.fixedMix ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
>
|
>
|
||||||
<mix-menu-drawer :active-route-name="activeParentRouteName" />
|
<mix-menu-drawer :visible="drawerVisible" :active-route-name="activeParentRouteName" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import type { VNodeChild } from 'vue';
|
import type { VNodeChild } from 'vue';
|
||||||
import { NScrollbar } from 'naive-ui';
|
import { NScrollbar } from 'naive-ui';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useAppStore, useThemeStore } from '@/store';
|
import { useAppStore, useThemeStore } from '@/store';
|
||||||
import { useVerticalMixSiderContext } from '@/context';
|
|
||||||
import { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
|
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
|
||||||
import { GlobalLogo } from '../../../common';
|
import { GlobalLogo } from '../../../common';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { useVerticalMixSiderProvide } = useVerticalMixSiderContext();
|
const { bool: drawerVisible, setTrue: openDrawer, setFalse: hideDrawer } = useBoolean();
|
||||||
|
|
||||||
const mixMenuWidth = computed(() => `${theme.menuStyle.mixWidth}px`);
|
const mixMenuWidth = computed(() => `${theme.menuStyle.mixWidth}px`);
|
||||||
const mixMenuCollapsedWidth = computed(() => `${theme.menuStyle.mixCollapsedWidth}px`);
|
const mixMenuCollapsedWidth = computed(() => `${theme.menuStyle.mixCollapsedWidth}px`);
|
||||||
@ -52,7 +53,6 @@ const firstDegreeMenus = menus.map(item => {
|
|||||||
const { routeName } = item;
|
const { routeName } = item;
|
||||||
const label = item.label as string;
|
const label = item.label as string;
|
||||||
const icon = item.icon! as () => VNodeChild;
|
const icon = item.icon! as () => VNodeChild;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routeName,
|
routeName,
|
||||||
label,
|
label,
|
||||||
@ -60,16 +60,26 @@ const firstDegreeMenus = menus.map(item => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeParentRouteName = computed(() => {
|
const activeParentRouteName = ref(getActiveRouteName());
|
||||||
|
|
||||||
|
function getActiveRouteName() {
|
||||||
let name = '';
|
let name = '';
|
||||||
const { matched } = route;
|
const { matched } = route;
|
||||||
if (matched.length) {
|
if (matched.length) {
|
||||||
name = matched[0].name as string;
|
name = matched[0].name as string;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
});
|
}
|
||||||
|
|
||||||
useVerticalMixSiderProvide();
|
function handleMixMenu(routeName: string) {
|
||||||
|
activeParentRouteName.value = routeName;
|
||||||
|
openDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeaveMenu() {
|
||||||
|
activeParentRouteName.value = getActiveRouteName();
|
||||||
|
hideDrawer();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mix-menu-width {
|
.mix-menu-width {
|
||||||
|
6
src/layouts/MultiMenuLayout/index.vue
Normal file
6
src/layouts/MultiMenuLayout/index.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<style scoped></style>
|
@ -1,4 +1,5 @@
|
|||||||
import BasicLayout from './BasicLayout/index.vue';
|
import BasicLayout from './BasicLayout/index.vue';
|
||||||
import BlankLayout from './BlankLayout/index.vue';
|
import BlankLayout from './BlankLayout/index.vue';
|
||||||
|
import MultiMenuLayout from './MultiMenuLayout/index.vue';
|
||||||
|
|
||||||
export { BasicLayout, BlankLayout };
|
export { BasicLayout, BlankLayout, MultiMenuLayout };
|
||||||
|
@ -5,6 +5,7 @@ import NotFound from '@/views/system/exception/404.vue';
|
|||||||
import ServiceError from '@/views/system/exception/500.vue';
|
import ServiceError from '@/views/system/exception/500.vue';
|
||||||
import DashboardAnalysis from '@/views/dashboard/analysis/index.vue';
|
import DashboardAnalysis from '@/views/dashboard/analysis/index.vue';
|
||||||
import DashboardWorkbench from '@/views/dashboard/workbench/index.vue';
|
import DashboardWorkbench from '@/views/dashboard/workbench/index.vue';
|
||||||
|
import MultimenuFirstSecond from '@/views/multimenu/first/second/index.vue';
|
||||||
|
|
||||||
const Exception403 = { ...NoPermission };
|
const Exception403 = { ...NoPermission };
|
||||||
const Exception404 = { ...NotFound };
|
const Exception404 = { ...NotFound };
|
||||||
@ -18,6 +19,7 @@ setCacheName(NotFound, RouteNameMap.get('not-found'));
|
|||||||
setCacheName(ServiceError, RouteNameMap.get('service-error'));
|
setCacheName(ServiceError, RouteNameMap.get('service-error'));
|
||||||
setCacheName(DashboardAnalysis, RouteNameMap.get('dashboard-analysis'));
|
setCacheName(DashboardAnalysis, RouteNameMap.get('dashboard-analysis'));
|
||||||
setCacheName(DashboardWorkbench, RouteNameMap.get('dashboard-workbench'));
|
setCacheName(DashboardWorkbench, RouteNameMap.get('dashboard-workbench'));
|
||||||
|
setCacheName(MultimenuFirstSecond, RouteNameMap.get('multimenu-first-second'));
|
||||||
setCacheName(Exception404, RouteNameMap.get('exception-404'));
|
setCacheName(Exception404, RouteNameMap.get('exception-404'));
|
||||||
setCacheName(Exception403, RouteNameMap.get('exception-403'));
|
setCacheName(Exception403, RouteNameMap.get('exception-403'));
|
||||||
setCacheName(Exception500, RouteNameMap.get('exception-500'));
|
setCacheName(Exception500, RouteNameMap.get('exception-500'));
|
||||||
@ -31,5 +33,6 @@ export {
|
|||||||
DashboardWorkbench,
|
DashboardWorkbench,
|
||||||
Exception403,
|
Exception403,
|
||||||
Exception404,
|
Exception404,
|
||||||
Exception500
|
Exception500,
|
||||||
|
MultimenuFirstSecond
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
import { Dashboard } from '@vicons/carbon';
|
import { Dashboard, Menu } from '@vicons/carbon';
|
||||||
import { ExceptionOutlined } from '@vicons/antd';
|
import { ExceptionOutlined } from '@vicons/antd';
|
||||||
import { BasicLayout, BlankLayout } from '@/layouts';
|
import { BasicLayout, BlankLayout, MultiMenuLayout } from '@/layouts';
|
||||||
import { EnumRoutePath, EnumRouteTitle } from '@/enum';
|
import { EnumRoutePath, EnumRouteTitle } from '@/enum';
|
||||||
import type { CustomRoute, LoginModuleType } from '@/interface';
|
import type { CustomRoute, LoginModuleType } from '@/interface';
|
||||||
import { getLoginModuleRegExp } from '@/utils';
|
import { getLoginModuleRegExp } from '@/utils';
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
ServiceError,
|
ServiceError,
|
||||||
DashboardAnalysis,
|
DashboardAnalysis,
|
||||||
DashboardWorkbench,
|
DashboardWorkbench,
|
||||||
|
MultimenuFirstSecond,
|
||||||
Exception403,
|
Exception403,
|
||||||
Exception404,
|
Exception404,
|
||||||
Exception500
|
Exception500
|
||||||
@ -184,6 +185,40 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: RouteNameMap.get('multimenu'),
|
||||||
|
path: EnumRoutePath.multimenu,
|
||||||
|
component: MultiMenuLayout,
|
||||||
|
redirect: { name: RouteNameMap.get('multimenu-first') },
|
||||||
|
meta: {
|
||||||
|
title: EnumRouteTitle.multimenu,
|
||||||
|
icon: Menu
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: RouteNameMap.get('multimenu-first'),
|
||||||
|
path: EnumRoutePath['multimenu-first'],
|
||||||
|
component: BasicLayout,
|
||||||
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
|
requiresAuth: true,
|
||||||
|
title: EnumRouteTitle['multimenu-first']
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: RouteNameMap.get('multimenu-first-second'),
|
||||||
|
path: EnumRoutePath['multimenu-first-second'],
|
||||||
|
component: MultimenuFirstSecond,
|
||||||
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
|
requiresAuth: true,
|
||||||
|
title: EnumRouteTitle['multimenu-first-second']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
6
src/views/multimenu/first/second/index.vue
Normal file
6
src/views/multimenu/first/second/index.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div>多级菜单-二级菜单</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<style scoped></style>
|
Loading…
Reference in New Issue
Block a user