《Vue 后台管理的优雅华章:小型实例鉴赏,2.0》
上一篇文章写了路由和登录组件以及登录账号密码定义的一些规则,接下来我将完善登录成功后面的页面,并且将路由中的细节写出来。
这将是我登录成功后的页面,比较简陋,不过后续会再加以完善。
状态文件
permiss.js
import { defineStore } from "pinia";
export const usePermissStore = defineStore("permiss", {
state: () => {
const defaultList = {
admin: [
"0",
"1",
"11",
"12",
"13",
"2",
"21",
"22",
"23",
"24",
"25",
"26",
"27",
"28",
"29",
"291",
"292",
"3",
"31",
"32",
"33",
"34",
"4",
"41",
"42",
"5",
"7",
"6",
"61",
"62",
"63",
"64",
"65",
"66",
],
user: ["0", "1", "11", "12", "13"],
};
const username = localStorage.getItem("ms_name");
return {
key: username == "admin" ? defaultList.admin : defaultList.user,
defaultList,
};
},
actions: {
handleSet(val) {
this.key = val;
},
},
});
这段代码使用 Pinia 的 defineStore 函数创建了一个名为 permiss 的存储。
在 state 部分:
-
定义了一个名为
defaultList的对象,其中包含了admin和user两种角色的权限列表。 -
通过
localStorage.getItem("ms_name")获取用户名,并根据用户名决定将defaultList.admin或defaultList.user赋值给key。
在 actions 部分:
- 定义了
handleSet方法,用于设置key的值。
sidebar.js
// sidebar 模块的共享状态
import { defineStore } from 'pinia';
// 一个文件就是一个状态模块
export const useSidebarStore = defineStore('sidebar', {
// state
state: () => {
return {
collapse: false
}
},
actions: {
// 状态的改变
handleCollapse() {
this.collapse = !this.collapse
}
}
})
这段代码使用 Pinia 的 defineStore 函数创建了一个名为 sidebar 的状态存储模块。
在 state 部分:
定义了一个名为 collapse 的状态,初始值为 false。这个状态可能用于表示侧边栏的展开或折叠状态。
在 actions 部分:
定义了一个名为 handleCollapse 的方法。当调用这个方法时,它会将 collapse 状态的值取反,实现侧边栏展开/折叠状态的切换。
上面两个js文件使用的是Pinia状态管理库,使用 defineStore 方法创建store。
Pinia 中的 store 包含 state(状态)、getters(类似于计算属性)和 actions(方法)等概念。例如,定义一个具有 getter 的 store:
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});
state存放的是状态,也可以理解我数据,getters(类似于计算属性)只计算,并不会改变值,actions(方法)主要就是用来修改值。
组件文件
用来呈存储数据
menu.js
export const menuData = [ { id: '0', title: '系统首页', index: '/dashboard', icon: 'Odometer', }, { id: '1', title: '系统管理', index: '1', icon: 'HomeFilled', children: [ { id: '11', pid: '1', index: '/system-user', title: '用户管理', }, { id: '12', pid: '1', index: '/system-role', title: '角色管理', }, { id: '13', pid: '1', index: '/system-menu', title: '菜单管理', }, ],
},
{
id: '2',
title: '组件',
index: '2-1',
icon: 'Calendar',
children: [
{
id: '21',
pid: '3',
index: '/form',
title: '表单',
},
{
id: '22',
pid: '3',
index: '/upload',
title: '上传',
},
{
id: '23',
pid: '2',
index: '/carousel',
title: '走马灯',
},
{
id: '24',
pid: '2',
index: '/calendar',
title: '日历',
},
{
id: '25',
pid: '2',
index: '/watermark',
title: '水印',
},
{
id: '26',
pid: '2',
index: '/tour',
title: '分布引导',
},
{
id: '27',
pid: '2',
index: '/steps',
title: '步骤条',
},
{
id: '28',
pid: '2',
index: '/statistic',
title: '统计',
},
{
id: '29',
pid: '3',
index: '29',
title: '三级菜单',
children: [
{
id: '291',
pid: '29',
index: '/editor',
title: '富文本编辑器',
},
{
id: '292',
pid: '29',
index: '/markdown',
title: 'markdown编辑器',
},
],
},
],
},
{
id: '3',
title: '表格',
index: '3',
icon: 'Calendar',
children: [
{
id: '31',
pid: '3',
index: '/table',
title: '基础表格',
},
{
id: '32',
pid: '3',
index: '/table-editor',
title: '可编辑表格',
},
{
id: '33',
pid: '3',
index: '/import',
title: '导入Excel',
},
{
id: '34',
pid: '3',
index: '/export',
title: '导出Excel',
},
],
},
{
id: '4',
icon: 'PieChart',
index: '4',
title: '图表',
children: [
{
id: '41',
pid: '4',
index: '/schart',
title: 'schart图表',
},
{
id: '42',
pid: '4',
index: '/echarts',
title: 'echarts图表',
},
],
},
{
id: '5',
icon: 'Guide',
index: '/icon',
title: '图标',
permiss: '5',
},
{
id: '7',
icon: 'Brush',
index: '/theme',
title: '主题',
},
{
id: '6',
icon: 'DocumentAdd',
index: '6',
title: '附加页面',
children: [
{
id: '61',
pid: '6',
index: '/ucenter',
title: '个人中心',
},
{
id: '62',
pid: '6',
index: '/login',
title: '登录',
},
{
id: '63',
pid: '6',
index: '/register',
title: '注册',
},
{
id: '64',
pid: '6',
index: '/reset-pwd',
title: '重设密码',
},
{
id: '65',
pid: '6',
index: '/403',
title: '403',
},
{
id: '66',
pid: '6',
index: '/404',
title: '404',
},
],
},
];
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="collapseChange">
<el-icon v-if="sidebarStore.collapse">
<Expand></Expand>
</el-icon>
<el-icon v-else>
<Fold></Fold>
</el-icon>
</div>
</div>
<div class="header-right">
<el-avatar class="user-avator" :size="30" :src="imgurl"></el-avatar>
<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>
<!-- 具名插槽 -->
<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>
<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 { useRouter } from "vue-router";
import { useSidebarStore } from "../store/sidebar.js";
import { ref, onMounted } from "vue";
const router = useRouter();
const sidebarStore = useSidebarStore();
const collapseChange = () => {
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) {
collapseChange();
}
});
</script>
<style lang="css" scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
width: 100%;
height: 70px;
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>
模板部分(<template>) :
-
整体布局为一个页面头部,包含左右两部分。
- 左边部分:依次展示了网站的 logo 图片、标题“后台管理系统”和一个用于控制侧边栏折叠状态的按钮。根据
sidebarStore.collapse的值,显示不同的图标。 - 右边部分:展示了用户的头像,以及一个点击触发的下拉菜单。下拉菜单中包含了链接和“退出登录”选项。
- 左边部分:依次展示了网站的 logo 图片、标题“后台管理系统”和一个用于控制侧边栏折叠状态的按钮。根据
-
事件处理:点击折叠按钮触发
collapseChange函数,下拉菜单的命令触发handleCommand函数。
脚本部分(<script setup>) :
-
引入了必要的模块和函数,包括路由
useRouter、侧边栏状态useSidebarStore以及 Vue 的ref和onMounted。 -
定义了以下变量和函数:
-
router:用于页面路由的操作。 -
sidebarStore:获取侧边栏的状态和操作方法。 -
collapseChange:调用侧边栏的折叠操作方法。 -
imgurl:通过ref定义的图片 URL 响应式数据。 -
username:从本地存储获取用户名,如果未获取到则默认为“游客”。 -
handleCommand:处理下拉菜单的命令,如果是“loginOut”,则清除本地存储的用户名并跳转到登录页面。 -
onMounted钩子:在组件挂载时,如果页面宽度小于 1500 像素,执行折叠侧边栏的操作。
-
样式部分(<style>) :
-
定义了头部相关元素的样式:
.header:整体头部的布局、宽度、高度、颜色、背景、边框等样式。.header-left:左侧部分的布局和内边距。.logo:logo 图片的宽度。.web-title:标题的边距和字体大小。.collapse-btn:折叠按钮的样式,包括布局、鼠标悬停效果等。.header-right:右侧部分的浮动和内边距。- 还定义了一些其他相关元素的样式,如用户头像、下拉菜单链接等。
补充:插槽
在模板21行开始使用了插槽,它可以被看作是子组件中预留的占位符,父组件可以在这些占位符中填充具体的内容,包括数据、HTML 代码、组件等。
插槽有三种:
默认插槽(或匿名插槽) :没有指定 name 属性的<slot>就是默认插槽。
具名插槽:带有 name 属性的<slot>即为具名插槽。
作用域插槽:是一种带有 props 数据的插槽。子组件通过在<slot>上绑定数据,将数据传递给父组件使用。
我使用的是具名插槽,插槽的使用方式如下:
子组件模板中定义插槽:
<template>
<div>
<!-- 具名插槽 -->
<slot name="content">我是插槽默认的内容,当父组件不填充任何内容时,我这句话才会出现</slot>
</div>
</template>
父组件中使用子组件并填充插槽内容:
<template>
<div>
<child>
<template v-slot:content>
<div>这是父组件在子组件中填充的内容,在子组件中显示</div>
<img src="https://s3.ax1x.com/2021/01/16/srjlq0.jpg" alt=""/>
</template>
</child>
</div>
</template>
sidebar.vue
<template>
<aside class="sidebar">
<el-menu
class="sidebar-el-menu"
:collapse="sidebar.collapse"
background-color="#324157"
text-color="#bfcbd9"
:defalut-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 { useRoute } from "vue-router";
import { useSidebarStore } from "../store/sidebar";
import { menuData } from "./menu";
const sidebar = useSidebarStore();
const onRoutes = () => {
return route.path;
};
</script>
模板部分(<template>) :
-
定义了一个侧边栏
<aside class="sidebar">,其中使用了el-menu组件来构建菜单结构。-
:collapse="sidebar.collapse"根据sidebar状态决定菜单是否折叠。 -
设置了背景颜色和文字颜色。
-
通过
:default-active="onRoutes"与路由关联,以确定默认选中的菜单项。 -
使用
router属性启用路由模式。 -
通过
v-for循环遍历menuData来动态生成菜单项和子菜单项。 -
根据菜单项是否有子项,分别使用
el-sub-menu或el-menu-item来展示。对于有子项的菜单项,还会进一步嵌套子菜单项的生成。
-
脚本部分(<script setup>) :
- 引入了
useRoute用于获取当前路由信息,useSidebarStore用于获取和操作侧边栏的状态。 - 引入了
menuData用于提供菜单的数据。 sidebar = useSidebarStore()获取侧边栏的状态。onRoutes函数用于返回当前路由的路径,以便与菜单的默认选中状态关联。
src文件
element-user.js
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElLink,
ElIcon,
ElAvatar,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ElMenu,
ElSubMenu,
ElMenuItem
} from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// console.log(ElementPlusIconsVue);
const components = [ElButton, ElForm,
ElFormItem, ElInput, ElCheckbox,
ElLink, ElIcon, ElAvatar, ElDropdown,
ElDropdownMenu, ElDropdownItem, ElMenu,
ElSubMenu, ElMenuItem];
export default (app) => {
components.forEach((component) => {
app.use(component);
})
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
以下是对代码的详细解释:
首先,引入了 Element Plus 中的一系列组件,如 ElButton、ElForm 等。
然后,定义了一个包含这些组件的数组 components 。
在 export default 导出的函数中,通过遍历 components 数组,使用 app.use(component) 来注册这些组件。
同时,通过遍历 ElementPlusIconsVue 对象的键值对,使用 app.component(key, component) 来注册其中的图标组件。
这样做的目的是让整个应用能够方便地使用 Element Plus 提供的组件和图标,提高开发效率和代码的复用性。
main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 凤梨
// 引入Vue组件库 70%的组件有组件库提供了
// 组件库依赖的样式
import 'element-plus/dist/index.css'
import './assets/styles/variable.css'
import App from './App.vue'
import ElementUse from './element-use.js'
import router from './router/index.js'
import * as DemoData from './test'
// console.log(DemoData);
// console.log(Object.entries(DemoData));
const app = createApp(App);
app.use(createPinia());
ElementUse(app);
app.use(router);
import { usePermissStore } from './store/permiss.js'
const permissStore = usePermissStore();
// 自定义指令
app.directive('permiss', {
// el dom , binding 绑定的属性
mounted(el, binding) {
if (binding.value && !permissStore.key.includes(String(binding.value))) {
el['hidden'] = true;
}
}
})
app.mount('#app');
这段 Vue 代码主要完成了以下几个重要的操作:
-
引入必要的模块和资源:
- 从
'vue'导入createApp函数来创建 Vue 应用实例。 - 从
'pinia'导入createPinia用于状态管理。 - 引入
Element Plus组件库的样式和自定义的样式文件。 - 引入根组件
App.vue、路由配置router以及一个用于注册Element Plus组件的模块ElementUse。 - 引入一些测试数据
DemoData。
- 从
-
创建和配置 Vue 应用:
- 使用
createApp(App)创建应用实例并赋值给app变量。 - 使用
app.use(createPinia())启用Pinia状态管理。 - 调用
ElementUse(app)注册Element Plus组件。 - 使用
app.use(router)应用路由配置。
- 使用
-
定义自定义指令
'permiss':- 在指令的
mounted钩子函数中,根据条件判断是否隐藏元素。如果binding.value存在且不在permissStore.key数组中,就将元素隐藏。
- 在指令的
总结
总的来说, 这个组件通过巧妙的模板和脚本结合,实现了侧边栏的灵活构建和与路由的集成,为用户提供了一个动态且功能丰富的侧边栏菜单体验。