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