wip: 临时提交

This commit is contained in:
Ray.Hao
2025-05-23 09:51:13 +08:00
parent 54b2164d25
commit af30411317
14 changed files with 809 additions and 308 deletions

View File

@@ -1,306 +0,0 @@
<template>
<div class="layout" :class="layoutClass">
<!-- 移动端遮罩层 -->
<div v-if="isMobile && isSidebarOpen" class="layout__overlay" @click="handleCloseSidebar" />
<!-- 侧边栏 -->
<Sidebar class="layout__sidebar" />
<!-- 混合布局 -->
<div v-if="layout === LayoutMode.MIX" class="layout__container">
<!-- 左侧菜单栏 -->
<div class="layout__sidebar--left">
<el-scrollbar>
<SidebarMenu :data="sideMenuRoutes" :base-path="activeTopMenuPath" />
</el-scrollbar>
<!-- 侧边栏切换按钮 -->
<div class="layout__sidebar-toggle">
<Hamburger :is-active="appStore.sidebar.opened" @toggle-click="handleToggleSidebar" />
</div>
</div>
<!-- 主内容区域 -->
<div :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<TagsView v-if="isShowTagsView" />
<AppMain />
<Settings v-if="defaultSettings.showSettings" />
<!-- 返回顶部按钮 -->
<el-backtop target=".app-main">
<div class="i-svg:backtop w-6 h-6" />
</el-backtop>
</div>
</div>
<!-- 左侧或顶部布局的主内容区 -->
<div v-else :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<NavBar v-if="layout === LayoutMode.LEFT" />
<TagsView v-if="isShowTagsView" />
<AppMain />
<Settings v-if="defaultSettings.showSettings" />
<!-- 返回顶部按钮 -->
<el-backtop target=".app-main">
<div class="i-svg:backtop w-6 h-6" />
</el-backtop>
</div>
</div>
</template>
<script setup lang="ts">
// 状态管理
import { useAppStore, useSettingsStore, usePermissionStore } from "@/store";
// 配置
import defaultSettings from "@/settings";
// 枚举
import { DeviceEnum } from "@/enums/settings/device.enum";
import { LayoutMode } from "@/enums/settings/layout.enum";
// 组件
import NavBar from "./components/NavBar/index.vue";
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const width = useWindowSize().width;
// 常量
const WIDTH_DESKTOP = 992; // 响应式布局容器固定宽度(大屏 >=1200px中屏 >=992px小屏 >=768px
// 计算属性
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE); // 是否为移动设备
const isSidebarOpen = computed(() => appStore.sidebar.opened); // 侧边栏是否展开
const isShowTagsView = computed(() => settingsStore.tagsView); // 是否显示标签视图
const layout = computed(() => settingsStore.layout); // 当前布局模式left、top、mix
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 顶部菜单激活路径
const sideMenuRoutes = computed(() => permissionStore.sideMenuRoutes); // 混合布局左侧菜单路由
// 监听顶部菜单激活路径变化,更新混合布局左侧菜单路由
watch(
() => activeTopMenuPath.value,
(newVal: string) => {
permissionStore.updateSideMenu(newVal);
},
{ deep: true, immediate: true }
);
// 监听窗口宽度变化,调整设备类型和侧边栏状态
watchEffect(() => {
const isDesktop = width.value >= WIDTH_DESKTOP;
appStore.toggleDevice(isDesktop ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE);
if (isDesktop) {
appStore.openSideBar();
} else {
appStore.closeSideBar();
}
});
// 监听路由变化,如果是移动设备且侧边栏展开,则关闭侧边栏
const route = useRoute();
watch(route, () => {
if (isMobile.value && isSidebarOpen.value) {
appStore.closeSideBar();
}
});
// 计算属性:布局样式
const layoutClass = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
mobile: appStore.device === DeviceEnum.MOBILE,
[`layout-${settingsStore.layout}`]: true,
}));
/**
* 处理遮罩层点击事件,关闭侧边栏
*/
function handleCloseSidebar() {
appStore.closeSideBar();
}
/**
* 处理切换侧边栏的展开/收起状态
*/
function handleToggleSidebar() {
appStore.toggleSidebar();
}
</script>
<style lang="scss" scoped>
.layout {
width: 100%;
height: 100%;
&__overlay {
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
}
&__sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 999;
width: $sidebar-width;
background-color: $menu-background;
transition: width 0.28s;
:deep(.el-menu) {
border: none;
}
}
&__main {
position: relative;
height: 100%;
margin-left: $sidebar-width;
overflow-y: auto;
transition: margin-left 0.28s;
.fixed-header {
position: sticky;
top: 0;
z-index: 9;
transition: width 0.28s;
}
}
}
// 占位符选择器
%layout__sidebar--horizontal {
width: 100% !important;
height: $navbar-height;
:deep(.el-scrollbar) {
flex: 1;
height: $navbar-height;
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title),
:deep(.el-menu--horizontal) {
height: $navbar-height;
line-height: $navbar-height;
}
}
.layout-top {
.layout__sidebar {
position: sticky;
display: flex;
@extend %layout__sidebar--horizontal;
}
.layout__main {
height: calc(100vh - $navbar-height);
margin-left: 0;
}
}
.layout-mix {
.layout__sidebar {
@extend %layout__sidebar--horizontal;
}
.layout__container {
display: flex;
height: 100%;
padding-top: $navbar-height;
.layout__sidebar--left {
position: relative;
width: $sidebar-width;
height: 100%;
background-color: var(--menu-background);
:deep(.el-scrollbar) {
height: calc(100vh - $navbar-height - 50px);
}
:deep(.el-menu) {
height: 100%;
border: none;
}
.layout__sidebar-toggle {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 50px;
line-height: 50px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
}
}
.layout__main {
flex: 1;
min-width: 0;
margin-left: 0;
}
}
}
.hideSidebar {
&.layout-left {
.layout__main {
margin-left: $sidebar-width-collapsed;
}
}
&.layout-top {
.layout__main {
margin-left: 0;
}
}
&.layout-mix {
.layout__sidebar {
width: 100%;
}
.layout__container {
.layout__sidebar--left {
width: $sidebar-width-collapsed;
}
}
}
}
.layout-left {
&.hideSidebar {
.layout__sidebar {
width: $sidebar-width-collapsed;
}
.layout__main {
margin-left: $sidebar-width-collapsed;
}
&.mobile {
.layout__sidebar {
pointer-events: none;
transform: translate3d(-$sidebar-width, 0, 0);
transition-duration: 0.3s;
}
.layout__main {
margin-left: 0;
}
}
}
&.openSidebar {
&.mobile {
.layout__main {
margin-left: 0;
}
}
}
}
</style>

