Skip to content
目录

Vue3后台管理系统(五):布局系统

一、自适应布局实现

1.1 布局系统设计思路

  1. 响应式设计:需要适配不同尺寸的屏幕
  2. 可折叠侧边栏:提供更多内容显示空间
  3. 多级菜单支持:适应复杂的导航需求
  4. 内容区域自适应:保持良好的内容展示

布局设计参考了经典的Admin布局,分为三个主要区域:

  • 侧边栏(Sidebar):显示导航菜单
  • 顶部栏(AppHeader):显示面包屑、用户信息等
  • 主内容区(AppMain):显示页面内容

1.2 基础布局组件实现

vue
<!-- src/layout/index.vue -->
<template>
	<div class="app-wrapper">
		<Sidebar class="sidebar-container" />
		<div class="main-container">
			<div class="header">
				<AppHeader />
			</div>
			<div class="app-main" v-loading="loading" element-loading-text="加载中..." element-loading-background="rgba(0, 0, 0, 0)">
				<router-view v-slot="{ Component }">
					<transition name="el-fade-in">
						<keep-alive :include="keepAliveRoutes">
							<component :is="Component" :key="$route.path" />
						</keep-alive>
					</transition>
				</router-view>
			</div>
		</div>
	</div>
</template>

<script setup>
	import Sidebar from './components/Sidebar.vue' // 侧边栏组件
	import AppHeader from './components/AppHeader.vue' // 顶部导航栏组件
	import { useLoadingStore } from '@/stores/loading' // 加载状态管理
	import { storeToRefs } from 'pinia' // 保持store属性的响应性
	import { ref, computed, provide } from 'vue' // Vue3组合式API

	// 从loading store中提取loading状态,保持响应性
	const { loading } = storeToRefs(useLoadingStore())

	// 路由历史记录 - 存储访问过的路由信息
	const routes = ref([])

	// 需要缓存的路由组件名列表 - 用于keep-alive
	const keepAliveRoutes = computed(() => {
		return routes.value.map((item) => item.name).filter(Boolean)
	})

	// 提供给AppHeader使用的方法和数据
	provide('routeHistory', {
		routes,
		keepAliveRoutes,
	})
	// 使用依赖注入共享数据
	// 1. 通过provide/inject在组件树中共享状态,避免props逐级传递
</script>

<style lang="scss" scoped>
	@use '@/styles/variables.scss' as *; // 导入全局样式变量
	// 主布局样式
	.app-wrapper {
		height: 100%;
		width: 100%;
		display: flex; 
		// flex布局实现左右分栏
		// 1. 简单高效地实现侧边栏+主内容区的布局
		// 2. 自动适应内容高度,无需手动计算
		// 侧边栏容器
		.sidebar-container {
			height: 100%;
			background-color: #304156;
			transition: width 0.3s; // 添加过渡效果,平滑宽度变化
		}
		// 主内容区容器
		.main-container {
			flex: 1; // 占据剩余空间
			display: flex;
			flex-direction: column; // 垂直方向flex布局
			overflow: hidden; // 防止内容溢出
			// 嵌套flex布局
			// 1. 外层flex实现左右布局
			// 2. 内层flex实现上下布局
			// 3. 完美适配各种屏幕尺寸,自动处理内容溢出
			// 顶部导航栏
			.header {
				height: $header-height; // 使用变量控制高度,便于统一管理
				background: #fff;
				box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); // 添加阴影,增强层次感
				// 使用SCSS变量
				// 1. 从变量文件导入尺寸,便于统一修改
				// 2. 使用轻微阴影分隔顶栏与内容区,增强视觉体验
			}
			// 主内容区
			.app-main {
				flex: 1; // 占据剩余空间
				padding: 10px; // 内容区内边距
				overflow-y: auto; // 内容超出时显示纵向滚动条
				background-color: $background-color; // 使用变量控制背景色
				// 自定义滚动条样式
				&::-webkit-scrollbar {
					width: 6px; // 窄滚动条,美观且不占空间
				}
				// 内容区自适应
				// 1. 使用flex: 1自动占据剩余空间
				// 2. 只在内容区显示滚动条,保持整体布局稳定
				// 3. 自定义滚动条样式,提升用户体验
			}
		}
	}
</style>

1.3 使用组合式API管理布局状态

使用Pinia与组合式API来管理布局状态:

javascript
// src/stores/layout.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

