1、准备所需框架,插件,组件库等。
- Vue框架(这里我是用了 vue2的框架)
- Element UI 组件(也可以自己封装的组件)
- axios(网络请求)
- quill(富文本编辑器)
- echarts(数据可视化工具)
axios的封装
axios 的封装,对请求拦截器和响应拦截器进行处理:
创建 axios ,在里面配置请求基地址和请求超时时间。在请求拦截器里面设置请求头,用户登录后,后台会返回token,请求其他数据是需要用到这个token,用来判断我们是否登录成功;在响应拦截器里面我们可以对 token 进行过期处理,还可以对后台返回给我们的状态码,比如成功的状态码200,和失败的状态码404 进行对应的处理,还有就是没有网络的时候,响应拦截器会返回给我们失败的状态,根据这些状态就可以给用户进行提示。
// 引入 axios 网路请求
import axios from 'axios'
// 引入 element ui 里消息提示框,对用户进行提示操作
import {Message, MessageBox} from "element-ui";
// 引入 router
import router from "@/router";
// 创建 axios
const server = axios.create({
// 请求基地址
baseURL: '请求基地址',
// 请求超时时间
timeout: 5000
})
// 请求拦截器
server.interceptors.request.use(config => {
// 获取 本地存储的 token
let token = localStorage.getItem('token')
// 判断本地是否存在 token ,登录成功后,后台会返回
if (token) {
// 添加请求头,请求数据的时候需要用
config.headers.Authorization = token
}
return config
}, error => {
Promise.reject(error)
})
// 响应拦截器
server.interceptors.response.use(res => {
// 状态码
let codeStatus = res.data.meta.status; // 状态码
// token 无效的过期处理
if (res.data.meta.msg == "无效token") {
MessageBox.confirm('登录失效, 是否重新登录?', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 删除本地存储的 token
localStorage.removeItem('token')
// 跳转到登录页面
router.push('/login')
}).catch(() => {
this.$message({
type: 'info',
message: '已取消重新登录'
});
});
}
// 失败的状态码处理,给用户进行提示
if (codeStatus === 400 || codeStatus === 401 ||
codeStatus === 403 || codeStatus === 404 ||
codeStatus === 422 || codeStatus === 500) {
Message.error(res.data.meta.msg);
}
return res.data
}, error => {
// 没有网络的时候进行处理,提示用户
let {message} = error
// Network Error 这个是固定的
if (message == 'Network Error') {
message = "后端接口连接异常"
// 请求超时处理
} else if (message.includes('timeout')) {
message = '系统接口亲请求超时'
} else {
return
}
Message.error(message);
})
export default server
二次处理:
这次的处理可以使页面的网络请求代码更加简洁,也方便后期的维护
// 引入 axios 的封装
import server from "@/api/server";
// 导出请求
export function getLogin(data) {
// 请求方式,请求地址,请求参数
return server.post('login', data)
}
// 如果是 GET 请求
// export function getLogin(params) {
// 请求方式,请求地址,请求参数
// return server.get('login', {params})
// }
2、登录(核心:登录的鉴权)
- 将后端接口所需的数据,前端进行验证表单(element ui 表单验证)。
- 将验证后的数据用 axios 发送给后端,
- 根据后端响应给我们的数据,状态,对应的给用户进行提示。
- 把 token 存储到本地,跳转到首页
用登录鉴权功能,防止用户在没有登录的情况下跳转页面
登录的鉴权我们需要用到 Vue 的路由守卫(分为全局路由守卫,组件路由守卫和路由独享守卫,这里用到了全局路由守卫的前置路由守卫)
在 router :
// 注意:全局路由守卫是在实例化路由后写的,不能在写在实例化之前
// to 是要进⼊的路由,from 离开之前的路由,next 是进入下一个路由
router.beforeEach((to, from, next) => {
// 获取本地存储的 token 判断我们是否登录
let token = localStorage.getItem('token')
// 如果 token 不存在,并且当前页面不是 login 页面
if (!token && to.path != '/login') {
// 跳转的登录页面
next('/login');
return
}
// 如果 token 存在,并且当前页面是 login 页面
if (token && to.path == '/login') {
// 跳转到主页
next('/')
return;
}
next()
})
// 也可以设置路由白名单,可以让用户在不登陆的情况下,也可以跳转到的页面
3、首页(核心:动态路由,左侧导航菜单刷新后还是对应的选中状态)
首页要实现的功能:
退出功能:
退出功能使用 element ui 的 MessageBox 弹框 对用户进行提示,让用户确定是否要退出,确定退出后,删除本地存储的 token 并且返回到登录页面。
左侧导航菜单功能:
- 使用 axios 发起网络请求,获取左侧菜单栏的数据
- 使用element ui 的 NavMenu 导航菜单 实现页面的布局
- 实现刷新还是对应选中的状态
// 注意事项:我这利用的的是获取当前路由的方式,不用去特意的存储路由 // 不过需要配置路由的时候,路由的 path 和 name 完全一样才可以,path 前面带 / 没事 // NavMenu 导航里 SubMenu Attribute 的 index 需要绑定 path // :default-active="$route.name" 就可以实现了 <el-menu :collapse="isCollapse" :collapse-transition="false" :default-active="$route.name" active-text-color="#409EFF" background-color="#333744" class="el-menu-vertical-demo" text-color="#fff" unique-opened> <el-submenu v-for="(item,index) in menusData" :key="item.id" :index="item.path"> <template slot="title"> <i v-show="index == 0" class="el-icon-s-custom"></i> <i v-show="index == 1" class="el-icon-s-help"></i> <i v-show="index == 2" class="el-icon-star-on"></i> <i v-show="index == 3" class="el-icon-s-order"></i> <i v-show="index == 4" class="el-icon-s-flag"></i> <span>{{ item.authName }}</span> </template> <el-menu-item-group> <template slot="title"><span v-show="isCollapse">{{ item.authName }}</span></template> <el-menu-item v-for="ele in item.children" :key="ele.id" :index="ele.path" @click="$router.push('/'+ele.path)">{{ ele.authName }} </el-menu-item> </el-menu-item-group> </el-submenu> </el-menu>
- 实现动态路由
// 实现动态路由,我们在获取左侧菜单的时候就要对其进行处理, // 如果获取的是树状结构的数据,我们需要对其转换为列表结构的数据,方便我们对其循环添加 // 在添加到对应的路由里 使用的router 的方法 addRoute // 因为浏览器刷新的时候,路由会初始化,所以我们在 router 里面也需要在做一次动态路由的添加, // 防止刷新后找不到对应的路由 // 所以 树状转列表 的递归方法需要封装一下,可以重用
树状结构转换为列表结构方法:
export function list(data) { // 声明一个空数组,用来添加动态路由的数据的 let array = []; // 使用后递归循环的方式将树形结构数据转化为列表结构 function listReplacement(data) { data.forEach(item => { // 判断每一项的children有没有数据, // 因为后台给我们返回的数据里,就算children里面没有数据也会把children以空数组的方式返回给我们 // 所以我们通过children数组的长度来判断 if (item.children.length) { // 调用函数自身,实现递归 listReplacement(item.children) } else { // 如果没有就将这一项数据添加到一个新的数组,达成列表结构 // 注意:如果你的路由文件需要和这个导航菜单的path需要一致。 // 如果不一致(比如首字母大写,再进行添加到compoent里面时需要将数据的path里面的第一个字母也转换成大写,具体还需要看你的数据写法) // console.log(item) // 我的因为path和对应文件名一致,就不需要在转换 // 指定文件的component 里,有的可能需要在路径末尾添加 .vue 后缀 array.push({ path: '/' + item.path, name: item.path, component: () => import(`@/views/page/${item.path}.vue`) }) } }) } listReplacement(data) // 返回通过递归循环转换成列表数据的数组 return array }
获取左侧菜单后的处理:
// 先引入递归方法和axios对应的请求 import {list} from "@/utils/listReplacement"; import {getMenus} from "@/api/api"; getMenus().then(res => { this.menusData = res.data // 将获取到的左侧菜单存到本地,方便在 router 里面的处理 localStorage.setItem('menus', JSON.stringify(res.data)) // 树状转列表 let menusList = list(res.data) // 列表数据循环 menusList.forEach(item => { // 使用router 的方法 addRoute进行添加 // 第一个参数是需要添加的路由的name属性,第二个参数是要添加的路由参数 this.$router.addRoute('home', item) }) })
在 router :
// 注意:此操作是在实例化路由后写的,不能在写在实例化之前 function dynamic() { // 获取本地存储的 token 用来判断我们是否登录 let token = localStorage.getItem('token') let menus = JSON.parse(localStorage.getItem('menus')) // 判断我们是否登录,是否有左侧菜单的数据 if (token && menus) { // 如果有,menus给的数据不是列表结构而是树状结构,就需要我们把树状结构转换为列表结构 //我们完成动态路由需要的是列表结构的导航菜单 let menusData = list(menus) // 使用router 的方法 addRoute进行添加 // 第一个参数是需要添加的路由的name,第二个参数是要添加的路由 menusData.forEach(item => { router.addRoute("home", item) }) } } // 调用一次,不然会出现白屏 dynamic()
- 以上操作完成就可以跳转对应的页面了
4、实现对应的页面功能
4-1、用户列表:
- axios 获取用户列表数据
- 用element ui 的 table 表格 ,Button 按钮 和 Pagination 分页 实现页面的布局
// 分页功能: // 主要就是修改我们的请求的 页码,每页多少条,然后再次发起请求,对页面进行渲染 <el-pagination :current-page="pagenum" :page-size="pagesize" :page-sizes="[5, 10, 25, 50]" :total="total" layout="total, sizes, prev, pager, next, jumper" style="margin-top: 20px" @size-change="handleSizeChange" @current-change="handleCurrentChange"> </el-pagination> // methods handleSizeChange(val) { this.pagenum = 1 this.pagesize = val this.getUsers() }, handleCurrentChange(val) { this.pagenum = val this.getUsers() },
- 添加,编辑,删除,状态,分配角色使用element ui 的 Dialog 对话框和表单验证
- 分配角色功能:
// 分配角色功能需要用到角色列表的数据, // 把角色列表的数据用下拉菜单渲染到分配角色的弹框中 // 将角色 id 和 用户 id 用 axios 发送给后端,完成分配角色功能 // 分配角色的确定按钮 setUpUser() { if (!this.setUpForm.rid) { this.$message({ message: '请选择分配的角色', type: 'warning' }) return; } getSetUpUsers(this.setUpForm).then(res => { if (res.meta.status === 200) { this.$message({ message: res.meta.msg, type: 'success' }) this.getUsers() } this.setUpUserVisible = false }) },
添加用户:
<el-button type="primary" @click="addUserVisible = true">添加用户</el-button>
addUser() {
getAddUsers(this.userForm).then(res => {
if (res.meta.status === 201) {
this.$message({
message: res.meta.msg,
type: 'success'
})
this.getUsers()
}
this.userForm = {}
this.addUserVisible = false
})
},
编辑:
// 编辑
edit(val) {
this.editUserForm = val
this.editUserVisible = true
},
// 编辑用户的确定按钮
editUser() {
getEditUsers(this.editUserForm).then(res => {
console.log(res)
if (res.meta.status === 200) {
this.$message({
message: res.meta.msg,
type: 'success'
})
this.getUsers()
}
this.editUserVisible = false
})
},
删除:
// 删除
del(val) {
this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
getDeleteUsers(val).then(res => {
console.log(res)
if (res.meta.status == 200) {
this.$message({
type: 'success',
message: '删除成功!'
});
this.getUsers()
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
修改状态:
// 修改状态
stateChange(val) {
getStateUsers(val).then(res => {
if (res.meta.status === 200) {
this.$message({
message: res.meta.msg,
type: 'success'
})
} else {
val.mg_state = !val.mg_state
}
})
},
4-2、角色列表:
渲染页面,带有展开:
<!--角色列表-->
<el-table :data="rolesData" border style="width: 100%">
<!--每一行展开数据-->
<el-table-column type="expand">
<template slot-scope="props">
<el-form class="demo-table-expand" inline label-position="left">
<el-row v-for="item in props.row.children" :key="item.id" :gutter="24" class="oneRow">
<el-col :span="5">
<el-tag closable effect="dark" @close="delRolesRights(props.row,item)">{{ item.authName }}</el-tag>
<i class="el-icon-caret-right" style="font-size: 18px"></i>
</el-col>
<el-col :span="19">
<el-row v-for="ele in item.children" :key="ele.id" :gutter="24" class="twoRow">
<el-col :span="6">
<el-tag closable effect="dark" type="success" @close="delRolesRights(props.row,ele)">
{{ ele.authName }}
</el-tag>
<i class="el-icon-caret-right" style="font-size: 18px"></i>
</el-col>
<el-col :span="18">
<el-row :gutter="24" class="rightBox">
<el-tag v-for="items in ele.children.slice(0,4)" :key="items.id"
closable effect="dark" type="warning"
@close="delRolesRights(props.row,items)">
{{ items.authName }}
</el-tag>
</el-row>
</el-col>
</el-row>
</el-col>
</el-row>
</el-form>
</template>
</el-table-column>
<el-table-column label="#" type="index">
</el-table-column>
<el-table-column label="角色名称" prop="roleName">
</el-table-column>
<el-table-column label="角色描述" prop="roleDesc">
</el-table-column>
<el-table-column fixed="right" label="操作">
<template slot-scope="scope">
<el-button icon="el-icon-edit" size="mini" type="primary" @click="editRole(scope.row)">编辑</el-button>
<el-button icon="el-icon-delete" size="mini" type="danger" @click="deleteRole(scope.row)">删除</el-button>
<el-button icon="el-icon-setting" size="mini" type="warning" @click="assignmentRights(scope.row)">分配权限
</el-button>
</template>
</el-table-column>
</el-table>
核心:就是分配权限功能:
// 分配权限功能思路就是获取树状权限列表的所有数据
// 利用递归获取当前角色拥有权限的id
// 根据角色拥有权限的id 选中对应权限
// 请求修改的时候需要使用 element ui 的tree树形控件,将半选中的和选中的id 用asiox 请求接口发送给后端
// 我这个写法有闪烁问题
// 需用在关闭弹框和打开弹框的时候都条用一次清空数据
//只用在关闭的时候使用setCheckedKeys 经行再次更新 就可以解决
// 关闭分配权限弹框的函数回调
assignmentRightsDown() {
this.assignmentRightsVisible = false
this.assignmentRightsKeys = []
// this.$nextTick(() => {
this.$refs.tree.setCheckedKeys(this.assignmentRightsKeys)
// })
},
// 分配权限弹框的确定按钮
async assignment() {
let getCheckedKeys = this.$refs.tree.getCheckedKeys()
let getHalfCheckedKeys = this.$refs.tree.getHalfCheckedKeys()
let rids = [...getCheckedKeys, ...getHalfCheckedKeys].join(',')
const assignmentright = await getAssignmentRights(this.assignmentRightFrom, rids)
if (assignmentright.meta.status === 200) {
this.$message({
type: 'success',
message: assignmentright.meta.msg
});
await this.getRoles()
}
this.assignmentRightsVisible = false
},
// 分配权限按钮
assignmentRights(val) {
this.assignmentRightsKeys = []
// this.$nextTick(() => {
// this.$refs.tree.setCheckedKeys(this.assignmentRightsKeys)
// })
this.assignmentRightFrom = val
this.assignmentRightsVisible = true
this.getLeafKeys(val.children, this.assignmentRightsKeys)
},
// 循环递归,拿到角色拥有权限的id
getLeafKeys(arrays, arr) {
arrays.forEach(item => {
if (!item.children) {
return arr.push(item.id)
}
this.getLeafKeys(item.children, arr)
// this.$nextTick(() => {
// this.$refs.tree.setCheckedKeys(arr)
// })
})
},
4-3、商品列表
核心:就是添加商品的图片上传功能和富文本
图片上传功能:
// 思路
// 使用element ui 的Upload 上传;
// 上传图片需要我们设置请求头发送token,
// 设置完毕后在上成功触发的函数里面,
// 后台会返回临时路径和正式地址
// 将图片信息添加到表单图片pics数组中
// 删除也是通过循环遍历得到对应的存储临时路径的下标进行删除的
// 图片预览就是点击图片,将正式地址用弹出框加img标签渲染就可以了
// action:指定图片上传api接口
// :on-preview : 当点击图片时会触发该事件进行预览操作,处理图片预览
// :on-remove : 当用户点击图片右上角的X号时触发执行
// :on-success:当用户点击上传图片并成功上传时触发
// list-type :设置预览图片的方式
// :headers :设置上传图片的请求头
// 请求路径
actionURL: 'http://127.0.0.1:8888/api/private/v1/upload',
// 设置请求头
headerToken: {
Authorization: localStorage.getItem('token')
},
// 预览图片
previewImg(file) {
// console.log(file)
this.previewVisible = true
this.previewURL = file.response.data.url
},
// 删除图片
removeImg(file) {
// console.log(file)
// console.log(this.goodsForm)
let path = file.response.data.tmp_path
let index = this.goodsForm.pics.findIndex(item => item.pic == path)
this.goodsForm.pics.splice(index, 1)
},
// 上传成功
successImg(file) {
// console.log(file)
let path = file.data.tmp_path
this.goodsForm.pics.push({pic: path})
},
富文本:
富文本步骤:
1. 安装vue-quill-editor
npm install vue-quill-editor -S
2. 安装quill
npm install quill -S
// 导入富文本编辑器 在mian.js引入
import VueQuillEditor from 'vue-quill-editor'
// require styles 导入富文本编辑器对应的样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
Vue.use(VueQuillEditor)
<!-- 富文本编辑器组件 -->
<quill-editor v-model="addForm.goods_introduce"></quill-editor>
4-4、数据报表
用的是echarts 数据可视化工具
// 安装echarts,建议不要装最新的,最好安装4版本的,不要安装5版本的
// 因为5版本的引入方式需要在加 *as ,打包的时候难处理
// 指定版本安装 echarts
cnpm i echarts@4.9.0 -S
// 引入
import echarts from 'echarts';
// 使用的时候注意事项
// 需要给echarts 容器给上宽高,不然是出不来效果的
// 因为用的是dom元素,所以在mounted() 里面写
// 因为没有自适应,但是官方给我们提供了方法
// 初始化dom元素
var myEchaets = echarts.init(document.getElementById('main'))
// 获取数据报表数据
const reports = await getReports()
this.option.legend.data = reports.data.legend.data
this.option.series = reports.data.series
this.option.xAxis = reports.data.xAxis
this.option.yAxis = reports.data.yAxis
myEchaets.setOption(this.option)
// 根据窗口自适应
window.onresize = function () {
myEchaets.resize();
};