59
src/layouts/README.md Normal file
View File

@@ -0,0 +1,59 @@
# 布局系统
本项目的布局系统采用模块化、可组合式API的架构支持三种不同的布局模式:
1. **左侧菜单布局 (LeftSideLayout)**: 传统的管理系统布局,左侧为菜单栏,顶部为导航栏
2. **顶部菜单布局 (TopMenuLayout)**: 顶部为主菜单栏,适合菜单项较少的应用
3. **混合菜单布局 (MixMenuLayout)**: 顶部为一级菜单,左侧为对应的子菜单,适合菜单层级较多的复杂应用
## 目录结构
```
layouts/
├── README.md # 文档说明
├── index.vue # 布局入口,根据设置选择对应的布局组件
├── composables/ # 可组合式API
│ ├── useLayout.ts # 布局通用逻辑
│ ├── useLayoutResponsive.ts # 响应式布局逻辑
│ └── useLayoutMenu.ts # 菜单处理逻辑
└── components/ # 布局组件
├── LayoutBase.vue # 基础布局组件
├── SidebarMenu.vue # 菜单组件
├── common/ # 公共组件
│ └── LayoutSidebar.vue # 侧边栏公共组件
├── LeftSideLayout/ # 左侧菜单布局
│ └── index.vue
├── TopMenuLayout/ # 顶部菜单布局
│ └── index.vue
└── MixMenuLayout/ # 混合菜单布局
└── index.vue
```
## 主要功能
1. **响应式适配**: 自动适配桌面端和移动端,移动端下自动收起侧边栏
2. **多种布局模式**: 支持左侧菜单、顶部菜单、混合菜单三种模式
3. **主题切换**: 支持明亮/暗黑主题
4. **标签页**: 支持多标签页功能,可通过设置开启/关闭
## 可组合式API
### useLayout
提供布局相关的基础功能:
- 侧边栏展开/收起控制
- 布局模式获取
- 布局样式类计算
### useLayoutResponsive
提供响应式布局功能:
- 根据屏幕尺寸自动调整设备类型
- 根据设备类型自动调整侧边栏状态
### useLayoutMenu
提供菜单相关功能:
- 获取菜单数据
- 处理菜单激活状态
- 混合布局下的菜单联动