/**
 * @description 布局状态管理
 * @Author Wangkaibing
 * @Date 2025-04-15
 * 
 * Usage:
 * import { useLayoutStore } from '@/stores/layout'
 * 
 * const layoutStore = useLayoutStore()
 * console.log(layoutStore.scale) // 获取当前缩放比例
 * layoutStore.setScale(0.8) // 设置缩放比例
 * 
 * // 在组件中结合计算属性使用
 * const isCollapse = computed(() => layoutStore.scale < 0.75)
 */
export const useLayoutStore = defineStore('layout', () => {
	// 页面缩放比例,默认为1(原始大小)
	const scale = ref(1)

	// 设置缩放比例的方法
	const setScale = (value) => {
		scale.value = value
	}
	// 提供修改状态的方法
	// 1. 使用方法封装状态修改,遵循单一职责原则

	// 返回状态和方法
	return {
		scale,
		setScale,
	}
})

二、侧边栏实现

2.1 侧边栏组件

vue
<!-- src/layout/components/Sidebar.vue -->
<template>
	<div class="sidebar" :class="{ 'is-collapse': isCollapse }">
		<div class="logo-container">
			<div class="logo-content">
				<img class="logo" src="https://cdn.xjy0.cn/filling/top_logo.png" />
				<div class="title" v-show="!isCollapse">畜牧产业数据填报系统</div>
				<!-- 根据折叠状态条件显示标题
					 1. 使用v-show而非v-if,避免频繁创建销毁DOM
					 2. 折叠时隐藏标题,节省空间
				-->
			</div>
		</div>
		<div class="menu-container">
			<el-scrollbar>
				<!-- 使用Element Plus滚动条组件,确保内容超出时可滚动 -->
				<el-menu 
					:default-active="route.path" 
					class="el-menu-vertical" 
					:collapse="isCollapse" 
					:unique-opened="true" 
					router
				>
					<!-- 菜单配置
						1. default-active绑定当前路由路径,自动高亮当前菜单
						2. collapse控制菜单折叠状态,响应式更新
						3. unique-opened确保同时只有一个子菜单展开
						4. router属性启用路由模式,点击菜单项自动跳转到对应路由
					-->
					<template v-for="item in menuRoutes" :key="item.path">
						<el-sub-menu :index="item.path" popper-class="sidebar-popper">
							<template #title>
								<!-- 菜单图标 -->
								<img 
									class="menu-icon" 
									:src="`https://cdn.xjy0.cn/filling/nav_icon${item.meta?.icon || '5'}.png`" 
								/>
								<span>{{ item.meta?.title }}</span>
								<el-icon class="menu-arrow-icon">
									<CaretBottom />
								</el-icon>
							</template>
							<!-- 子菜单遍历 -->
							<el-menu-item 
								v-for="child in item.children" 
								:key="child.path" 
								:index="item.path + '/' + child.path"
							>
                <span>{{ child.meta?.title }}</span>
              </el-menu-item>
						</el-sub-menu>
					</template>
				</el-menu>
			</el-scrollbar>
			<!-- 折叠/展开按钮 -->
			<div class="collapse-btn" @click="toggleCollapse">
				<el-icon class="collapse-icon">
					<Expand v-if="isCollapse" />
					<Fold v-else />
					<!-- 动态图标
						1. 根据当前状态显示不同图标
						2. 展开时显示折叠图标,折叠时显示展开图标
						3. 图标直观表示点击后的状态变化
					-->
				</el-icon>
			</div>
		</div>
	</div>
</template>

<script setup>
	import { computed, ref } from 'vue' // Vue3 组合式API
	import { useRoute } from 'vue-router' // Vue Router组合式API
	import { usePermissionStore } from '@/composables/usePermission' // 权限相关组合式API
	import { useLayoutStore } from '@/stores/layout' // 布局状态管理

	const route = useRoute() // 获取当前路由实例
	const permissionStore = usePermissionStore() // 获取权限状态管理
	const layoutStore = useLayoutStore() // 获取布局状态管理

	// 手动控制折叠状态
	const manualCollapse = ref(false)

	// 计算侧边栏是否折叠
	const isCollapse = computed(() => {
		// 如果是手动控制,优先使用手动状态
		if (manualCollapse.value) {
			return true
		}
		// 否则根据scale自动计算
		return layoutStore.scale < 0.75
	})

	// 计算显示的路由菜单
	const menuRoutes = computed(() => {
		return permissionStore.routes.filter((route) => 
			route.meta?.sidebarVisible !== false && // 过滤不应在侧边栏显示的路由
			route.children?.length > 0 // 确保有子路由才显示
		)
		// 菜单数据处理
		// 1. 基于权限路由动态生成菜单
		// 2. 使用computed自动响应路由变化
		// 3. 使用可选链操作符安全访问属性
		// 4. 通过meta.sidebarVisible控制菜单项显隐
	})

	// 手动切换菜单展开/收起
	const toggleCollapse = () => {
		if (isCollapse.value) {
			// 展开
			manualCollapse.value = false
			layoutStore.setScale(1) // 恢复原始比例
		} else {
			// 收起
			manualCollapse.value = true
			layoutStore.setScale(0.8) // 设置较小比例
		}
	}
