Vue 实战——后台管理系统(二)
引言
基本主页框架
我们从创建主页的基本结构开始。这涉及设置主要布局,并整合诸如 Header.vue
和 Sidebar.vue
等子组件。
首先我们先写一个主页的基本框架。
Home.vue
<template>
<div class="wrapper">
<v-header />
<v-sidebar />
<main class="content-box" :class="{'content-collapse':sidebarStore.Collapse}">
<router-view />
</main>
</div>
</template>
<script setup>
import vHeader from '../components/header.vue';
import vSidebar from '../components/sidebar.vue';
// 转译成了 v-header
import { useSidebarStore } from '../store/sidebar';
const sidebarStore = useSidebarStore();
</script>
<style >
/* 没 scoped 就是全局样式 */
.wrapper{
height: 100vh;
overflow: hidden;
}
.content-box {
position: absolute;
left: 250px;
right: 0;
top: 70px;
bottom: 0;
padding-bottom: 30px;
-webkit-transition: left 0.3s ease-in-out;
transition: left 0.3s ease-in-out;
background: #eef0fc;
overflow: hidden;
}
.content {
width: auto;
height: 100%;
padding: 20px;
overflow-y: scroll;
box-sizing: border-box;
}
.content::-webkit-scrollbar {
width: 0;
}
.content-collapse {
left: 55px;
}
</style>
这段代码展示了一个基本的主页布局,包括了 Header
和 Sidebar
组件,用于去渲染头部栏和侧边导航栏,以及一个主要内容区域用于展示路由视图。
设计头部组件
header.vue
是一个包含网站头部的 Vue 组件,主要功能包括显示 logo、标题、用户头像、下拉菜单等。
首先,我们在 components
文件夹下创建一个头部组件 header.vue
,代码如下:
<template>
<header class="header">
<div class="header-left">
<img src="../assets/images/logo.svg" alt="" class="logo">
<div class="web-title">后台管理系统</div>
<div class="collapse-btn" @click="collaseChange">
<el-icon v-if="sidebarStore.Collapse">
<Expand />
</el-icon>
<el-icon v-else>
<Fold />
</el-icon>
</div>
</div>
<div class="header-right">
<el-avatar class="user-avator" :size="30" :src="imgurl" />
<el-dropdown class="user-name" trigger="click"
@command="handleCommand">
<!-- 默认插槽 -->
<span class="el-dropdown-link">
{{ username }}
<el-icon class="el-icon-right">
<arrow-down />
</el-icon>
</span>
<!-- 具名插槽 -->
<!-- 定位到 dropdown里 -->
<template #dropdown>
<el-dropdown-menu>
<a href="https://github.com/linxin/vue-manage-system" target="_black">
<el-dropdown-item>项目仓库</el-dropdown-item>
</a>
<a href="https://github.com/linxin/vue-manage-system" target="_black">
<el-dropdown-item>官方文档</el-dropdown-item>
</a>
<el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
</template>
<script setup>
import { useSidebarStore } from '../store/sidebar.js'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const sidebarStore = useSidebarStore()
const collaseChange = () => {
sidebarStore.handleCollapse()
}
const imgurl = ref('https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png')
const username = localStorage.getItem('ms_name') || ''
const handleCommand = (command) => {
if(command = 'loginout'){
localStorage.removeItem('ms_name');
router.push('/login')
}
}
onMounted(() => {
if(document.body.clientWidth < 1500){
collaseChange()
}
})
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
width: 100%;
height: 70px;
/* css变量 CSS4新语法 方便后面修改 */
color: var(--header-text-color);
background-color: var(--header-bg-color);
border-bottom: 1px solid #ddd;
}
.header-left {
display: flex;
align-items: center;
padding-left: 20px;
height: 100%;
}
.logo {
width: 35px;
}
.web-title {
margin: 0 40px 0 10px;
font-size: 22px;
}
.collapse-btn {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 0 10px;
cursor: pointer;
opacity: 0.8;
font-size: 22px;
}
.collapse-btn:hover {
opacity: 1;
}
.header-right {
float: right;
padding-right: 50px;
}
.header-user-con {
display: flex;
height: 70px;
align-items: center;
}
.btn-fullscreen {
transform: rotate(45deg);
margin-right: 5px;
font-size: 24px;
}
.btn-icon {
position: relative;
width: 30px;
height: 30px;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
color: var(--header-text-color);
margin: 0 5px;
font-size: 20px;
}
.btn-bell-badge {
position: absolute;
right: 4px;
top: 0px;
width: 8px;
height: 8px;
border-radius: 4px;
background: #f56c6c;
color: var(--header-text-color);
}
.user-avator {
margin: 0 10px 0 20px;
}
.el-dropdown-link {
color: var(--header-text-color);
cursor: pointer;
display: flex;
align-items: center;
}
.el-dropdown-menu__item {
text-align: center;
}
</style>
在头部栏主要就两个版块:
-
Collapse 按钮:用于切换侧边栏的展开/收起状态。
-
用户信息:显示用户头像和名称,并提供下拉菜单,允许用户访问项目仓库、官方文档和退出登录。
而用户信息部分主要是用了两种插槽去实现。 默认插槽 和 具名插槽。
默认插槽
默认插槽定义
默认插槽是最基础的插槽类型,它允许父组件向子组件传递内容。子组件没有指定名称的插槽部分,接收的内容会被渲染在子组件中预留的位置。
在项目中的使用
默认插槽里插入下拉菜单的触发元素(用户名称和箭头图标)
<el-dropdown class="user-name" trigger="click" @command="handleCommand">
<span class="el-dropdown-link">
{{ username }}
<el-icon class="el-icon-right">
<arrow-down />
</el-icon>
</span>
</el-dropdown>
其中 span
标签中的 username
和 el-icon
便是插槽里的元素,也是显示在头部栏的。
具名插槽
具名插槽定义
具名插槽允许你定义多个插槽,并为每个插槽指定一个名称。在父组件中,你可以通过插槽名称来插入特定内容到子组件中的指定位置。
在项目中的使用
具名插槽用于定义下拉菜单的内容。#dropdown
是具名插槽的名称,它会插入到 el-dropdown
组件的特定位置:
<template #dropdown>
<el-dropdown-menu>
<a href="https://github.com/linxin/vue-manage-system" target="_black">
<el-dropdown-item>项目仓库</el-dropdown-item>
</a>
<a href="https://github.com/linxin/vue-manage-system" target="_black">
<el-dropdown-item>官方文档</el-dropdown-item>
</a>
<el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
将具体内容(项目仓库、官方文档、退出登录)插入到下拉菜单,需要点击下拉菜单才能展开看到。
且我们在 el-dropdown
组件上,使用了 @command
事件监听器来监听菜单项的点击事件,并将其传递给 handleCommand
方法。
<el-dropdown class="user-name" trigger="click" @command="handleCommand">
在 handleCommand
方法中,根据传递的 command
值来执行特定的逻辑。在这个案例中,handleCommand
方法检查 command
是否等于 'loginout'
,如果是,则执行退出登录操作。
const handleCommand = (command) => {
if (command === 'loginout') {
localStorage.removeItem('ms_name'); // 移除存储的用户信息
router.push('/login'); // 重定向到登录页面
}
}
侧边栏状态
我们在头部栏还设计了一个 Collapse
按钮。用于切换侧边栏的展开/收起状态。
我们设计了一个切换状态的函数 collaseChange
,并将其绑定成了按钮的响应事件,当点击后会调用我们存在 sidebar.js
仓库中的改变状态的函数,去改变全局的状态。
<script setup>
import { useSidebarStore } from '../store/sidebar.js'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const sidebarStore = useSidebarStore()
const collaseChange = () => {
sidebarStore.handleCollapse()
}
const imgurl = ref('https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png')
const username = localStorage.getItem('ms_name') || ''
const handleCommand = (command) => {
if (command === 'loginout') {
localStorage.removeItem('ms_name');
router.push('/login')
}
}
onMounted(() => {
if (document.body.clientWidth < 1500) {
collaseChange()
}
})
</script>
这里有一点值得注意,我们用了钩子函数,在挂载后就去判断客户端的屏幕宽度是否小于设定的值,小于则把侧边栏设为折叠状态。做前端页面这些细节很重要!
数据管理
我们在store目录下再新建一个 sidebar.js
去存放侧边栏的状态。
我们使用 defineStore
来创建一个 Pinia 状态管理模块。这个模块包含了侧边栏的状态和操作方法:
// sidebar 模块的共享状态
import { defineStore } from 'pinia'
// 一个文件就是一个状态模块
export const useSidebarStore = defineStore('sidebar',{
// 有点类似计算属性
state:() =>{
return {
Collapse: false
}
},
actions:{
// 状态的改变
handleCollapse(){
this.Collapse =!this.Collapse
}
}
})
我们定义了一个代表侧边栏展开或收缩的状态,handleCollapse
方法用于切换 Collapse
状态,便于在 header.vue
组件中去调用改变侧边栏展开或收缩状态。
设计侧边栏
sidebar.vue
组件负责渲染侧边栏的 UI,包括菜单项和子菜单。它通过 Pinia 状态管理模块来控制侧边栏的折叠和展开状态。以下是该组件的详细讲解:
<template>
<aside class="sidebar">
<el-menu
class="sidebar-el-menu"
:collapse="sidebar.collapse"
background-color="#324157"
text-color="#bfcbd9"
:default-active="onRoutes"
router
>
<template v-for="item in menuData">
<template v-if="item.children">
<el-sub-menu :index="item.index" :key="item.index" v-permiss="item.id">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.title }}</span>
</template>
<template v-for="subItem in item.children">
<el-sub-menu
v-if="subItem.children"
:index="subItem.index"
:key="subItem.index"
v-permiss="item.id"
>
<template #title>{{ subItem.title }}</template>
<el-menu-item
v-for="(threeItem, i) in subItem.children"
:key="i"
:index="threeItem.index"
>
{{ threeItem.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="subItem.index" v-permiss="item.id">
{{ subItem.title }}
</el-menu-item>
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="item.index" :key="item.index" v-permiss="item.id">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</template>
</el-menu>
</aside>
</template>
<script setup>
import { useSidebarStore } from '../store/sidebar'
import { menuData } from './menu'
import {useRoute} from 'vue-router'
const sidebar = useSidebarStore()
const onRoutes = () =>{
return route.path
}
</script>
我们用 <el-menu>
Element Plus 提供的菜单组件,用于构建侧边栏。
菜单组件
:collapse="sidebar.collapse"
绑定 collapse
属性,以控制菜单的折叠状态。这个属性从 Pinia 状态管理模块 sidebar
中获取。:default-active="onRoutes"
绑定当前激活的菜单项,通过 onRoutes
函数获取当前路由路径,用于高亮显示当前选中的菜单项。
菜单项渲染
递归渲染是一种常见的编程模式,特别是在处理树状数据结构时。在这个侧边栏组件中,我们通过递归方式来渲染多级菜单项。
首先我们解构 menu.js
中的存放菜单数据的数组 menuData
。
初始模板遍历
<template v-for="item in menuData">
<template v-if="item.children">
<el-sub-menu :index="item.index" :key="item.index" v-permiss="item.id">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.title }}</span>
</template>
<template v-for="subItem in item.children">
<!-- 递归渲染子菜单项 -->
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="item.index" :key="item.index" v-permiss="item.id">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</template>
在上述代码中,首先通过 v-for
指令遍历 menuData
数组,对每个菜单项 item
进行渲染。
判断子菜单
<template v-if="item.children">
<el-sub-menu :index="item.index" :key="item.index" v-permiss="item.id">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.title }}</span>
</template>
<template v-for="subItem in item.children">
<!-- 递归渲染子菜单项 -->
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="item.index" :key="item.index" v-permiss="item.id">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
-
<template v-if="item.children">
:如果当前菜单项item
有子菜单,则渲染el-sub-menu
组件,并递归遍历其子菜单item.children
。 -
<template v-else>
:如果当前菜单项item
没有子菜单,则直接渲染el-menu-item
组件。
递归渲染子菜单项
<template v-for="subItem in item.children">
<template v-if="subItem.children">
<el-sub-menu :index="subItem.index" :key="subItem.index" v-permiss="item.id">
<template #title>{{ subItem.title }}</template>
<el-menu-item v-for="(threeItem, i) in subItem.children" :key="i" :index="threeItem.index">
{{ threeItem.title }}
</el-menu-item>
</el-sub-menu>
</template>
<el-menu-item v-else :index="subItem.index" v-permiss="item.id">
{{ subItem.title }}
</el-menu-item>
</template>
-
<template v-for="subItem in item.children">
:在el-sub-menu
内部,再次通过v-for
指令遍历item.children
,并对每个子菜单项subItem
进行渲染。 -
<template v-if="subItem.children">
:如果当前子菜单项subItem
还有子菜单,则渲染嵌套的el-sub-menu
组件,并递归遍历其子菜单subItem.children
。 -
<template v-else>
:如果当前子菜单项subItem
没有子菜单,则直接渲染el-menu-item
组件。
选择性渲染有权限菜单
值得留意的是我们在这里用了 v-permiss
。v-permiss
是一个自定义指令,用于控制菜单项的显示与否,具体取决于用户的权限。
我们一般在主函数中定义并且注册。
app.directive('permiss', {
mounted(el, binding) {
if (binding.value && !permissStore.key.includes(String(binding.value))) {
el['hidden'] = true
}
}
})
这里定义了自定义指令 v-permiss
,用于控制元素的显示与否。该指令在元素被挂载到 DOM 上时触发,通过检查绑定值(权限 ID)和用户权限列表,如果用户没有相应权限,则将元素隐藏。
主函数
完整的主函数如下
import { createApp } from 'vue'
import {createPinia} from 'pinia'
// 引入Vue组件库 70%的组件有组件库提供了
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElLink,
ElIcon,
ElAvatar,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ElMenu,
ElMenuItem,
ElSubMenu
} from 'element-plus'
// 组件库依赖的样式
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
// * as 引入全部组件库
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './assets/styles/variable.css'
console.log(ElementPlusIconsVue,'///');
const app = createApp(App)
// 注册全部组件库
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
// app.component()这个函数需要的两个参数是 key 和 component ,用entries 将对象拆分成这样的格式去注册
}
app
.use(createPinia())
.use(router)
.use(ElButton)
.use(ElForm)
.use(ElFormItem)
.use(ElInput)
.use(ElCheckbox)
.use(ElLink)
.use(ElIcon)
.use(ElAvatar)
.use(ElDropdown)
.use(ElDropdownMenu)
.use(ElDropdownItem)
.use(ElMenu)
.use(ElMenuItem)
.use(ElSubMenu)
// 自定义指令
import { usePermissStore } from './store/permiss'
const permissStore = usePermissStore();
app.directive('permiss',{
// v-if v-show el 承载指令的节点 binding 绑定的属性,就是传递的值
mounted(el,binding){
if(binding.value && !permissStore.key.includes(String(binding.value))){
el['hidden'] = true
}
}
})
app
.mount('#app')
此外主函数还有些特色之处。
在项目的 main.js
中,可以看到以下两种引入方法的应用:
引入全部组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
-
import * as ... from ''
是一种将模块中所有导出内容一次性引入的方式。 -
这种方法将
@element-plus/icons-vue
模块中所有的图标组件都导入到ElementPlusIconsVue
对象中。这个对象的每个属性都是一个图标组件,属性名是组件的名称。
逐个解构引入
import { ElButton, ElForm, ElFormItem, ElInput, ElCheckbox, ElLink, ElIcon, ElAvatar, ElDropdown, ElDropdownMenu, ElDropdownItem, ElMenu, ElMenuItem, ElSubMenu } from 'element-plus'
-
这种方法用于从
element-plus
模块中按需引入特定的组件。 -
我们逐个指定了要引入的组件,如
ElButton
、ElForm
等,这种方法可以避免引入不必要的组件,减少最终打包后的文件体积。
Object.entries
我们还使用了
Object.entries
,Object.entries
是 ES6 新增的一个方法,用于遍历对象。它返回一个数组,数组中的每个元素都是一个 [key, value]
数组,代表对象的键值对。
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
- 这里的
Object.entries
被用来将ElementPlusIconsVue
对象中的每个图标组件遍历出来。 for...of
循环配合Object.entries
使得可以将每个[key, component]
对作为参数传递给app.component
方法,从而在 Vue 应用中全局注册所有的图标组件。
这些亮点可以丰富你的项目,且很好地体现出了对八股文的掌握。
总结
在本篇文章中,我们对后台管理系统的首页布局进行了详细讲解,并实现了以下几个关键点:
-
基本主页框架:
- 我们首先创建了
Home.vue
组件,定义了主页的基本结构。这个结构包括Header
和Sidebar
组件,以及一个主要内容区域来展示路由视图。
- 我们首先创建了
-
头部组件设计:
header.vue
组件包括了网站的 logo、标题、用户头像和下拉菜单。我们使用了默认插槽和具名插槽来实现用户信息部分的灵活展示,同时设计了一个切换侧边栏状态的按钮,并通过 Pinia 管理侧边栏的折叠状态。
-
侧边栏组件设计:
sidebar.vue
组件负责渲染侧边栏的菜单项和子菜单。我们通过递归渲染多级菜单项,并利用 Pinia 状态管理模块来控制侧边栏的折叠和展开状态。
-
数据管理:
- 我们在
store
目录下创建了sidebar.js
文件,使用 Pinia 管理侧边栏的状态,并通过defineStore
方法创建了一个状态模块来控制侧边栏的折叠状态。
- 我们在
通过本篇文章,我们不仅学习了如何设计和实现一个高效的后台管理系统首页,还深入了解了 Vue 的组件化思想和状态管理的最佳实践。希望这些内容能帮助你构建更加优雅和功能丰富的后台管理系统。如果这篇文章对你有帮助可以点个赞哦😊。
转载自:https://juejin.cn/post/7397274001066213385