View File

@@ -0,0 +1,46 @@
<template>
<div class="layout" :class="layoutClass">
<!-- 移动端遮罩层 -->
<div v-if="isMobile && isSidebarOpen" class="layout__overlay" @click="closeSidebar" />
<!-- 布局内容插槽 -->
<slot></slot>
<!-- 设置面板 -->
<Settings v-if="isShowSettings" />
<!-- 返回顶部按钮 -->
<el-backtop target=".app-main">
<div class="i-svg:backtop w-6 h-6" />
</el-backtop>
</div>
</template>
<script setup lang="ts">
import { useLayout } from "../composables/useLayout";
import { useLayoutResponsive } from "../composables/useLayoutResponsive";
import Settings from "@/layout/components/Settings/index.vue";
// 布局相关
const { layoutClass, isShowSettings, isSidebarOpen, closeSidebar } = useLayout();
// 响应式处理
const { isMobile } = useLayoutResponsive();
</script>
<style lang="scss" scoped>
.layout {
width: 100%;
height: 100%;
&__overlay {
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
}
}
</style>

View File

@@ -0,0 +1,130 @@
<!-- 菜单组件 -->
<template>
<el-menu
ref="menuRef"
:default-active="currentRoute.path"
:collapse="!appStore.sidebar.opened"
:background-color="
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-background']
: undefined
"
:text-color="
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-text']
: undefined
"
:active-text-color="
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-active-text']
: undefined
"
:popper-effect="theme"
:unique-opened="false"
:collapse-transition="false"
:mode="menuMode"
@open="onMenuOpen"
@close="onMenuClose"
>
<!-- 菜单项 -->
<SidebarMenuItem
v-for="route in data"
:key="route.path"
:item="route"
:base-path="resolveFullPath(route.path)"
/>
</el-menu>
</template>
<script lang="ts" setup>
import { ref, computed, watch, PropType } from "vue";
import { useRoute } from "vue-router";
import path from "path-browserify";
import type { MenuInstance } from "element-plus";
import type { RouteRecordRaw } from "vue-router";
import { SidebarColor } from "@/enums/settings/theme.enum";
import { useSettingsStore, useAppStore } from "@/store";
import { isExternal } from "@/utils/index";
import SidebarMenuItem from "@/layout/components/Sidebar/components/SidebarMenuItem.vue";
import variables from "@/styles/variables.module.scss";
const props = defineProps({
data: {
type: Array as PropType<RouteRecordRaw[]>,
default: () => [],
},
basePath: {
type: String,
required: true,
example: "/system",
},
menuMode: {
type: String as PropType<"vertical" | "horizontal">,
default: "vertical",
validator: (value: string) => ["vertical", "horizontal"].includes(value),
},
});
const menuRef = ref<MenuInstance>();
const settingsStore = useSettingsStore();
const appStore = useAppStore();
const currentRoute = useRoute();
// 存储已展开的菜单项索引
const expandedMenuIndexes = ref<string[]>([]);
// 获取主题
const theme = computed(() => settingsStore.theme);
// 获取浅色主题下的侧边栏配色方案
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme);
/**
* 获取完整路径
*
* @param routePath 当前路由的相对路径 /user
* @returns 完整的绝对路径 D://vue3-element-admin/system/user
*/
function resolveFullPath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
// 解析路径,生成完整的绝对路径
return path.resolve(props.basePath, routePath);
}
/**
* 打开菜单
*
* @param index 当前展开的菜单项索引
*/
const onMenuOpen = (index: string) => {
expandedMenuIndexes.value.push(index);
};
/**
* 关闭菜单
*
* @param index 当前收起的菜单项索引
*/
const onMenuClose = (index: string) => {
expandedMenuIndexes.value = expandedMenuIndexes.value.filter((item) => item !== index);
};
/**
* 监听菜单模式变化:当菜单模式切换为水平模式时,关闭所有展开的菜单项,
* 避免在水平模式下菜单项显示错位。
*/
watch(
() => props.menuMode,
(newMode) => {
if (newMode === "horizontal" && menuRef.value) {
expandedMenuIndexes.value.forEach((item) => menuRef.value!.close(item));
}
}
);
</script>

View File

