# Vue2 通用后台管理系统项目实战

​ 本项目是纯 vue 的练手项目,数据的测试主要用 mock 来实现,适合 vue 初学者的练习,需要的技术栈有

1
2
3
4
5
6
7
echarts
vuex
vue-router
element-ui
axios
mock.js
Cookiejs

# 1. 环境的搭建

# 1.1 项目初始化

在 vue-cli 搭建脚手架时选择自定义安装

1
2
3
4
5
6
7
Vue CLI v5.0.8
Failed to check for updates
? Please pick a preset:
SaveRouteVuex2.0 ([Vue 2] babel, router, vuex, eslint)
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
> Manually select features

选择自带 vuex 与 vue-router

1
2
3
4
5
6
7
8
9
10
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
>(*) Vuex
( ) CSS Pre-processors
(*) Linter / Formatter
( ) Unit Testing
( ) E2E Testing

根据需求自定义安装

1
2
3
4
5
6
7
8
9
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 2.x //vue2
? Use history mode for router? (Requires proper server setup for index fallback in production) No //是否使用history在路由里
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? Yes
? Save preset as: test

# 1.2 引入 element-ui

1
npm i element-ui -S

安装完成后在 main.js 中导入并全局引用,同时不要忘记引入样式

1
2
3
4
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

# 1.3 引入 echarts

1
npm install echarts --save

main.js 中全局引入 echarts

1
2
3
import * as echarts from 'echarts'

Vue.prototype.$echarts = echarts//如果报错可以使用

# 1.4 引入 sass

1
2
npm i node-sass
npm i sass-loader

# 2. 主要版型搭建过程的一些难点

# 2.1 路由视图的引用

由于我们在路由写入时将 home 组件设为‘/’根路由,所以在 router-view 的使用时我们要在 home 和 APP 中同时写入 router-view 防止页面出现问题

# 2.2 左侧导航收入功能

​ 由于左侧导航的收入按钮在头部组件当中,而控制是否收回的属性值 :collapse 位于 leader 组件中的 el-menu 中,所以我们将控制是否收回的布尔值 isCollapse 创建在 vuex 中

1
2
3
4
5
6
7
8
9
state: {
isCollapse: false,//控制左侧导航栏展开还是收缩,
},
mutations:{
changeIC(state,value) {
// console.log(value);
state.isCollapse=!state.isCollapse
},
}

​ 创建完控制收回的函数后,将其绑定在 header 组件的头部按钮上

1
2
3
4
5
methods:{
changeIC(){
this.$store.commit('changeIC',1)
},
}

在收入过程中,左侧的 h4 标签的字可以用三元表达式来动态变化

1
<h4>{{isCollapse?"后台":"通用后台管理系统"}}</h4>

# 2.3 数据的动态引入与图表的动态变化

左侧导航栏有分主路由与子路由,此时要动态引入我们就需要对拿到的数据进行第一层判断

1
2
3
4
5
6
7
8
noChildren(){
return this.menuData.filter(item=>!item.children)
},

//有子菜单
hasChildren(){
return this.menuData.filter(item=>item.children)
},

判断结束后再在 div 中进行 v-for 循环遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<el-menu-item @click='clickMenu(item)' v-for="item in noChildren" :key='item.name' :index='item.name'>
<i :class="`el-icon-${item.icon}`"></i> //采用`拼接icon来导入icon
<span slot="title">{{item.label}}</span>
</el-menu-item>
<el-submenu v-for="item in hasChildren" :key='item.label' :index='item.label'>
<template slot="title">
<i class="`el-icon-${item.icon}`"></i>
<span>{{item.label}}</span>
</template>
<el-menu-item-group v-for="mitem in item.children" :key="mitem.name">
<el-menu-item @click='clickMenu(mitem)' :index="mitem.path">
<i :class="`el-icon-${mitem.icon}`"></i>
<span>{{mitem.label}}</span>
</el-menu-item>
</el-menu-item-group>
</el-submenu>

# 2.4 面包屑与底下标签联动效果

1666760987101

