mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat(projects): vertical-mix的导航模式的二级菜单显示
This commit is contained in:
@ -3,11 +3,7 @@
|
|||||||
class="relative flex-center h-30px pl-14px border-1px rounded-2px cursor-pointer"
|
class="relative flex-center h-30px pl-14px border-1px rounded-2px cursor-pointer"
|
||||||
:class="[
|
:class="[
|
||||||
closable ? 'pr-6px' : 'pr-14px',
|
closable ? 'pr-6px' : 'pr-14px',
|
||||||
active || isHover
|
active || isHover ? 'text-primary border-primary border-opacity-30 ' : 'border-[#e5e7eb] dark:border-[#ffffff3d]',
|
||||||
? 'text-primary border-primary border-opacity-30'
|
|
||||||
: darkMode
|
|
||||||
? 'border-[#ffffff3d]'
|
|
||||||
: 'border-[#e5e7eb]',
|
|
||||||
{ 'bg-primary bg-opacity-10': active }
|
{ 'bg-primary bg-opacity-10': active }
|
||||||
]"
|
]"
|
||||||
@mouseenter="setTrue"
|
@mouseenter="setTrue"
|
||||||
@ -34,10 +30,6 @@ defineProps({
|
|||||||
closable: {
|
closable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
|
||||||
darkMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex-col-stretch p-10px"
|
class="flex-1 flex-col-stretch p-10px bg-[#f5f7f9] dark:bg-black"
|
||||||
:class="{ 'bg-[#f5f7f9]': !theme.darkMode, 'overflow-hidden': routeProps.fullPage }"
|
:class="{ 'overflow-hidden': routeProps.fullPage }"
|
||||||
>
|
>
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<transition :name="theme.pageStyle.animateType" mode="out-in" appear>
|
<transition :name="theme.pageStyle.animateType" mode="out-in" appear>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout-footer>
|
<n-layout-footer>
|
||||||
<div class="flex-center h-48px text-[#00000073]">Copyright ©2021 Soybean Admin</div>
|
<div class="flex-center h-48px text-[#333639] dark:text-[#ffffffd1]">Copyright ©2021 Soybean Admin</div>
|
||||||
</n-layout-footer>
|
</n-layout-footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout-sider
|
<n-layout-sider
|
||||||
:style="{ zIndex }"
|
:style="{ zIndex }"
|
||||||
:native-scrollbar="false"
|
|
||||||
:inverted="inverted"
|
:inverted="inverted"
|
||||||
collapse-mode="width"
|
collapse-mode="width"
|
||||||
:collapsed="app.menu.collapsed"
|
:collapsed="app.menu.collapsed"
|
||||||
@ -10,7 +9,9 @@
|
|||||||
@collapse="handleMenuCollapse(true)"
|
@collapse="handleMenuCollapse(true)"
|
||||||
@expand="handleMenuCollapse(false)"
|
@expand="handleMenuCollapse(false)"
|
||||||
>
|
>
|
||||||
|
<div class="flex-col-stretch h-full">
|
||||||
<global-logo v-if="theme.isVerticalNav" />
|
<global-logo v-if="theme.isVerticalNav" />
|
||||||
|
<n-scrollbar class="flex-1-hidden">
|
||||||
<n-menu
|
<n-menu
|
||||||
:value="activeKey"
|
:value="activeKey"
|
||||||
:collapsed="app.menu.collapsed"
|
:collapsed="app.menu.collapsed"
|
||||||
@ -19,13 +20,15 @@
|
|||||||
:options="menus"
|
:options="menus"
|
||||||
@update:value="handleUpdateMenu"
|
@update:value="handleUpdateMenu"
|
||||||
/>
|
/>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { NMenu, NLayoutSider } from 'naive-ui';
|
import { NLayoutSider, 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 { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
|
@ -1,19 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-6px px-4px cursor-pointer">
|
<div
|
||||||
|
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 }"
|
:class="{ 'text-primary bg-primary bg-opacity-10': isActive, 'text-primary': isHover }"
|
||||||
>
|
>
|
||||||
<component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" />
|
<component :is="icon" :class="[isMini ? 'text-16px' : 'text-20px']" />
|
||||||
<p v-show="!isMini" class="pt-8px text-12px">{{ label }}</p>
|
<p
|
||||||
|
class="pt-8px text-12px overflow-hidden transition-height duration-200 ease-in-out"
|
||||||
|
:class="[isMini ? 'h-0 pt-0' : 'h-20px pt-8px']"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
import type { PropType, VNodeChild } from 'vue';
|
import type { PropType, VNodeChild } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useBoolean } from '@/hooks';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
routeName: {
|
routeName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@ -33,7 +46,44 @@ defineProps({
|
|||||||
isMini: {
|
isMini: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
hoverRoute: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:hoverRoute']);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { bool: isHover, setTrue, setFalse } = useBoolean();
|
||||||
|
|
||||||
|
const hoverRouteName = ref(props.hoverRoute);
|
||||||
|
function setHoverRouteName(name: string) {
|
||||||
|
hoverRouteName.value = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRouter() {
|
||||||
|
router.push({ name: props.routeName });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseEvent(type: 'enter' | 'leave') {
|
||||||
|
if (type === 'enter') {
|
||||||
|
setTrue();
|
||||||
|
setHoverRouteName(props.routeName);
|
||||||
|
} else {
|
||||||
|
setFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.hoverRoute,
|
||||||
|
newValue => {
|
||||||
|
setHoverRouteName(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(hoverRouteName, newValue => {
|
||||||
|
emit('update:hoverRoute', newValue);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-center h-36px border-t-1px border-[#eee] cursor-pointer" @click="toggleMenu">
|
<div class="flex-center h-36px text-[#333639] dark:text-[#ffffffd1] cursor-pointer" @click="toggleMenu">
|
||||||
<icon-ph-caret-double-right-bold v-if="app.menu.collapsed" class="text-16px" />
|
<icon-ph-caret-double-right-bold v-if="app.menu.collapsed" class="text-16px" />
|
||||||
<icon-ph:caret-double-left-bold v-else class="text-16px" />
|
<icon-ph:caret-double-left-bold v-else class="text-16px" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
drawer-shadow
|
||||||
|
absolute-lt
|
||||||
|
flex-col-stretch
|
||||||
|
h-full
|
||||||
|
overflow-hidden
|
||||||
|
transition-width
|
||||||
|
duration-300
|
||||||
|
ease-in-out
|
||||||
|
bg-white
|
||||||
|
dark:bg-[#18181c]
|
||||||
|
"
|
||||||
|
:style="{ width: showDrawer ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
|
@mouseleave="handleResetHoverRoute"
|
||||||
|
>
|
||||||
|
<header class="header-height flex-y-center justify-between">
|
||||||
|
<h2 class="pl-8px text-16px text-primary font-bold">{{ title }}</h2>
|
||||||
|
|
||||||
|
<div class="px-8px text-16px cursor-pointer" @click="toggleFixedMixMenu">
|
||||||
|
<icon-mdi:pin-off v-if="app.menu.fixedMix" />
|
||||||
|
<icon-mdi:pin v-else />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="flex-1-hidden">
|
||||||
|
<n-scrollbar>
|
||||||
|
<n-menu :value="activeKey" :options="childMenus" @update:value="handleUpdateMenu" />
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { NScrollbar, NMenu } from 'naive-ui';
|
||||||
|
import type { MenuOption } from 'naive-ui';
|
||||||
|
import { useThemeStore, useAppStore } from '@/store';
|
||||||
|
import { useAppTitle } from '@/hooks';
|
||||||
|
import { menus } from '@/router';
|
||||||
|
import type { GlobalMenuOption } from '@/interface';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
hoverRoute: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['reset-hover-route']);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const theme = useThemeStore();
|
||||||
|
const app = useAppStore();
|
||||||
|
const { toggleFixedMixMenu } = useAppStore();
|
||||||
|
const title = useAppTitle();
|
||||||
|
|
||||||
|
const childMenus = computed(() => {
|
||||||
|
const children: MenuOption[] = [];
|
||||||
|
menus.some(item => {
|
||||||
|
const flag = item.routeName === props.hoverRoute && Boolean(item.children?.length);
|
||||||
|
if (flag) {
|
||||||
|
children.push(...item.children!);
|
||||||
|
}
|
||||||
|
return flag;
|
||||||
|
});
|
||||||
|
return children;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDrawer = computed(() => childMenus.value.length || app.menu.fixedMix);
|
||||||
|
|
||||||
|
const activeKey = computed(() => getActiveKey());
|
||||||
|
|
||||||
|
function getActiveKey() {
|
||||||
|
return route.name as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResetHoverRoute() {
|
||||||
|
emit('reset-hover-route');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateMenu(key: string, item: MenuOption) {
|
||||||
|
const menuItem = item as GlobalMenuOption;
|
||||||
|
router.push(menuItem.routePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerHeight = computed(() => {
|
||||||
|
const { height } = theme.headerStyle;
|
||||||
|
return `${height}px`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.drawer-shadow {
|
||||||
|
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
|
||||||
|
}
|
||||||
|
.header-height {
|
||||||
|
height: v-bind(headerHeight);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,4 +1,5 @@
|
|||||||
import MixMenu from './MixMenu.vue';
|
import MixMenu from './MixMenu.vue';
|
||||||
import MixMenuCollapse from './MixMenuCollapse.vue';
|
import MixMenuCollapse from './MixMenuCollapse.vue';
|
||||||
|
import MixMenuDrawer from './MixMenuDrawer.vue';
|
||||||
|
|
||||||
export { MixMenu, MixMenuCollapse };
|
export { MixMenu, MixMenuCollapse, MixMenuDrawer };
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="flex h-full bg-white dark:bg-[#18181c]">
|
||||||
<div
|
<div
|
||||||
class="h-full transition-width duration-500 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']"
|
||||||
>
|
>
|
||||||
<div class="flex-col-stretch h-full">
|
|
||||||
<global-logo />
|
<global-logo />
|
||||||
<div class="flex-1-hidden">
|
<div class="flex-1-hidden">
|
||||||
<n-scrollbar>
|
<n-scrollbar>
|
||||||
<mix-menu
|
<mix-menu
|
||||||
v-for="item in firstDegreeMenus"
|
v-for="item in firstDegreeMenus"
|
||||||
:key="item.routeName"
|
:key="item.routeName"
|
||||||
|
v-model:hover-route="hoverRoute"
|
||||||
:route-name="item.routeName"
|
:route-name="item.routeName"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
@ -20,17 +21,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<mix-menu-collapse />
|
<mix-menu-collapse />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative h-full transition-width duration-300 ease-in-out"
|
||||||
|
:style="{ width: app.menu.fixedMix ? theme.menuStyle.width + 'px' : '0px' }"
|
||||||
|
>
|
||||||
|
<mix-menu-drawer :hover-route="hoverRoute" @reset-hover-route="resetHoverRoute" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } 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 { menus } from '@/router';
|
import { menus } from '@/router';
|
||||||
import { MixMenu, MixMenuCollapse } from './components';
|
import { MixMenu, MixMenuCollapse, MixMenuDrawer } from './components';
|
||||||
import { GlobalLogo } from '../../../common';
|
import { GlobalLogo } from '../../../common';
|
||||||
|
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
@ -60,6 +67,11 @@ const activeParentRouteName = computed(() => {
|
|||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hoverRoute = ref('');
|
||||||
|
function resetHoverRoute() {
|
||||||
|
hoverRoute.value = '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mix-menu-width {
|
.mix-menu-width {
|
||||||
|
@ -146,6 +146,7 @@ export const customRoutes: CustomRoute[] = [
|
|||||||
name: RouteNameMap.get('exception'),
|
name: RouteNameMap.get('exception'),
|
||||||
path: EnumRoutePath.exception,
|
path: EnumRoutePath.exception,
|
||||||
component: BasicLayout,
|
component: BasicLayout,
|
||||||
|
redirect: { name: RouteNameMap.get('exception-403') },
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: EnumRouteTitle.exception,
|
title: EnumRouteTitle.exception,
|
||||||
|
@ -17,6 +17,8 @@ interface AppState {
|
|||||||
interface MenuState {
|
interface MenuState {
|
||||||
/** 菜单折叠 */
|
/** 菜单折叠 */
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
/** 混合菜单vertical-mix是否固定二级菜单 */
|
||||||
|
fixedMix: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MultiTabRoute = Partial<RouteLocationNormalizedLoaded> & {
|
type MultiTabRoute = Partial<RouteLocationNormalizedLoaded> & {
|
||||||
@ -40,7 +42,8 @@ const appStore = defineStore({
|
|||||||
id: 'app-store',
|
id: 'app-store',
|
||||||
state: (): AppState => ({
|
state: (): AppState => ({
|
||||||
menu: {
|
menu: {
|
||||||
collapsed: false
|
collapsed: false,
|
||||||
|
fixedMix: false
|
||||||
},
|
},
|
||||||
multiTab: {
|
multiTab: {
|
||||||
routes: [],
|
routes: [],
|
||||||
@ -62,6 +65,10 @@ const appStore = defineStore({
|
|||||||
handleMenuCollapse(collapsed: boolean) {
|
handleMenuCollapse(collapsed: boolean) {
|
||||||
this.menu.collapsed = collapsed;
|
this.menu.collapsed = collapsed;
|
||||||
},
|
},
|
||||||
|
/** 设置混合菜单是否固定 */
|
||||||
|
toggleFixedMixMenu() {
|
||||||
|
this.menu.fixedMix = !this.menu.fixedMix;
|
||||||
|
},
|
||||||
/** 切换折叠/展开菜单 */
|
/** 切换折叠/展开菜单 */
|
||||||
toggleMenu() {
|
toggleMenu() {
|
||||||
this.menu.collapsed = !this.menu.collapsed;
|
this.menu.collapsed = !this.menu.collapsed;
|
||||||
|
Reference in New Issue
Block a user