使用AI一步一步实现若依前端(12)
功能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.value
if (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).href
import 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.value
sidebarWidth.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 覆盖实现个性化效果