feat: 支持双列布局次级菜单折叠及三档响应式断点
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="layout-root" :class="layoutClass">
|
||||
<div v-if="isMobile && isSidebarOpen" class="layout-root__overlay" @click="closeSidebar" />
|
||||
<div
|
||||
v-if="showOverlay && isMobile && isSidebarOpen"
|
||||
class="layout-root__overlay"
|
||||
@click="closeSidebar"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
@@ -9,6 +13,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useLayout } from "./composables/useLayout";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** 移动端展开侧边栏时是否显示遮罩层(LeftLayout 需要,MixLayout 不需要) */
|
||||
showOverlay?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showOverlay: true,
|
||||
}
|
||||
);
|
||||
|
||||
const { layoutClass, isSidebarOpen, isMobile, closeSidebar } = useLayout();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout-navbar">
|
||||
<div class="flex-y-center">
|
||||
<Hamburger :is-active="isSidebarOpened" @toggle-click="toggleSideBar" />
|
||||
<Hamburger :is-active="sidebarState.opened" @toggle-click="sidebarState.toggle" />
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
|
||||
@@ -16,13 +16,25 @@ import { useAppStore } from "@/stores";
|
||||
import Hamburger from "@/components/Hamburger/index.vue";
|
||||
import Breadcrumb from "@/components/Breadcrumb/index.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
toggleTarget?: "primary" | "secondary";
|
||||
}>(),
|
||||
{
|
||||
toggleTarget: "primary",
|
||||
}
|
||||
);
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const isSidebarOpened = computed(() => appStore.sidebar.opened);
|
||||
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSidebar();
|
||||
}
|
||||
const sidebarState = computed(() =>
|
||||
props.toggleTarget === "secondary"
|
||||
? {
|
||||
opened: appStore.secondarySidebar?.opened ?? true,
|
||||
toggle: () => appStore.toggleSecondarySidebar(),
|
||||
}
|
||||
: { opened: appStore.sidebar.opened, toggle: () => appStore.toggleSidebar() }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-menu
|
||||
ref="menuRef"
|
||||
:default-active="activeMenuPath"
|
||||
:collapse="props.alwaysExpand ? false : !appStore.sidebar.opened"
|
||||
:collapse="props.collapseOverride ?? (props.alwaysExpand ? false : !appStore.sidebar.opened)"
|
||||
:background-color="menuThemeProps.backgroundColor"
|
||||
:text-color="menuThemeProps.textColor"
|
||||
:active-text-color="menuThemeProps.activeTextColor"
|
||||
@@ -52,6 +52,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 显式覆盖 collapse 状态,优先级高于 alwaysExpand 和全局 sidebar.opened */
|
||||
collapseOverride: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const menuRef = ref<MenuInstance>();
|
||||
|
||||
@@ -2,23 +2,30 @@ import { useWindowSize } from "@vueuse/core";
|
||||
import { useAppStore } from "@/stores";
|
||||
import { DeviceEnum } from "@/enums/settings";
|
||||
|
||||
const DESKTOP_BREAKPOINT = 992;
|
||||
const WIDE_BREAKPOINT = 992;
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
/**
|
||||
* 根据窗口宽度同步设备类型和侧边栏展开状态
|
||||
*
|
||||
* 三档断点:
|
||||
* - ≥992px:桌面宽屏,侧边栏展开(完整菜单)
|
||||
* - 768~992px:桌面窄屏,侧边栏收缩(仅图标)
|
||||
* - <768px:移动端,侧边栏完全隐藏
|
||||
*/
|
||||
export function useLayoutDevice() {
|
||||
const appStore = useAppStore();
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT);
|
||||
const isDesktop = computed(() => width.value >= MOBILE_BREAKPOINT);
|
||||
const isWideDesktop = computed(() => width.value >= WIDE_BREAKPOINT);
|
||||
|
||||
watchEffect(() => {
|
||||
const device = isDesktop.value ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE;
|
||||
|
||||
appStore.toggleDevice(device);
|
||||
|
||||
if (isDesktop.value) {
|
||||
if (isWideDesktop.value) {
|
||||
appStore.openSidebar();
|
||||
} else {
|
||||
appStore.closeSidebar();
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<aside
|
||||
v-show="!appStore.contentFullscreen"
|
||||
class="layout-double"
|
||||
:class="{ 'is-collapsed': !isSidebarOpen }"
|
||||
:class="{
|
||||
'is-collapsed': !isSidebarOpen,
|
||||
'is-secondary-collapsed': !secondaryExpanded,
|
||||
}"
|
||||
>
|
||||
<div class="layout-double__primary">
|
||||
<LayoutLogo v-if="showLogo" :collapse="true" />
|
||||
@@ -33,7 +36,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="layout-double__secondary">
|
||||
<div class="layout-double__secondary" :class="{ 'is-collapsed': !secondaryExpanded }">
|
||||
<div v-if="showLogo" class="layout-double__title">
|
||||
{{ appConfig.title }}
|
||||
</div>
|
||||
@@ -42,7 +45,7 @@
|
||||
<LayoutSidebar
|
||||
:data="sideMenuRoutes"
|
||||
:base-path="activeTopMenuPath"
|
||||
:always-expand="true"
|
||||
:collapse-override="!secondaryExpanded"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
@@ -52,10 +55,11 @@
|
||||
class="layout-main"
|
||||
:class="{
|
||||
'is-collapsed': !isSidebarOpen,
|
||||
'is-secondary-collapsed': !secondaryExpanded,
|
||||
'is-fullscreen': appStore.contentFullscreen,
|
||||
}"
|
||||
>
|
||||
<LayoutNavbar v-show="!appStore.contentFullscreen" />
|
||||
<LayoutNavbar v-show="!appStore.contentFullscreen" toggle-target="secondary" />
|
||||
<LayoutTagsView v-if="showTagsView" />
|
||||
<LayoutMain />
|
||||
</main>
|
||||
@@ -79,6 +83,8 @@ import LayoutMenuIcon from "../components/LayoutMenuIcon.vue";
|
||||
const appStore = useAppStore();
|
||||
const { showTagsView, showLogo, isSidebarOpen, toggleSidebar } = useLayout();
|
||||
|
||||
const secondaryExpanded = computed(() => appStore.secondarySidebar?.opened ?? true);
|
||||
|
||||
const { topMenuItems, activeTopMenuPath, sideMenuRoutes, handleTopMenuSelect } = useMixMenu();
|
||||
</script>
|
||||
|
||||
@@ -115,13 +121,24 @@ $double-sidebar-width: $sidebar-primary-width + $sidebar-secondary-width;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-secondary-collapsed {
|
||||
width: $sidebar-primary-width + $sidebar-width-collapsed;
|
||||
|
||||
.layout-double__secondary {
|
||||
flex: 0 0 $sidebar-secondary-width;
|
||||
width: $sidebar-secondary-width;
|
||||
min-width: $sidebar-secondary-width;
|
||||
border-right: 1px solid var(--menu-border);
|
||||
flex: 0 0 $sidebar-width-collapsed;
|
||||
width: $sidebar-width-collapsed;
|
||||
min-width: $sidebar-width-collapsed;
|
||||
}
|
||||
|
||||
.layout-double__title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapsed.is-secondary-collapsed {
|
||||
width: $sidebar-width-collapsed * 2;
|
||||
}
|
||||
|
||||
&__primary {
|
||||
@@ -263,6 +280,21 @@ $double-sidebar-width: $sidebar-primary-width + $sidebar-secondary-width;
|
||||
:deep(.el-sub-menu__icon-arrow) {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
width: $sidebar-width-collapsed;
|
||||
|
||||
:deep(.el-menu--collapse) {
|
||||
.el-menu-item span,
|
||||
.el-sub-menu__title span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__secondary-scroll {
|
||||
@@ -294,6 +326,14 @@ $double-sidebar-width: $sidebar-primary-width + $sidebar-secondary-width;
|
||||
margin-left: $sidebar-width-collapsed + $sidebar-secondary-width;
|
||||
}
|
||||
|
||||
&.is-secondary-collapsed {
|
||||
margin-left: $sidebar-primary-width + $sidebar-width-collapsed;
|
||||
|
||||
&.is-collapsed {
|
||||
margin-left: $sidebar-width-collapsed + $sidebar-width-collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-fullscreen {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<BaseLayout :show-overlay="false">
|
||||
<div v-show="!appStore.contentFullscreen" class="layout-header">
|
||||
<div class="layout-header__content">
|
||||
<div v-if="showLogo" class="layout-header__logo">
|
||||
@@ -240,10 +240,20 @@ const useMenuColors = computed(
|
||||
|
||||
&.is-collapsed {
|
||||
width: $sidebar-width-collapsed !important;
|
||||
min-width: 0; // 覆盖 flex 子元素默认 min-width: auto,防止 .el-menu--collapse 64px 推挤容器
|
||||
overflow: hidden; // 裁剪 .el-menu--collapse 溢出,防止其可见溢出推挤 .layout-container 宽度
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar) {
|
||||
@include sidebar-scroll-height-with-toggle;
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu) {
|
||||
@@ -286,26 +296,6 @@ const useMenuColors = computed(
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.is-mobile) {
|
||||
.layout-container {
|
||||
.layout-sidebar {
|
||||
position: fixed;
|
||||
top: $navbar-height;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
transition: transform 0.28s;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-sidebar-collapsed {
|
||||
.layout-sidebar {
|
||||
width: $sidebar-width !important;
|
||||
transform: translateX(-$sidebar-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.dark),
|
||||
:global(html.sidebar-color-blue) {
|
||||
.layout-header__menu {
|
||||
|
||||
@@ -34,6 +34,13 @@ export const useAppStore = defineStore("app", () => {
|
||||
withoutAnimation: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 双列布局第二列(次级菜单)展开状态
|
||||
*/
|
||||
const secondarySidebar = reactive({
|
||||
opened: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 当前激活的顶部菜单路径
|
||||
*/
|
||||
@@ -73,6 +80,13 @@ export const useAppStore = defineStore("app", () => {
|
||||
sidebarStatus.value = SidebarStatus.OPENED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换第二列(次级菜单)展开状态
|
||||
*/
|
||||
function toggleSecondarySidebar() {
|
||||
secondarySidebar.opened = !secondarySidebar.opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换设备类型
|
||||
*/
|
||||
@@ -111,6 +125,7 @@ export const useAppStore = defineStore("app", () => {
|
||||
return {
|
||||
device,
|
||||
sidebar,
|
||||
secondarySidebar,
|
||||
language,
|
||||
locale,
|
||||
size,
|
||||
@@ -121,6 +136,7 @@ export const useAppStore = defineStore("app", () => {
|
||||
toggleSidebar,
|
||||
closeSidebar,
|
||||
openSidebar,
|
||||
toggleSecondarySidebar,
|
||||
setActiveTopMenuPath,
|
||||
toggleContentFullscreen,
|
||||
activeTopMenuPath,
|
||||
|
||||
@@ -124,7 +124,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .layout-root,
|
||||
/* 隐藏侧边栏 el-scrollbar 的水平滚动条(el-scrollbar 的 wrap 宽度因滚动条 gutter 比内容少 1px,导致 .el-menu--collapse 始终溢出) */
|
||||
.layout-sidebar .el-scrollbar__bar.is-horizontal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html.sidebar-color-blue .layout-root,
|
||||
html.dark .el-menu--popup,
|
||||
html.sidebar-color-blue .el-menu--popup {
|
||||
|
||||
Reference in New Issue
Block a user