项目简介
实现功能
用VUE框架搭建一个通用后台管理系统,组件化、模块化实现用户登陆、后台管理、页面显示和跳转等功能。
技术栈
参考文档
vue通用后台管理系统(一)
vue通用后台管理系统(二)
vue通用后台管理系统(三)
vue通用后台管理系统(四)
实现流程
环境配置
Vue安装及环境配置、开发工具_vue开发工具
创建项目
查看环境配置
创建项目
创建:vue create my-app
更改命名规范设置
导入Element-UI
官网网址
Element - 网站快速成型工具
安装
注意版本的兼容性,这里选择安装2.15.13版本
导入
//导入Element-UI
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI)
测试
less
cnpm i less@4.1.2 npm i less-loader@6.0.0
Vuex
安装:cnpm i vuex@3.6.2
Anxios
安装:cnpm install axios@0.27.2
Echarts
安装:cnpm i echarts@5.1.2
Vue-Router
官网
Vue Router
安装
cnpm i vue-router@3.6.5
测试
创建相关文件目录文件和组件
router文件下的index.js代码
import Vue from 'vue'
import VueRouter from 'vue-router'
//导入组件
import Home from '../views/Home.vue'
import User from '../views/User.vue'
//使用vuerouter
Vue.use(VueRouter)
//路由与组件映射
const routes = [
{ path: '/home', component: Home },
{ path: '/user', component: User }
]
//创建router实例
const router = new VueRouter({
routes,
})
//对外暴露路由
export default router
在main.js中进行导入和挂载
![[屏幕截图 2023-12-04 145945.png]]
//导入router
import router from './router'
new Vue({
//挂载router
router,
render: h => h(App),
}).$mount('#app')
在需要渲染的地方(App.vue)添加代码
<router-view></router-view>
嵌套路由
创建Main组件并在router的index.js文件中修改路由
import Main from '../views/Main.vue'
const routes = [
{
path: '/',
component: Main,
children: [
{ path: 'home', component: Home },
{ path: 'user', component: User }
]
}
]
在Main组件中设置路由出口
Container布局容器
结构显示框图
Main.vue代码
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
左侧菜单栏的引入
创建CommonAside组件
将ElementUI中的导航菜单导入进来,注意一个容器内只允许容纳一个ElementUI标签。
在Main组件中导入并渲染
根据menuData对菜单栏进行调整
在CommonAside组件中导入menuData数据
menuData:
[
{
path: '/',
name: 'home',
label: '首页',
icon: 's-home',
url: 'Home/Home'
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'video-play',
url: 'MallManage/MallManage'
},
{
path: '/user',
name: 'user',
label: '用户管理',
icon: 'user',
url: 'UserManage/UserManage'
},
{
label: '其他',
icon: 'location',
children: [
{
path: '/page1',
name: 'page1',
label: '页面1',
icon: 'setting',
url: 'Other/PageOne'
},
{
path: '/page2',
name: 'page2',
label: '页面2',
icon: 'setting',
url: 'Other/PageTwo'
}
]
}
]
计算属性分类menuData
computed: {
//没有子菜单
noChildren() {
return this.menuData.filter(item => !item.children)
},
//有子菜单
hasChildren() {
return this.menuData.filter(item => item.children)
},
}
对menuData中的对象进行遍历的代码
<template>
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#454545"
text-color="#ffffff"
active-text-color="#00ffff">
<!-- 对menuData中无子节点的对象进行遍历 -->
<el-menu-item v-for="item in noChildren" :key="item.name" :index="item.name">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<!-- 对menuData中有子节点的对象进行遍历 -->
<el-submenu v-for="item in hasChildren" :key="item.name" :index="item.name">
<template slot="title">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</template>
<!-- <el-menu-item v-for="subItem in item.children" :key="subItem.path" :index="subItem.path">{{ subItem.label }}</el-menu-item> -->
<el-menu-item-group v-for="subItem in item.children" :key="subItem.path">
<el-menu-item :index="subItem.path">{{ subItem.label }}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</template>
样式调整
CommonAside.vue
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu{
border-right: none;
height: 100vh;
//less样式
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400;
}
}
</style>
Main.vue
<style lang="less" scoped>
el-header{
padding: 0;
}
</style>
App.vue
<!-- 清除边框 -->
<style lang="less">
html, body, h3{
margin:0px;
padding: 0px;
}
</style>
效果
![[Pasted image 20231204175502.png]]
实现点击菜单栏进行路由跳转
![[屏幕截图 2023-12-04 203236.png]]
在mothods中添加方法
clickMenu(item){
//跳转前判断路由是否合法
if(this.$route.path !== item.path && !(this.$route.path === '/home' && item.path == '/')){
this.$router.push(item.path);
}
}
创建CommonHeader组件并导入
![[屏幕截图 2023-12-04 204327.png]]
CommonHeader模板代码及其基本样式
<template>
<div class="headerContainer">
<div class="l-content">
<el-button style="margin-right:20px" icon="el-icon-menu" size="mini"></el-button>
<el-breadcrumb separator="/">
<el-breadcrumb-item>首页</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="r-content">
<el-dropdown>
<span class="el-dropdown-link">
<img class="user" src="../assets/images/user.png" alt="">
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
</script>
<style lang="less" scoped>
.headerContainer{
padding: 0 20px;
background-color: #333;
height: 60px;
display: flex;
justify-content: space-between;//左右分离
align-items: center;//垂直居中
.l-content{
display: flex;
align-items: center;
/deep/.el-breadcrumb__item {
.el-breadcrumb__inner {
font-weight: normal;
&.is-link{
color: #999
}
}
&:last-child {
.el-breadcrumb__inner {
color: #FFF
}
}
}
}
.r-content{
.user {
width: 40px;
height: 40px;
border-radius: 50%;
}
}
.text {
color: #fff;
font-size: 14px;
margin-left: 10px;
}
}
</style>
组件通信-vuex
创建store目录及其index.js和tab.js文件
index.js代码
import Vue from 'vue'
import Vuex from 'vuex'
import tab from './tab'
Vue.use(Vuex)
export default new Vuex.Store({
modules:{
tab,
}
})
tab.js代码
export default {
state:{
},
mutations: {
}
}
导入并挂载store
组件通信方式实现菜单栏的展开折叠
vuex部分
CommonHeader组件部分
CommonAside部分
系统名在折叠时的显示
<h3>{{ isCollapse ? '后台':'通用后台管理系统' }}</h3>
首页
Layout布局
<template>
<el-row>
<el-col :span="8">
<div ></div>
</el-col>
<el-col :span="16">
<div ></div>
</el-col>
</el-row>
</template>
Home组件基本模板及左上角名片代码
<template>
<el-row>
<el-col :span="8">
<el-card class="box-card">
<div class="user">
<img src="../assets/images/user.png" alt="">
<div class="userinfo">
<p class="name">Admin</p>
<p class="access">管理员</p>
<p></p>
</div>
</div>
<div class="login-info">
<p>上次登陆地点: <span>广东 珠海</span></p>
<p>上次登陆时间: <span>20231224</span></p>
</div>
</el-card>
<el-zard style="margin-top: 20px; height: 460px"></el-card
</el-col>
<el-col :span="16">
<div ></div>
</el-col>
</el-row>
</template>
<script>
export default {
name: 'Home',
data() {
return {
}
}
}
</script>
<style lang="less" scoped>
.user{
display: flex;
align-items: center;
padding-bottom: 20px;
margin-bottom: 20px;
img{
margin-right: 40px;
width: 150px;
height: 150px;
border-radius: 50%;
}
.userinfo {
.name {
font-size: 32px;
margin-bottom: 10px;
}
.access{
color: #999999;
}
}
}
.login-info {
p {
line-height: 28px;
font-size: 14px;
color:#999999;
span {
color: #666;
margin-left: 60px;
}
}
}
</style>
右侧订单的实现
countData数据
countData: [
{
name: "今日支付订单",
value: 1234,
icon: "success",
color: "#2ec7c9",
},
{
name: "今日收藏订单",
value: 210,
icon: "star-on",
color: "#ffb980",
},
{
name: "今日未支付订单",
value: 1234,
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "本月支付订单",
value: 1234,
icon: "success",
color: "#2ec7c9",
},
{
name: "本月收藏订单",
value: 210,
icon: "star-on",
color: "#ffb980",
},
{
name: "本月未支付订单",
value: 1234,
icon: "s-goods",
color: "#5ab1ef",
},
],
代码实现
<div class="num">
<el-card v-for="item in countData" :key="item.name" :body-style="{ display: 'flex', padding: 0 }">
<i class="icon" :class="`el-icon-${item.icon}`" :style="{ background: item.color }"></i>
<div class="detail">
<p class="price">¥{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
Axios
创建utils目录及其request.js文件
import axios from 'axios'
const http = axios.create({
baseURL: '/api',
timeout: 10000,
})
// 添加请求拦截器
http.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
export default http
创建api目录及其index.js文件
//api文件夹中放置关于接口数据的信息
import http from '../utils/request'
//请求首页数据
export const getData = () => {
//返回一个Promise对象
return http.get('/home/getData')
}
Mock数据模拟
安装
cnpm i mockjs@1.1.0
配置后端数据模拟
mock.js代码
import Mock from 'mockjs'
import homeApi from './mockServeData/home'
import user from './mockServeData/user'
import permission from './mockServeData/permission'
Mock.mock('/api/home/getData', homeApi.getStatisticalData);
//用户列表的数据
Mock.mock('/api/user/add', 'post', user.createUser);
Mock.mock('/api/user/edit', 'post', user.updateUser);
Mock.mock('/api/user/del', 'post', user.deleteUser);
Mock.mock(/api\/user\/get/, user.getUserList);
Mock.mock(/api\/permission\/getMenu/, 'post', permission.getMenu)
在main.js文件中引入
import './api/mock'
home.js代码
// mock数据模拟
import Mock from 'mockjs'
// 图表数据
let List = []
export default {
getStatisticalData: () =>
//Mock.Random.float 产生随机数100到8000之间 保留小数 最小0位 最大0位
for (let i = 0; i < 7; i++) {
List.push(
Mock.mock({
苹果: Mock.Random.float(100, 8000, 0, 0),
vivo: Mock.Random.float(100, 8000, 0, 0),
oppo: Mock.Random.float(100, 8000, 0, 0),
魅族: Mock.Random.float(100, 8000, 0, 0),
三星: Mock.Random.float(100, 8000, 0, 0),
小米: Mock.Random.float(100, 8000, 0, 0)
})
)
}
return {
code: 20000,
data: {
// 饼图
videoData: [
{
name: '小米',
value: 2999
},
{
name: '苹果',
value: 5999
},
{
name: 'vivo',
value: 1500
},
{
name: 'oppo',
value: 1999
},
{
name: '魅族',
value: 2200
},
{
name: '三星',
value: 4500
}
],
// 柱状图
userData:[
{
date: '周一',
new: 5,
active: 200
},
{
date: '周二',
new: 10,
active: 500
},
{
date: '周三',
new: 12,
active: 550
},
{
date: '周四',
new: 60,
active: 800
},
{
date: '周五',
new: 65,
active: 550
},
{
date: '周六',
new: 53,
active: 770
},
{
date: '周日',
new: 33,
active: 170
}
],
// 折线图
orderData: {
date: ['20191001', '20191002', '20191003', '20191004', '20191005', '20191006', '20191007'],
data: List
},
tableData: [
{
name: 'oppo',
todayBuy: 500,
monthBuy: 3500,
totalBuy: 22000
},
{
name: 'vivo',
todayBuy: 300,
monthBuy: 2200,
totalBuy: 24000
},
{
name: '苹果',
todayBuy: 800,
monthBuy: 4500,
totalBuy: 65000
},
{
name: '小米',
todayBuy: 1200,
monthBuy: 6500,
totalBuy: 45000
},
{
name: '三星',
todayBuy: 300,
monthBuy: 2000,
totalBuy: 34000
},
{
name: '魅族',
todayBuy: 350,
monthBuy: 3000,
totalBuy: 22000
}
]
}
}
}
}
permission.js代码
import Mock from 'mockjs'
export default {
getMenu: config => {
const { username, password } = JSON.parse(config.body)
// 先判断用户是否存在
// 判断账号和密码是否对应
if (username === 'admin' && password === 'admin') {
return {
code: 20000,
data: {
menu: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 's-home',
url: 'Home.vue'
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'video-play',
url: 'Mall.vue'
},
{
path: '/user',
name: 'user',
label: '用户管理',
icon: 'user',
url: 'User.vue'
},
{
label: '其他',
icon: 'location',
children: [
{
path: '/page1',
name: 'page1',
label: '页面1',
icon: 'setting',
url: 'PageOne.vue'
},
{
path: '/page2',
name: 'page2',
label: '页面2',
icon: 'setting',
url: 'PageTwo.vue'
}
]
}
],
token: Mock.Random.guid(),
message: '获取成功'
}
}
} else if (username === 'xiaoxiao' && password === 'xiaoxiao') {
return {
code: 20000,
data: {
menu: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 's-home',
url: 'Home.vue'
},
{
path: '/video',
name: 'video',
label: '商品管理',
icon: 'video-play',
url: 'Mall.vue'
}
],
token: Mock.Random.guid(),
message: '获取成功'
}
}
} else {
return {
code: -999,
data: {
message: '密码错误'
}
}
}
}
}
user.js代码
import Mock from 'mockjs'
// get请求从config.url获取参数,post从config.body中获取参数
function param2Obj (url) {
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(
'{"' +
decodeURIComponent(search)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
)
}
let List = []
const count = 200
for (let i = 0; i < count; i++) {
List.push(
Mock.mock({
id: Mock.Random.guid(),
name: Mock.Random.cname(),
addr: Mock.mock('@county(true)'),
'age|18-60': 1,
birth: Mock.Random.date(),
sex: Mock.Random.integer(0, 1)
})
)
}
export default {
// /**
// * 获取列表
// * 要带参数 name, page, limt; name可以不填, page,limit有默认值。
// * @param name, page, limit
// * @return {{code: number, count: number, data: *[]}}
// */
getUserList: config => {
const { name, page = 1, limit = 20 } = param2Obj(config.url)
console.log('name:' + name, 'page:' + page, '分页大小limit:' + limit)
const mockList = List.filter(user => {
if (name && user.name.indexOf(name) === -1 && user.addr.indexOf(name) === -1) return false
return true
})
const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return {
code: 20000,
count: mockList.length,
list: pageList
}
},
/**
* 增加用户
* @param name, addr, age, birth, sex
* @return {{code: number, data: {message: string}}}
*/
createUser: config => {
const { name, addr, age, birth, sex } = JSON.parse(config.body)
console.log(JSON.parse(config.body))
List.unshift({
id: Mock.Random.guid(),
name: name,
addr: addr,
age: age,
birth: birth,
sex: sex
})
return {
code: 20000,
data: {
message: '添加成功'
}
}
},
/**
* 删除用户
* @param id
* @return {*}
*/
deleteUser: config => {
const { id } = JSON.parse(config.body)
if (!id) {
return {
code: -999,
message: '参数不正确'
}
} else {
List = List.filter(u => u.id !== id)
return {
code: 20000,
message: '删除成功'
}
}
},
/**
* 批量删除
* @param config
* @return {{code: number, data: {message: string}}}
*/
batchremove: config => {
let { ids } = param2Obj(config.url)
ids = ids.split(',')
List = List.filter(u => !ids.includes(u.id))
return {
code: 20000,
data: {
message: '批量删除成功'
}
}
},
/**
* 修改用户
* @param id, name, addr, age, birth, sex
* @return {{code: number, data: {message: string}}}
*/
updateUser: config => {
const { id, name, addr, age, birth, sex } = JSON.parse(config.body)
const sex_num = parseInt(sex)
List.some(u => {
if (u.id === id) {
u.name = name
u.addr = addr
u.age = age
u.birth = birth
u.sex = sex_num
return true
}
})
return {
code: 20000,
data: {
message: '编辑成功'
}
}
}
}
Home.vue代码
<template>
<el-row>
<el-col :span="8" style="padding-right: 10px">
<el-card class="box-card">
<div class="user">
<img src="../assets/images/user.png" alt="">
<div class="userinfo">
<p class="name">Admin</p>
<p class="access">SuperAdmin</p>
</div>
</div>
<div class="login-info">
<p>上次登录时间:<span>20231123</span></p>
<p>上次登录地点:<span>珠海</span></p>
</div>
</el-card>
<el-card style="margin-top:20px; height:460px">
<el-table :data="tableData" style="width: 100%">
<!-- <el-table-column prop="name" label="课程" width="180">
</el-table-column>
<el-table-column prop="todayBuy" label="今日购买" width="180">
</el-table-column>
<el-table-column prop="monthBuy" label="本月购买" width="180">
</el-table-column>
<el-table-column prop="totalBuy" label="总购买" width="180">
</el-table-column> -->
<el-table-column v-for="(val, key) in tableLabel" :key="key" :prop="key" :label="val" />
</el-table>
</el-card>
</el-col>
<el-col :span="16" style="padding-left: 10px">
<div class="num">
<el-card v-for="item in countData" :key="item.name" :body-style="{ display: 'flex', padding: 0 }">
<i class="icon" :class="`el-icon-${item.icon}`" :style="{ background: item.color }"></i>
<div class="detail">
<p class="price">¥{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
<el-card style="height: 280px">
<!-- 折线图 -->
<div ref="echarts1" style="height: 280px;"></div>
</el-card>
<div class="graph">
<el-card style="height: 260px">
<div ref="echarts2" style="height: 260px;"></div>
</el-card>
<el-card style="height: 260px">
<div ref="echarts3" style="height: 240px;"></div>
</el-card>
</div>
</el-col>
</el-row>
</template>
<script>
import { getData } from '../api'
import * as echarts from 'echarts'
import { toRefs } from 'vue';
import { ref } from 'vue';
import { onMounted } from 'vue';
export default {
data() {
return {
tableData: [],
tableLabel: {
name: '课程',
todayBuy: '今日购买',
monthBuy: '本月购买',
totalBuy: '总购买'
},
countData: [
{
name: "今日支付订单",
value: 1234,
icon: "success",
color: "#2ec7c9",
},
{
name: "今日收藏订单",
value: 210,
icon: "star-on",
color: "#ffb980",
},
{
name: "今日未支付订单",
value: 1234,
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "本月支付订单",
value: 1234,
icon: "success",
color: "#2ec7c9",
},
{
name: "本月收藏订单",
value: 210,
icon: "star-on",
color: "#ffb980",
},
{
name: "本月未支付订单",
value: 1234,
icon: "s-goods",
color: "#5ab1ef",
},
],
}
},
mounted() {
getData().then(({ data }) => {
const { tableData } = data.data;
this.tableData = tableData;
//基于准备好的dom,初始化echarts实例
const echarts1 = echarts.init(this.$refs.echarts1)
//指定图标的配置项和数据
var echarts1option = {}
//处理数据xAxis-x轴
const { orderData, userData, videoData } = data.data
const xAxis = Object.keys(orderData.data[0])
const xAxisData = { data: xAxis }
echarts1option.xAxis = xAxisData;
echarts1option.yAxis = {}
echarts1option.legend = xAxisData;
echarts1option.series = []
//对原数据进行重新组装
xAxis.forEach(key => {
echarts1option.series.push({
name: key,
data: orderData.data.map(item => item[key]),
type: 'line',
})
})
//使用刚指定的配置项和数据显示图表
echarts1.setOption(echarts1option)
//柱状图
const echarts2 = echarts.init(this.$refs.echarts2)
const echarts2option = {
legend: {
// 图例文字颜色
textStyle: {
color: "#333",
},
},
grid: {
left: "20%",
},
// 提示框
tooltip: {
trigger: "axis",
},
xAxis: {
type: "category", // 类目轴
data: userData.map(item => item.date),
axisLine: {
lineStyle: {
color: "#17b3a3",
},
},
axisLabel: {
interval: 0,
color: "#333",
},
},
yAxis: [
{
type: "value",
axisLine: {
lineStyle: {
color: "#17b3a3",
},
},
},
],
color: ["#2ec7c9", "#b6a2de"],
series: [
{
name: '新增用户',
data: userData.map(item => item.new),
type: 'bar'
},
{
name: '活跃用户',
data: userData.map(item => item.active),
type: 'bar'
}
],
}
//调用实例的setOption
echarts2.setOption(echarts2option)
//饼状图
const echarts3 = echarts.init(this.$refs.echarts3)
const echarts3option = {
tooltip: {
trigger: "item",
},
color: [
"#0f78f4",
"#dd536b",
"#9462e5",
"#a6a6a6",
"#e1bb22",
"#39c362",
"#3ed1cf",
],
series: [
{
data: videoData,
type: 'pie',
},
],
}
echarts3.setOption(echarts3option);
})
}
}
</script>
<style lang="less" scoped>
.user {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
img {
margin-right: 40px;
width: 150px;
height: 150px;
border-radius: 50%;
}
.userinfo {
.name {
font-size: 32px;
margin-bottom: 10px;
}
.access {
color: #999999;
}
}
}
.login-info {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
.num {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.icon {
width: 80px;
height: 80px;
font-size: 30px;
color: #fff;
text-align: center;
line-height: 80px;
}
.detail {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.price {
padding: none;
font-size: 30px;
margin-top: 10px;
margin-bottom: 10px;
line-height: 30px;
height: 30px;
}
.desc {
font-size: 10px;
color: #999;
text-align: center;
}
}
.el-card {
width: 32%;
margin-bottom: 20px;
}
}
.graph {
margin-top: 20px;
display: flex;
justify-content: space-between;
.el-card {
width: 48%
}
}
</style>