功能12:折叠/展开侧边栏
功能11:实现面包屑功能
功能10:添加首页菜单项
功能9:退出登录功能
功能8:页面权限控制
功能7:路由全局前置守卫
功能6:动态添加路由记录
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目
前言
el-menu提供了侧边栏的折叠属性collapse,可以水平折叠收起菜单。
一.操作步骤
1.新建appStore
定义appStore,保存控制侧边栏状态的相关信息。
showSidebar :false侧边栏展开,true侧边栏折叠。
sidebarWidth :展开时的组件宽度。
import { defineStore } from 'pinia'
import { ref } from 'vue'const useAppStore = defineStore('app', () => {const showSidebar = ref(false)const sidebarWidth = ref(180)const setSidebar = () => {showSidebar.value = !showSidebar.valueif (sidebarWidth.value === 180) {sidebarWidth.value = 64} else {sidebarWidth.value = 180}}return {showSidebar,sidebarWidth,setSidebar}
})export default useAppStore
2.修改Navbar.vue
在面包屑前,增加一个图标,点击图标会触发appStore里的状态变化,实现侧边栏折叠和展开。
<template><el-header class="navbar"><div class="parent"><el-icon :size="20" class="hamburger-container" @click="toggleSideBar"><template v-if="appStore.showSidebar"><Expand /></template><template v-else><Fold /></template></el-icon><el-breadcrumb separator="/"><el-breadcrumb-item v-for="(item, index) in breadcrumbItems" :key="item.path"><router-link v-if="index === 0" :to="item.path">{{ item.meta.title }}</router-link><span v-else>{{ item.meta.title }}</span></el-breadcrumb-item></el-breadcrumb></div><div class="user-menu"><el-dropdown trigger="hover"><div class="avatar-container"><img :src="avatarUrl" alt="用户头像" class="avatar-image" /></div><template #dropdown><el-dropdown-menu class="dropdown-menu"><el-dropdown-item @click="handleSettings" class="menu-item"><span>个人设置</span></el-dropdown-item><el-dropdown-item divided @click="handleLogout" class="menu-item"><span>退出登录</span></el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></el-header>
</template><script setup>
import { ElMessageBox } from 'element-plus'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()const breadcrumbItems = computed(() => {// 获取当前路由的匹配数组,过滤无meta的路由const matched = route.matched.filter(item => item.meta?.title);// 确保首页始终在首位(如果当前路由不是首页)if (route.path !== '/' && matched[0]?.path !== '/') {matched.unshift({ path: '/index', meta: { title: '首页' } });}if (route.path === '/' || route.path === '/index') {matched.splice(0, matched.length - 1)}return matched;
})// 请确保在 assets 目录下存在 user-avatar.png 图片文件
const avatarUrl = new URL('@/assets/images/profile.jpg', import.meta.url).hrefimport useUserStore from '@/stores/user'
const userStore = useUserStore()const handleLogout = () => {// 处理退出登录逻辑ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {userStore.logout().then(() => {location.href = '/index';})}).catch(() => { });
}const handleSettings = () => {// 处理设置逻辑console.log('打开设置页面')
}import useAppStore from '@/stores/app'
const appStore = useAppStore()
const toggleSideBar = () => {appStore.setSidebar()
}
</script><style scoped lang="scss">
.parent {display: flex; /* 启用 Flex 布局 */gap: 16px; /* 子元素间距(可选) */justify-content: flex-start; /* 水平对齐方式(可选) */align-items: center; /* 垂直对齐方式(可选) */.hamburger-container {line-height: 46px;height: 100%;float: left;cursor: pointer;transition: background 0.3s;-webkit-tap-highlight-color: transparent;&:hover {background: rgba(0, 0, 0, 0.025);}}
}
/* 导航栏整体样式 */
.navbar {background-color: #ffffff;border-bottom-width: 1px;border-bottom-style: solid;border-bottom-color: #e5e7eb;display: flex;align-items: center;justify-content: space-between;height: 64px;padding: 0 24px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}/* 用户菜单容器 */
.user-menu {position: relative;z-index: 1000;
}/* 头像容器 */
.avatar-container {position: relative;width: 40px;height: 40px;transition: all 0.3s ease;
}/* 头像图片样式 */
.avatar-image {width: 100%;height: 100%;border-radius: 9999px;object-fit: cover;border: 2px solid #fff;box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}/* 悬停效果 */
.avatar-container:hover .avatar-image {transform: scale(1.1);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}/* 下拉菜单样式 */
.dropdown-menu {border: none !important;border-radius: 8px !important;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;margin-top: 12px !important;padding: 8px 0 !important;
}/* 菜单项样式 */
.menu-item {font-size: 14px;color: #333 !important;transition: all 0.2s ease;
}.menu-item:hover {background: #f5f7fa !important;color: #409eff !important;transform: translateX(4px);
}/* 分割线样式 */
.el-dropdown-menu__item--divided {border-color: #ebeef5 !important;
}
</style>
3.修改Sidebar.vue
将el-menu的collapse属性绑定到appstore的showSidebar,响应式的控制侧边栏的展开和折叠。
<template><el-menu router :default-active="activeMenu" :default-openeds="openedMenus" class="el-menu-vertical":collapse="appStore.showSidebar" unique-opened><template v-for="item in menuData" :key="item.path"><MenuItem :item="item" :level="0" /></template></el-menu>
</template><script setup>
import { defineProps, computed, ref } from 'vue';
import MenuItem from './MenuItem.vue';
import { useRoute } from 'vue-router';import useAppStore from '@/stores/app'
const appStore = useAppStore()const route = useRoute()
defineProps({menuData: {type: Array,required: true}
});const activeMenu = computed(() => {return route.path
})const openedMenus = computed(() => {return route.matched.map(item => item.path).filter(path => path !== '/')
})
</script><style lang="scss" scoped>
.el-menu-vertical {height: 100%;--el-menu-bg-color: #304156;--el-menu-text-color: #bfcbd9;--el-menu-active-color: #409EFF;--el-menu-hover-bg-color: #1e0b39;
}
</style>
4.修改MenuItem.vue
按照Element Plus的官网,调整el-menu的一些显示,让折叠的时候达到预期效果。
<template><div v-if="!item.hidden"><template v-if="hasChildren"><el-sub-menu :index="item.path"><template #title><el-icon><component is="House" /></el-icon><span>{{ item.meta?.title }}</span></template><template v-for="child in item.children" :key="child.path"><MenuItem :item="child" :level="level + 1" :base-path="resolvePath(item.path)" /></template></el-sub-menu></template><template v-else><el-menu-item :index="resolvePath(item.path)"><el-icon><component is="House" /></el-icon><template #title><span>{{ item.meta?.title }}</span></template></el-menu-item></template></div>
</template>
5.修改layout/index.vue
增加样式,让侧边栏的高度撑满整个屏幕。通过绑定appStore里的控制侧边栏宽度属性,响应式改变侧边栏的组件宽度。
<template><el-container class="container"><el-aside :width="appStore.sidebarWidth + 'px'" class="aside"><Sidebar :menu-data="permissionStore.arrayForMenu" /></el-aside><el-container><el-header height="48px"><Navbar /></el-header><AppMain /></el-container></el-container>
</template><script setup>
import { ElContainer, ElAside } from 'element-plus'
import Sidebar from './components/Sidebar.vue'
import Navbar from './components/Navbar.vue'
import AppMain from './components/AppMain.vue'
import usePermissionStore from '@/stores/permission'
import useAppStore from '@/stores/app'const appStore = useAppStore()
const permissionStore = usePermissionStore()
</script><style>
.container {height: 100%;.aside {height: 100%;}
}.el-header {--el-header-padding: 0;height: auto;
}
</style>
6.修改index.html
增加全局控制样式
<style>html,body,#app {height: 100%;margin: 0px;padding: 0px;}</style>
7.新建全局样式表src\assets\styles\index.scss
控制el-menu相关效果的属性
/* 覆盖所有折叠弹出菜单的背景色 */
.el-menu--popup {background-color: #304156 !important;/* 自定义背景色 */
}/* 覆盖悬停色 */
.el-menu--popup .el-menu-item:hover,
.el-menu--popup .el-sub-menu__title:hover {background-color: #1e0b39 !important;
}/* 默认文字颜色 */
.el-menu--popup .el-menu-item,
.el-menu--popup .el-sub-menu__title {color: #bfcbd9 !important;
}/* 隐藏折叠后的菜单文字 */
.el-menu--collapse .el-sub-menu__title span {display: none;
}/* 隐藏折叠后的展开箭头 */
.el-menu--collapse .el-sub-menu__icon-arrow {display: none;
}
在main.js文件里进行全局注册
import '@/assets/styles/index.scss'
二.功能验证
运行项目,浏览器访问http://localhost:5173/index,点击导航栏里的图标,折叠和展开的边栏
三.知识点拓展
1、Pinia 状态管理(核心知识点)
通俗理解:就像在项目中创建一个"共享记事本",所有组件都能读写这个记事本里的内容。
代码体现:
// 定义存储侧边栏状态的"记事本"
const useAppStore = defineStore('app', () => {const showSidebar = ref(false) // 折叠状态const sidebarWidth = ref(180) // 侧边栏宽度// 修改状态的"笔"const setSidebar = () => {showSidebar.value = !showSidebar.valuesidebarWidth.value = showSidebar.value ? 64 : 180}return { showSidebar, sidebarWidth, setSidebar }
})
应用场景:
• 在导航栏组件点击折叠按钮时调用 setSidebar
• 侧边栏组件通过 showSidebar
自动响应折叠状态
• 布局组件通过 sidebarWidth
调整侧边栏宽度
2、组件间通信
通俗理解:不同组件之间通过"共享记事本"(Pinia)传递信息,无需直接对话。
实现方式:
• Navbar 组件:触发状态修改
<el-icon @click="toggleSideBar"></el-icon>
• Sidebar 组件:自动响应状态变化
<el-menu :collapse="appStore.showSidebar">
3、响应式设计
通俗理解:当数据变化时,页面会自动像"镜子"一样实时反映变化。
技术实现:
ref()
创建响应式数据computed()
计算属性const activeMenu = computed(() => route.path)
实际效果:
• 点击折叠按钮 → 自动更新侧边栏宽度和菜单状态
• 路由变化 → 面包屑自动更新
4、组件化开发
通俗理解:把页面拆分成多个"积木块",每个积木独立且可复用。
组件结构:
• Navbar.vue
:顶部导航栏
• Sidebar.vue
:侧边栏容器
• MenuItem.vue
:递归渲染菜单项(可无限嵌套)
递归组件:
<!-- 菜单项组件自己调用自己 -->
<MenuItem v-for="child in item.children" :item="child" :level="level + 1"
/>
5、Element Plus 使用技巧
关键属性:
<el-menu :collapse="appStore.showSidebar" unique-opened
>
样式覆盖:
/* 修改 Element 默认样式 */
.el-menu--popup {background-color: #304156 !important;
}
折叠优化:
• 隐藏折叠后的文字和箭头
.el-menu--collapse .el-sub-menu__title span {display: none;
}
6、CSS 布局技巧
布局方案:
- 全屏布局
html, body, #app { height: 100% }
- Flex 布局
.parent {display: flex;align-items: center; }
- 过渡动画
.avatar-image {transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
7、最佳实践总结
- 状态集中管理:复杂状态交给 Pinia
- 组件解耦:每个组件专注单一功能
- 响应式思维:用数据驱动视图变化
- 样式分层:
• 组件样式用scoped
• 全局样式统一管理 - UI 框架深度定制:通过 CSS 覆盖实现个性化效果