</script>

<style lang="scss" scoped>
	.sidebar {
		height: 100%;
		width: $sidebar-width; // 使用SCSS变量
		transition: all 0.3s; // 过渡效果
		position: relative;
		z-index: 10; // 确保侧边栏位于顶层
		// 折叠状态样式
		&.is-collapse {
			width: $sidebar-collapse-width;
			.logo-container {
				padding: 20px 0;
				.logo-content {
					justify-content: center;
					.logo {
						margin: 0;
						width: 35px;
						height: 30px;
					}
				}
			}
			.collapse-btn {
				right: calc(50% - 20px);
			}
		}
	}
	// LOGO容器样式
	.logo-container {
		height: $logo-height;
		padding: 20px;
		background-color: $sidebar-bg-color;
		position: relative;
		.logo-content {
			height: 100%;
			display: flex;
			align-items: center;
			flex-direction: column;
			justify-content: center; // 垂直居中排列
			.logo {
				width: $logo-width;
				height: 43px;
				margin-bottom: 10px;
			}
			.title {
				color: $sidebar-text-color;
				font-size: 22px;
				letter-spacing: 3px;
				font-weight: 900;
				line-height: 38px;
				margin: 0;
				white-space: nowrap; // 防止文字换行
			}
		}
	}
	// 菜单容器样式
	.menu-container {
		height: calc(100% - #{$logo-height}); // 动态计算高度
		background-color: $primary-light-color;
		box-shadow: 0 2px 4px rgba(0, 47, 141, 0.05); // 轻微阴影
		// SCSS插值语法
		// 1. 使用calc(100% - #{$logo-height})动态计算菜单容器高度
		// 2. 通过SCSS变量确保高度计算与实际logo高度保持同步
	}
	// 自定义菜单图标样式
	.menu-icon {
		width: 20px;
		height: 20px;
		margin-right: 14px;
		vertical-align: middle;
	}
	:deep(.el-menu) {
		border: none; // 去除默认边框
		background-color: transparent; // 背景透明
		// 菜单项和子菜单标题样式
		.el-menu-item,
		.el-sub-menu__title {
			height: 65px; // 较大的菜单高度
			line-height: 65px;
			color: $sidebar-text-color;
			background-color: transparent;
			font-size: $menu-text-size;
			padding: 0 20px !important; // 强制覆盖默认padding
			&:hover {
				color: $sidebar-active-text-color;
				background-color: $primary-active-color;
			}
		}
		// 菜单项样式
		.el-menu-item {
			padding-left: 20px !important;
			&.is-active {
				color: $sidebar-active-text-color;
				background-color: $primary-active-color;
			}
		}
		// 子菜单样式
		.el-sub-menu {
			&.is-active {
				> .el-sub-menu__title {
					color: $sidebar-active-text-color;
				}
			}
			// 子菜单列表样式
			.el-menu {
				background-color: $primary-dark-color !important; // 强制深色背景
				.el-menu-item {
					padding-left: 55px !important; // 子菜单项缩进
					height: 65px;
					line-height: 65px;
					&:hover {
						color: $sidebar-active-text-color;
						background-color: $primary-active-color;
					}
					&.is-active {
						color: $sidebar-active-text-color;
						background-color: $primary-active-color;
					}
          span {
            width: 200px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis; // 文字溢出显示省略号
          }
					// 文本溢出处理
					// 1. 宽度限制为200px,超出宽度文本自动截断
					// 2. 使用text-overflow: ellipsis显示省略号
					// 3. 保持布局整洁,防止长文本破坏UI
				}
			}
		}
	}
	// 子菜单标题样式
	:deep(.el-sub-menu) {
		.el-sub-menu__title {
			position: relative;
			.el-sub-menu__icon-arrow {
				display: none; // 隐藏默认箭头图标
			}
		}
	}
	// 自定义菜单箭头图标
	.menu-arrow-icon {
		position: absolute;
		right: 20px;
		top: 50%;
		transform: translateY(-50%); // 垂直居中
		font-size: 28px;
		opacity: 50%; // 半透明
		color: currentColor; // 继承父元素颜色
		transition: transform 0.3s;
		transform-origin: center 25%;
		// 自定义箭头图标
		// 1. 使用绝对定位将箭头放在菜单项右侧
		// 2. 使用transform垂直居中
		// 3. 半透明效果使界面更柔和
		// 4. 使用currentColor继承父元素文字颜色
	}
	// 折叠菜单样式
	:deep(.el-menu--collapse) {
		width: $sidebar-collapse-width; // 折叠宽度
		.el-menu-item,
		.el-sub-menu__title {
			padding: 0 !important;
			justify-content: center; // 居中对齐
			.menu-icon {
				margin: 0; // 移除图标右侧间距
			}
			.menu-arrow-icon {
				display: none; // 隐藏箭头
			}
		}
		// 折叠状态优化
		// 1. 固定宽度与变量保持一致
		// 2. 内容居中显示,只保留图标
		// 3. 隐藏不必要的元素如箭头,简化界面
	}
	// 折叠按钮样式
	.collapse-btn {
		position: absolute;
		right: 20px;
		bottom: 20px; // 定位在侧边栏底部
		cursor: pointer; // 鼠标指针样式
		.collapse-icon {
			font-size: 45px;
			color: #ffffff;
		}
		// 折叠控制按钮
		// 1. 使用绝对定位,固定在侧边栏底部
		// 2. 较大图标尺寸,提高可点击性
		// 3. 白色图标在深色背景上对比度高,易于识别
	}
</style>

2.2 侧边栏折叠功能

根据屏幕尺寸和用户操作来控制侧边栏的折叠:

  • 当屏幕缩放比例低于0.75时自动折叠
  • 用户可以通过底部按钮手动折叠/展开

三、顶部导航栏实现

顶部导航栏通常包含面包屑导航、用户信息和操作菜单:

vue
<!-- src/layout/components/AppHeader.vue -->
<template>
	<div class="navbar">
		<div class="left-menu">
			<div class="breadcrumb-tags">
				<router-link v-for="(item, index) in routeHistory.routes.value" :key="item.path" :to="item.path" class="tag-item" :class="{ active: route.path === item.path }">
					{{ item.title }}
					<el-icon class="close-icon" @click.prevent="closePage(item, index)" v-if="routeHistory.routes.value.length > 1">
						<Close />
					</el-icon>
				</router-link>
			</div>
		</div>
		<div class="right-menu">
			<el-button class="right-menu-item" link @click="handChangeRole" v-role="'ROLE_SWITCH_USER'">
				<el-image style="width: 24px; height: 24px; margin-right: 5px" src="https://cdn.xjy0.cn/filling/changeRole.png" />
				切换账号
			</el-button>
			<el-dropdown class="avatar-container" trigger="click">
				<div class="avatar-wrapper">
					<el-avatar shape="square" :size="32" src="https://cdn.xjy0.cn/filling/Group.png" />
					<span class="name">{{ authStore.state.userInfo.fullName }}</span>
					<el-icon class="el-icon--right">
						<arrow-down />
					</el-icon>
				</div>
				<template #dropdown>
					<el-dropdown-menu class="user-dropdown">
						<router-link to="/my/account">
							<el-dropdown-item>我的账户</el-dropdown-item>
						</router-link>
						<el-dropdown-item divided @click="handleLogout">退出登录</el-dropdown-item>
					</el-dropdown-menu>
				</template>
			</el-dropdown>
		</div>
		<el-dialog v-model="dialogVisible" title="切换账号" width="600px" :close-on-click-modal="false" destroy-on-close>
			<el-form ref="formRef" :model="formData" :rules="rules" label-width="130px">
				<el-form-item label="请输入账号" prop="mobile">
					<el-input v-model="formData.mobile" placeholder="请输入手机号" maxlength="11" show-word-limit />
				</el-form-item>
			</el-form>
			<template #footer>
				<span class="dialog-footer flex-between">
					<el-button @click="dialogVisible = false">取消</el-button>
					<el-button type="primary" @click="handleSubmit">确定</el-button>
				</span>
			</template>
		</el-dialog>
	</div>
</template>

<script setup>
	import { useAuthStore } from '@/stores/auth'
	import { useLogin } from '@/composables/useLogin.js'
	import { usePost } from '@/api/request.js'
	import { TokenService } from '@/utils/token.js'
  import { invalidateCache } from 'alova'

	const router = useRouter()
	const route = useRoute()
	const { logout } = useLogin()
	const authStore = useAuthStore()
	// 注入路由历史相关数据和方法
	const routeHistory = inject('routeHistory')
	const dialogVisible = ref(false)
	const formRef = ref(null)
	const formData = reactive({
		mobile: '',
	})

	watch(
		() => route.path,
		() => {
			// 排除登录页和其他不需要记录的页面
			const excludePaths = ['/login', '/404']
			if (excludePaths.includes(route.path)) return
			const currentRoute = {
				path: route.path,
				name: route.name,
				title: route.meta.title,
			}

			// 检查是否已存在
			const index = routeHistory.routes.value.findIndex((item) => item.path === currentRoute.path)
			if (index === -1) {
				// 不存在则添加
				if (routeHistory.routes.value.length >= 6) {
					routeHistory.routes.value.shift()
				}
				routeHistory.routes.value.push(currentRoute)
			}
		},
		{ immediate: true },
	)

	// 校验
	const rules = {
		mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
	}

	// 切换账号
	const handChangeRole = () => {
		formData.mobile = ''
		dialogVisible.value = true
	}

	const handleSubmit = async () => {
		if (!formRef.value) return
		await formRef.value.validate(async (valid) => {
			if (valid) {
				usePost('/api/xxxx', { mobile: formData.mobile, token: TokenService.getRefreshToken() }).then((res) => {
					if (res && res.data) {
						const { token_type, access_token, refresh_token, expires_in } = res
						TokenService.setTokens(token_type + ' ' + access_token, refresh_token, expires_in * 1000)
						dialogVisible.value = false
            router.push('/')
						setTimeout(() => {
							window.location.reload()
						}, 500)
					}
				})
			}
		})
	}

	// 关闭页面
	const closePage = (item, index) => {
		routeHistory.routes.value.splice(index, 1)
		// 如果关闭的是当前页面,需要跳转
		if (item.path === route.path) {
			if (index === 0) {
				// 如果关闭的是第一个,跳转到新的第一个
				if (routeHistory.routes.value.length > 0) {
					router.push(routeHistory.routes.value[0].path)
				} else {
					router.push('/my/account')
				}
			} else {
				// 否则跳转到前一个
				router.push(routeHistory.routes.value[index - 1].path)
			}
		}
	}

	// 退出
	const handleLogout = () => {
		setTimeout(() => {
      // 清除接口缓存
      invalidateCache()
			routeHistory.routes.value = []
			logout()
		}, 500)
	}
</script>

<style lang="scss" scoped>
	.navbar {
		height: 60px;
		background: #fff;
		border-bottom: 1px solid #f0f0f0;
		box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 0 10px;
	}
	.left-menu {
		.breadcrumb-tags {
			display: flex;
			align-items: center;
			height: 60px;
			.tag-item {
				display: inline-flex;
				align-items: center;
				height: 32px;
				padding: 0 8px;
				margin-right: 8px;
				color: #666;
				background: #f5f7fa;
				border: 1px solid #e4e7ed;
				border-radius: 2px;
				text-decoration: none;
				transition: all 0.3s;
				.close-icon {
					margin-left: 4px;
					font-size: 12px;
					cursor: pointer;
					&:hover {
						color: #1f75e3;
					}
				}
				&:hover {
					color: #1f75e3;
					border-color: #1f75e3;
					background: #ecf5ff;
				}
				&.active {
					color: #1f75e3;
					border-color: #1f75e3;
					background: #ecf5ff;
				}
			}
		}
	}
	.right-menu {
		display: flex;
		align-items: center;
		gap: 8px;
		.right-menu-item {
			display: inline-flex;
			align-items: center;
			justify-content: center;
			height: 60px;
			padding: 0 12px;
			color: #666;
			vertical-align: text-bottom;
			background: transparent;
			border-radius: 0;
			&:hover {
				background: rgba(0, 0, 0, 0.025);
				color: #1f75e3;
			}
			.icon {
				width: 16px;
				height: 16px;
				margin-right: 4px;
			}
		}
	}
	.avatar-container {
		margin-left: 8px;
		.avatar-wrapper {
			display: flex;
			align-items: center;
			padding: 0 12px;
			height: 60px;
			cursor: pointer;
			.el-avatar--square {
				background-color: transparent;
			}
			&:hover {
				background: rgba(0, 0, 0, 0.025);
			}
			.name {
				margin: 0 4px 0 8px;
				color: #666;
			}
			.el-icon--right {
				color: #666;
				font-size: 12px;
			}
		}
	}
	:deep(.user-dropdown) {
		.el-dropdown-menu__item {
			padding: 8px 16px;
			line-height: 1.5;
			color: #666;
			&:not(.is-disabled):hover {
				background-color: #f5f7fa;
				color: #1f75e3;
			}
		}
	}
</style>

3.1 标签式导航

在顶部导航栏中实现了类似浏览器标签页的导航功能,方便快速在多个访问过的页面间切换。每个标签都对应一个路由,包含以下特点:

  1. 自动记录访问历史
  2. 点击标签可直接跳转到对应页面
  3. 可以关闭不需要的标签页
  4. 限制最大标签数量,防止过多标签影响用户体验

四、多级菜单与路由整合

4.1 动态菜单与权限

基于权限系统生成动态菜单:

  1. 通过permissionStore.routes获取当前用户有权限访问的路由
  2. 使用computed属性过滤出需要在侧边栏显示的菜单项
  3. 结合路由元数据(meta)展示菜单图标和标题

菜单与路由的关系非常紧密,通过在路由配置中添加元数据来控制菜单的显示:

javascript
// src/router/routes.js 示例
{
  path: '/platform',
  component: Layout,
  redirect: '/platform/user',
  meta: {
    title: '平台管理',
    icon: '6',
    sidebarVisible: true, // 是否在侧边栏显示
  },
  children: [
    {
      path: 'user',
      name: 'PlatformUsers',
      component: () => import('@/views/platform/user.vue'),
      meta: {
        title: '用户管理',
      },
    },
    {
      path: 'role',
      name: 'PlatformRole',
      component: () => import('@/views/platform/role.vue'),
      meta: {
        title: '角色管理',
      },
    },
  ],
}

五、响应式设计与自适应布局

5.1 使用SCSS变量统一布局尺寸

为了保持布局的一致性和可维护性,使用SCSS变量定义关键的布局尺寸:

scss
// src/styles/variables.scss
// 布局相关变量
$sidebar-width: 210px;                      // 侧边栏宽度
$sidebar-collapse-width: 54px;              // 侧边栏折叠后宽度
$header-height: 60px;                       // 顶部导航栏高度
$logo-height: 130px;                        // Logo区域高度
$logo-width: 80px;                          // Logo宽度

// 颜色变量
$primary-color: #1f75e3;                    // 主色调
$primary-light-color: #304156;              // 侧边栏背景色
$primary-dark-color: #1f2d3d;               // 侧边栏子菜单背景色
$primary-active-color: rgba(0, 0, 0, 0.2);  // 菜单激活背景色
$sidebar-bg-color: #263445;                 // 侧边栏Logo区域背景色
$sidebar-text-color: #f5f5f5;               // 侧边栏文字颜色
$sidebar-active-text-color: #fff;           // 侧边栏激活文字颜色
$background-color: #f0f2f5;                 // 页面背景色

// 字体尺寸变量
$menu-text-size: 15px;                      // 菜单文字大小

5.2 缩放自适应处理

使用缩放比例来处理不同屏幕尺寸的适应:

javascript
// 在合适的组件中导入并使用自适应逻辑
import { useLayoutStore } from '@/stores/layout'
import autofit from 'autofit.js'

const layoutStore = useLayoutStore()

// 监听缩放比例变化
const resizeHandler = () => {
  // 根据屏幕宽度计算缩放比例
  const scale = autofit.getScale()
  layoutStore.setScale(scale)
}

// 挂载时添加监听
onMounted(() => {
  window.addEventListener('resize', resizeHandler)
  // 初始化时调用一次
  resizeHandler()
})

// 卸载时移除监听
onUnmounted(() => {
  window.removeEventListener('resize', resizeHandler)
})

5.3 自动折叠侧边栏

当屏幕宽度过窄或用户手动切换时,侧边栏会自动折叠:

javascript
// 在Sidebar.vue中
const isCollapse = computed(() => {
  // 如果是手动控制,优先使用手动状态
  if (manualCollapse.value) {
    return true
  }
  // 否则根据scale自动计算
  return layoutStore.scale < 0.75
})

Released under the MIT License.