@@ -0,0 +1,110 @@
<template>
<LayoutBase>
<!-- 左侧菜单栏 -->
<div class="layout__sidebar" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
<LayoutSidebar :show-logo="isShowLogo" :is-collapsed="!isSidebarOpen">
<el-scrollbar>
<SidebarMenu :data="routes" base-path="" />
</el-scrollbar>
</LayoutSidebar>
</div>
<!-- 主内容区 -->
<div
:class="{
hasTagsView: isShowTagsView,
'layout__main--collapsed': !isSidebarOpen,
}"
class="layout__main"
>
<NavBar />
<TagsView v-if="isShowTagsView" />
<AppMain />
</div>
</LayoutBase>
</template>
<script setup lang="ts">
import { useLayout } from "../../composables/useLayout";
import { useLayoutMenu } from "../../composables/useLayoutMenu";
import LayoutBase from "../LayoutBase.vue";
import LayoutSidebar from "../common/LayoutSidebar.vue";
import NavBar from "@/layout/components/NavBar/index.vue";
import TagsView from "@/layout/components/TagsView/index.vue";
import AppMain from "@/layout/components/AppMain/index.vue";
import SidebarMenu from "../LayoutMenu.vue";
// 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen } = useLayout();
// 菜单相关
const { routes } = useLayoutMenu();
</script>
<style lang="scss" scoped>
.layout {
&__sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 999;
width: $sidebar-width;
background-color: $menu-background;
transition: width 0.28s;
&--collapsed {
width: $sidebar-width-collapsed;
}
}
&__main {
position: relative;
height: 100%;
margin-left: $sidebar-width;
overflow-y: auto;
transition: margin-left 0.28s;
&--collapsed {
margin-left: $sidebar-width-collapsed;
}
.fixed-header {
position: sticky;
top: 0;
z-index: 9;
transition: width 0.28s;
}
}
}
/* 移动端样式 */
:deep(.mobile) {
.layout__sidebar {
position: fixed;
left: 0;
transition: transform 0.28s;
}
&.hideSidebar {
.layout__sidebar {
width: $sidebar-width !important;
transform: translateX(-$sidebar-width);
}
.layout__main {
margin-left: 0 !important;
}
}
.layout__main {
margin-left: 0;
}
}
:deep(.hasTagsView) {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<LayoutBase>
<!-- 顶部菜单栏 -->
<div class="layout__header">
<LayoutSidebar :show-logo="isShowLogo" :is-collapsed="false">
<SidebarMixTopMenu />
<NavbarActions />
</LayoutSidebar>
</div>
<!-- 主内容区容器 -->
<div class="layout__container">
<!-- 左侧菜单栏 -->
<div class="layout__sidebar--left" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
<el-scrollbar>
<SidebarMenu :data="sideMenuRoutes" :base-path="activeTopMenuPath" />
</el-scrollbar>
<!-- 侧边栏切换按钮 -->
<div class="layout__sidebar-toggle">
<Hamburger :is-active="isSidebarOpen" @toggle-click="toggleSidebar" />
</div>
</div>
<!-- 主内容区 -->
<div :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<TagsView v-if="isShowTagsView" />
<AppMain />
</div>
</div>
</LayoutBase>
</template>
<script setup lang="ts">
import { useLayout } from "../../composables/useLayout";
import { useLayoutMenu } from "../../composables/useLayoutMenu";
import LayoutBase from "../LayoutBase.vue";
import LayoutSidebar from "../common/LayoutSidebar.vue";
import SidebarMixTopMenu from "@/layout/components/Sidebar/components/SidebarMixTopMenu.vue";
import NavbarActions from "@/layout/components/NavBar/components/NavbarActions.vue";
import TagsView from "@/layout/components/TagsView/index.vue";
import AppMain from "@/layout/components/AppMain/index.vue";
import SidebarMenu from "../LayoutMenu.vue";
import Hamburger from "@/components/Hamburger/index.vue";
// 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen, toggleSidebar } = useLayout();
// 菜单相关
const { sideMenuRoutes, activeTopMenuPath } = useLayoutMenu();
</script>
<style lang="scss" scoped>
.layout {
&__header {
position: sticky;
top: 0;
z-index: 999;
width: 100%;
height: $navbar-height;
background-color: $menu-background;
:deep(.layout-sidebar) {
display: flex;
width: 100% !important;
height: $navbar-height;
}
}
&__container {
display: flex;
height: calc(100vh - $navbar-height);
padding-top: 0;
.layout__sidebar--left {
position: relative;
width: $sidebar-width;
height: 100%;
background-color: var(--menu-background);
transition: width 0.28s;
&--collapsed {
width: $sidebar-width-collapsed !important;
}
:deep(.el-scrollbar) {
height: calc(100vh - $navbar-height - 50px);
}
:deep(.el-menu) {
height: 100%;
border: none;
}
.layout__sidebar-toggle {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 50px;
line-height: 50px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
}
}
.layout__main {
flex: 1;
min-width: 0;
height: 100%;
margin-left: 0;
overflow-y: auto;
}
}
}
/* 移动端样式 */
:deep(.mobile) {
.layout__container {
.layout__sidebar--left {
position: fixed;
top: $navbar-height;
bottom: 0;
left: 0;
z-index: 1000;
transition: transform 0.28s;
}
}
&.hideSidebar {
.layout__sidebar--left {
width: $sidebar-width !important;
transform: translateX(-$sidebar-width);
}
}
}
:deep(.hasTagsView) {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<LayoutBase>
<!-- 顶部菜单栏 -->
<div class="layout__header">
<LayoutSidebar :show-logo="isShowLogo" :is-collapsed="false">
<el-scrollbar>
<SidebarMenu :data="routes" base-path="" menu-mode="horizontal" />
</el-scrollbar>
<NavbarActions />
</LayoutSidebar>
</div>
<!-- 主内容区 -->
<div :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<TagsView v-if="isShowTagsView" />
<AppMain />
</div>
</LayoutBase>
</template>
<script setup lang="ts">
import { useLayout } from "../../composables/useLayout";
import { useLayoutMenu } from "../../composables/useLayoutMenu";
import LayoutBase from "../LayoutBase.vue";
import LayoutSidebar from "../common/LayoutSidebar.vue";
import NavbarActions from "@/layout/components/NavBar/components/NavbarActions.vue";
import TagsView from "@/layout/components/TagsView/index.vue";
import AppMain from "@/layout/components/AppMain/index.vue";
import SidebarMenu from "../LayoutMenu.vue";
// 布局相关参数
const { isShowTagsView, isShowLogo } = useLayout();
// 菜单相关
const { routes } = useLayoutMenu();
</script>
<style lang="scss" scoped>
.layout {
&__header {
position: sticky;
top: 0;
z-index: 999;
width: 100%;
height: $navbar-height;
background-color: $menu-background;
:deep(.layout-sidebar) {
display: flex;
width: 100% !important;
height: $navbar-height;
.el-scrollbar {
flex: 1;
height: $navbar-height;
}
.el-menu-item,
.el-sub-menu__title,
.el-menu--horizontal {
height: $navbar-height;
line-height: $navbar-height;
}
}
}
&__main {
position: relative;
height: calc(100vh - $navbar-height);
margin-left: 0;
overflow-y: auto;
.fixed-header {
position: sticky;
top: 0;
z-index: 9;
}
}
}
:deep(.hasTagsView) {
.app-main {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div :class="{ 'has-logo': showLogo }" class="layout-sidebar">
<!-- Logo -->
<SidebarLogo v-if="showLogo" :collapse="isCollapsed" />
<!-- 主菜单内容 -->
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineProps({
/**
* 是否显示Logo
*/
showLogo: {
type: Boolean,
default: true,
},
/**
* 是否折叠
*/
isCollapsed: {
type: Boolean,
default: false,
},
});
</script>
<style lang="scss" scoped>
.layout-sidebar {
position: relative;
height: 100%;
background-color: var(--menu-background);
transition: width 0.28s;
&.has-logo {
.el-scrollbar {
height: calc(100vh - $navbar-height);
}
}
:deep(.el-menu) {
border: none;
}
}
</style>

View File

@@ -0,0 +1,66 @@
import { computed, watchEffect } from "vue";
import { useAppStore, useSettingsStore } from "@/store";
import defaultSettings from "@/settings";
/**
* 布局相关的通用逻辑
*/
export function useLayout() {
const appStore = useAppStore();
const settingsStore = useSettingsStore();
// 计算当前布局模式
const currentLayout = computed(() => settingsStore.layout);
// 侧边栏展开状态
const isSidebarOpen = computed(() => appStore.sidebar.opened);
// 是否显示标签视图
const isShowTagsView = computed(() => settingsStore.tagsView);
// 是否显示设置面板
const isShowSettings = computed(() => defaultSettings.showSettings);
// 是否显示Logo
const isShowLogo = computed(() => settingsStore.sidebarLogo);
// 布局CSS类
const layoutClass = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
mobile: appStore.device === "mobile",
[`layout-${settingsStore.layout}`]: true,
}));
/**
* 处理切换侧边栏的展开/收起状态
*/
function toggleSidebar() {
appStore.toggleSidebar();
}
/**
* 关闭侧边栏(移动端)
*/
function closeSidebar() {
appStore.closeSideBar();
}
// 监听路由变化,在移动端自动关闭侧边栏
watchEffect(() => {
if (appStore.device === "mobile" && appStore.sidebar.opened) {
appStore.closeSideBar();
}
});
return {
currentLayout,
isSidebarOpen,
isShowTagsView,
isShowSettings,
isShowLogo,
layoutClass,
toggleSidebar,
closeSidebar,
};
}

View File

@@ -0,0 +1,58 @@
import { computed, watch } from "vue";
import { useRoute } from "vue-router";
import { useAppStore, usePermissionStore } from "@/store";
/**
* 布局菜单处理逻辑
*/
export function useLayoutMenu() {
const route = useRoute();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
// 顶部菜单激活路径
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
// 常规路由(左侧菜单或顶部菜单)
const routes = computed(() => permissionStore.routes);
// 混合布局左侧菜单路由
const sideMenuRoutes = computed(() => permissionStore.sideMenuRoutes);
// 当前激活的菜单
const activeMenu = computed(() => {
const { meta, path } = route;
// 如果设置了activeMenu则使用
if (meta?.activeMenu) {
return meta.activeMenu;
}
return path;
});
// 监听顶部菜单路径变化,更新侧边菜单
watch(
() => activeTopMenuPath.value,
(newPath) => {
permissionStore.updateSideMenu(newPath);
},
{ immediate: true }
);
/**
* 处理顶部菜单点击
* @param path 菜单路径
*/
function handleTopMenuClick(path: string) {
appStore.activeTopMenu(path);
}
return {
routes,
sideMenuRoutes,
activeMenu,
activeTopMenuPath,
handleTopMenuClick,
};
}

View File

@@ -0,0 +1,36 @@
import { watchEffect, computed } from "vue";
import { useWindowSize } from "@vueuse/core";
import { useAppStore } from "@/store";
import { DeviceEnum } from "@/enums/settings/device.enum";
/**
* 布局响应式处理逻辑
*/
export function useLayoutResponsive() {
const appStore = useAppStore();
const { width } = useWindowSize();
// 定义响应式断点
const WIDTH_DESKTOP = 992; // 桌面设备断点 (>=992px)
// 设置当前设备类型并调整侧边栏状态
watchEffect(() => {
const isDesktop = width.value >= WIDTH_DESKTOP;
const deviceType = isDesktop ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE;
// 更新设备类型
appStore.toggleDevice(deviceType);
// 根据设备类型调整侧边栏状态
if (isDesktop) {
appStore.openSideBar();
} else {
appStore.closeSideBar();
}
});
return {
isDesktop: computed(() => width.value >= WIDTH_DESKTOP),
isMobile: computed(() => appStore.device === DeviceEnum.MOBILE),
};
}

27
src/layouts/index.vue Normal file
View File

@@ -0,0 +1,27 @@
<template>
<component :is="currentLayoutComponent" />
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useLayout } from "./composables/useLayout";
import LeftSideLayout from "./components/LeftSideLayout/index.vue";
import TopMenuLayout from "./components/TopMenuLayout/index.vue";
import MixMenuLayout from "./components/MixMenuLayout/index.vue";
import { LayoutMode } from "@/enums/settings/layout.enum";
const { currentLayout } = useLayout();
// 根据当前布局模式选择对应的组件
const currentLayoutComponent = computed(() => {
switch (currentLayout.value) {
case LayoutMode.TOP:
return TopMenuLayout;
case LayoutMode.MIX:
return MixMenuLayout;
case LayoutMode.LEFT:
default:
return LeftSideLayout;
}
});
</script>

View File

@@ -1,7 +1,7 @@
import type { App } from "vue";
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
export const Layout = () => import("@/layout/index.vue");
export const Layout = () => import("@/layouts/index.vue");
// 静态路由
export const constantRoutes: RouteRecordRaw[] = [

View File

@@ -5,7 +5,7 @@ import router from "@/router";
import MenuAPI, { type RouteVO } from "@/api/system/menu.api";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
const Layout = () => import("@/layouts/index.vue");
export const usePermissionStore = defineStore("permission", () => {
// 存储所有路由,包括静态路由和动态路由