diff --git a/src/layout/index.vue b/src/layout/index.vue deleted file mode 100644 index 9227afc8..00000000 --- a/src/layout/index.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - diff --git a/src/layouts/README.md b/src/layouts/README.md new file mode 100644 index 00000000..f31eb7b5 --- /dev/null +++ b/src/layouts/README.md @@ -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 + +提供菜单相关功能: +- 获取菜单数据 +- 处理菜单激活状态 +- 混合布局下的菜单联动 \ No newline at end of file diff --git a/src/layouts/components/LayoutBase.vue b/src/layouts/components/LayoutBase.vue new file mode 100644 index 00000000..bcd43332 --- /dev/null +++ b/src/layouts/components/LayoutBase.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/layouts/components/LayoutMenu.vue b/src/layouts/components/LayoutMenu.vue new file mode 100644 index 00000000..2c5ae936 --- /dev/null +++ b/src/layouts/components/LayoutMenu.vue @@ -0,0 +1,130 @@ + + + + diff --git a/src/layouts/components/LeftSideLayout/index.vue b/src/layouts/components/LeftSideLayout/index.vue new file mode 100644 index 00000000..429f9eb6 --- /dev/null +++ b/src/layouts/components/LeftSideLayout/index.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/layouts/components/MixMenuLayout/index.vue b/src/layouts/components/MixMenuLayout/index.vue new file mode 100644 index 00000000..aa78e9a2 --- /dev/null +++ b/src/layouts/components/MixMenuLayout/index.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/layouts/components/TopMenuLayout/index.vue b/src/layouts/components/TopMenuLayout/index.vue new file mode 100644 index 00000000..9f7607c0 --- /dev/null +++ b/src/layouts/components/TopMenuLayout/index.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/layouts/components/common/LayoutSidebar.vue b/src/layouts/components/common/LayoutSidebar.vue new file mode 100644 index 00000000..6933bd30 --- /dev/null +++ b/src/layouts/components/common/LayoutSidebar.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/layouts/composables/useLayout.ts b/src/layouts/composables/useLayout.ts new file mode 100644 index 00000000..9fec35dd --- /dev/null +++ b/src/layouts/composables/useLayout.ts @@ -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, + }; +} diff --git a/src/layouts/composables/useLayoutMenu.ts b/src/layouts/composables/useLayoutMenu.ts new file mode 100644 index 00000000..68d35e9a --- /dev/null +++ b/src/layouts/composables/useLayoutMenu.ts @@ -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, + }; +} diff --git a/src/layouts/composables/useLayoutResponsive.ts b/src/layouts/composables/useLayoutResponsive.ts new file mode 100644 index 00000000..b8dd9ada --- /dev/null +++ b/src/layouts/composables/useLayoutResponsive.ts @@ -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), + }; +} diff --git a/src/layouts/index.vue b/src/layouts/index.vue new file mode 100644 index 00000000..6c7209a3 --- /dev/null +++ b/src/layouts/index.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/router/index.ts b/src/router/index.ts index 57058ee4..38bf3e60 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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[] = [ diff --git a/src/store/modules/permission.store.ts b/src/store/modules/permission.store.ts index 968eb08a..007e786e 100644 --- a/src/store/modules/permission.store.ts +++ b/src/store/modules/permission.store.ts @@ -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", () => { // 存储所有路由,包括静态路由和动态路由