由于要分两个组件来放置,故我们依然采用 store 中的 state 来储存 tab 数组(即标签页数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tabsList: [
{
path: '/homePage',
name: 'homePage',
label: '首页',
icon: 's-home',
url: 'Home/Home'
},

],

changeTabs(state, val) {
if (val.name !== 'homePage') {
const index = state.tabsList.findIndex(item => item.name === val.name)
if (index === -1) {
state.tabsList.push(val)
}
}
},

tabsList 固定包含一个首页标签,故首页标签写死存在于 state 中

顶部面包屑不包含删除标签功能,故实行起来比较简单

1
2
3
4
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="item in tags" :key="item.name" :to="{path:item.path}">{{item.label}}</el-breadcrumb-item>
</el-breadcrumb>
//to为指向路由的名字

而下方的标签则要相对复杂一些

1
2
3
4
5
6
7
8
9
<el-tag
v-for="(item,index) in items"
:key="item.label"
@click="changeMenu(item)"
:closable="item.name!=='homePage'"
@close="closeMenu(item,index)"
:effect="$route.name===item.name?'dark':'plain'">
{{ item.label }}
</el-tag>

​ click 为点击事件,close 为删除标签的动作,同时需要考虑一个问题:如果删除的标签是当前指向的标签,那么删除以后的动作该如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 methods:{
changeMenu(item){
if(this.$route.path!==item.path&&!(this.$router.path==='/homePage'&&item.path==='/')){
this.$router.push({
name:item.name
})
}
},
closeMenu(item,index){
this.$store.commit("closeTabs",item)
let length = this.items.length;
// console.log(length);
if(item.name!==this.$route.name){
return
}
if(length===index){
this.$router.push({
name:this.items[index-1].name
})
}
if(index<length){
this.$router.push({
name:this.items[index].name
})
}
}
},

​ 在这里,我选择了删除本标签,如果右侧还有标签,则默认跳转至右侧标签,如果右侧没有,则默认跳转至左侧标签。

​ 故删除标签的函数需要获取到标签当前的 index 即下标值,再进行判断后跳转

# 3 home 组件内部搭建的一些难点

# 3.1 echarts 的使用

​ 在 echarts 的使用过程中,我遇到了一些问题。echarts 的数据是从 mock 中动态引入的,而我将 echarts 搭建的过程放在了 mounted 生命周期中,但是如果我先获取 mock 中的数据再放入图表中,图表内的数值会刷不出来。所以在这个过程中我直接将 echarts 表格的数据获取和搭建全放在了 api 的 getData 函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mounted() {
getData().then(({data})=>{
const {userData}=data.data;
this.userData=userData;
console.log(userData);
let myChart3 = this.$echarts.init(document.getElementById("myChart3"));
const eachrts3Option = {
legend: {
// 图例文字颜色
textStyle: {
color: "#333",
},
},
tooltip: {
trigger: "axis",
},
xAxis: {
type: 'category',
data: userData.map(item => item.date),
},
yAxis: {
type: 'value'
},
series: [
{
data: userData.map(item=>item.new),
type: 'bar',
},
{
data: userData.map(item=>item.active),
type: 'bar',
color:'skyblue'
}
]
}
myChart3.setOption(eachrts3Option)
})
},

# 4 login 组件的难点

# 4.1 登录时本地储存与路由守卫

​ 为了防止出现还未登录就通过路由跳转实现进入的行为,我们需要在登陆时将用户信息加入本地储存同时通过路由守卫的方式来阻止。

login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
submit(){
this.$refs.form.validate((valid) => {
if (valid) {
getMenu(this.form).then(({ data }) => {
console.log(data)
if (data.code === 20000) {
// token信息存入cookie用于不同页面间的通信
Cookie.set('token', data.data.token)
// console.log("data的数据",data.username);
localStorage.setItem('username',data.username)
localStorage.setItem('power',data.power)
localStorage.setItem('menu',JSON.stringify(data.data.menu))
this.$store.commit('changeMenu',JSON.stringify(data.data.menu))
this.$store.commit('changeRouter',this.$router)
// 获取菜单的数据,存入store中
// this.$store.commit('setMenu', data.data.menu)
// this.$store.commit('addMenu', this.$router)
// 跳转到首页
console.log(this.$route);
this.$router.push('/homePage')
} else {
this.$message.error(data.data.message);
}
})
}
})
}

router.js

1
2
3
4
5
6
7
8
9
10
11
//添加路由守卫
router.beforeEach((to,from,next) => {
let token = Cookie.get('token')
if (!token && to.name !== 'login') {
next({name:'login'})
} else if (token && to.name === 'login') {
next({name:'homePage'})
} else {
next()
}
})

# 4.2 不同用户登录进入不同的系统

​ 因为不同的用户权限不同,所以我们需要在后台中将不同用户的菜单也输入并匹配,并在 leader 组件中配置。同时因为如果储存在 store 的 state 中在刷新后会丢失,同时也要将 menu 放入本地储存中

1
2
3
menuData(){
return JSON.parse(localStorage.getItem('menu'))||this.$store.state.tab.menu
},

​ 但是在这个情况下我们仍然可以通过路由跳转进入到不属于自己权限的页面,所以在路由的写入时也需要动态引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
changeRouter(state, router) {
if(!localStorage.getItem('menu')) return
const menu = JSON.parse(localStorage.getItem('menu'))
state.menu = menu
const MenuData = []
menu.forEach(item => {
if (item.children) {
item.children=item.children.map(item => {
item.component = () => import(`../views/${item.url}`)
return item
})
console.log('item.children',item.children);
MenuData.push(...item.children)
} else {
item.component = () => import(`../views/${item.url}`)
MenuData.push(item)
}
// console.log('MenuData', MenuData);
// localStorage.setItem('MenuData', JSON.stringify(MenuData))
// console.log('router',router);
MenuData.forEach(item => {
router.addRoute('Home',item)
})
});
}

​ 但是在 vue2 中我们没有 router 的 remove 函数,所以我们需要在退出按钮添加一个刷新的函数以清空写入的路由,为下一次登录做准备

1
location.reload()

完成。

项目地址:IRON/vue 通用后台 (gitee.com)