Vue项目
项目功能
-
1 登录
-
2 首页
-
3 退出
-
4 用户管理
-
4.1 列表展示
-
4.2 分页
-
4.3 查询
-
4.4 启用/禁用用户
-
4.5 添加用户
-
4.6 编辑用户
-
4.7 删除用户
-
-
5 权限管理
-
5.1 权限列表
-
-
6 角色管理
-
6.1 角色列表
-
6.2 编辑角色
-
6.3 删除角色
-
6.4 表格项展开,展示角色权限
-
6.5 权限模块业务说明
-
6.6 分配权限
-
-
7 用户管理
-
7.1 分配角色
-
-
8 动态菜单
项目搭建
-
1
vue init webpack shop_admin
Project name :默认
Project description :默认
Author :默认
Vue build :选择 Runtime + Compiler
Install vue-router? :Y
Use ESLint to lint your code? :Y 选择 Standard
Set up unit tests :n
Setup e2e tests with Nightwatch? : n
Should we run `npm install` for you after the project has been created? (recommended) : Yes, use NPM
-
2 进入项目:cd shop_admin
-
3 运行项目:npm run dev
-
将脚手架默认生成的内容去掉,然后,添加了一个 Login 组件
如何添加一个新的功能???
-
1 在
components
中新建一个文件夹(login),在文件中创建组件(Login.vue) -
2 在
router/index.js
中导入组件(Login.vue) -
3 配置路由规则
在项目中使用 element-ui
-
安装:
npm i element-ui -S
// main.js
// 导入elementui - js
import ElementUI from 'element-ui'
// 导入elementui - css
import 'element-ui/lib/theme-chalk/index.css'
// 安装插件
Vue.use(ElementUI)
项目启动做了什么
-
1 在终端中运行:
npm run dev
,实际上就是运行了:webpack-dev-server ...
-
2 使用 webpack-dev-server 开启一个服务器
-
3 根据指定的入口
src/main.js
开始分析入口中使用到的模块 -
4 当遇到
import
的时候,webpack 就会加载这些模块内容(如果有重复模块,比如:Vue,实际上将来只会加载一次),遇到代码就执行这些代码 -
5 创建 Vue 实例,将 App 组件作为模板进行编译,并且将 App 组件中 template 的内容渲染在页面 #app 的位置
登录功能
-
1 安装:
npm i -S axios
-
2 在
Login.vue
组件中导入 axios -
3 使用 axios 根据接口文档来发送请求,完成登录
编程式导航
-
就是通过 JS 代码来实现路由的跳转功能
// 注意:是 router 不是 route
// router用来实现路由跳转,route用来获取路由参数
// push 方法的参数为:要跳转到的路由地址(path)
this.$router.push('/home')
密码
-
给输入框组件添加 type="password" 就变为密码框状态了
<el-input type="password" v-model="loginForm.password"></el-input>
登录拦截
-
说明:在没有登录的情况不应该让用户来访问除登录以外的任何页面
登录和拦截的整个流程说明
-
1 在登录成功以后,将 token 存储到 localStorage 中
-
2 在 导航守卫 中先判断当前访问的页面是不是登录页面
-
3 如果是登录页面,直接放行(next())
-
4 如果不是登录页面,就从 localStorage 中获取 token,判断有没有登录
-
5 如果登录了,直接放行(next())
-
6 如果没有登录,就跳转到登录页面让用户登录(next('/login'))
token 机制的说明
-
在项目中,如果登录成功了,那么,服务器会给我们返回一个 token
-
这个 token 就是登录成功的标识
-
这个 token 就相当于使用 cookie+session 机制中的 sessionid
公司人员和项目开发流程
-
1 产品经理定制项目的需求
-
2 分配任务:先将所有的任务分配到项目组,然后,再由项目组具体分配给每个开发人员
-
3 开发:拿到 产品原型 + 需求文档 + UI 设计稿 资料,转化为 HTML 页面,完成功能
-
4 功能完成后,自己测试有没有 Bug
-
5 由测试人员来测试你的功能,当测试出 Bug 后,就会通过 禅道 这样的项目管理系统,来提出 Bug
-
6 由 自己 修改 测试人员提出来的 bug
-
7 最终,没有 bug 了,项目才会上线
产品经理(Product Manager)
提需求
产出: 产品原型 + 需求文档
原型设计软件:Axure 、墨刀
UI(设计)
将 产品经理 给的 原型图 设计为 好看的UI稿
FE(前端)front-end
产品原型 + 需求文档 + UI设计稿 ===> HTML页面
BE(后端) back-end
给前端提供数据接口
测试人员
产品原型 + 需求文档 + UI设计稿 来测试我们写的功能
发现你的功能 与 需求 不一致,那就说明除Bug了,那么,测试人员就会提Bug
Bug系统: 禅道
项目经理(管理技术)
技术攻坚,与其他项目组人员沟通,分配任务 等
vue 单文件组件中的 scoped
-
作用:给
style
标签添加scoped
属性以后,样式只会对当前组件中的结构生效,而不会影响到其他组件
vue 单文件组件中的 lang
-
作用:添加
lang="less"
后,就可以使用 less 语法了 -
但是需要自己安装:
npm i -D less less-loader
VSCode 中使用 Vetur 插件格式化单文件组件的 template
-
打开设置,配置:
"vetur.format.defaultFormatter.html": "js-beautify-html"
接口调用的说明
-
注意:所有接口都需要传递 token,只有传递了正确的 token,服务器才会将数据返回给前端
-
如果没有传递
token
,服务器会返回401
,告诉接口调用者,没有权限来访问这个接口
cookie+session VS token
Git 使用
刚进入公司,一般都是先通过 git 将项目 clone 到本地:git clone
实现一些功能后,需要将本地写完的代码,提交到git服务器中:git push 仓库地址
每天去公司开发前,都会使用 git pull 仓库地址 来获取下最新的代码
# 克隆仓库
git clone [仓库地址]
# 推送
git push [仓库地址] master
# 简化推送地址
git remote add shop_admin [仓库地址]
git push -u shop_admin master
# 第一次执行上面两条命令,以后只需要输入以下命令即可
git push shop_admin
# 拉取
git pull [仓库地址] master
git pull shop_admin master
分页
-
当前页 curPage
-
每页大小 pageSize
-
总条数 total
总页数 = Math.ceil(total / pageSize)
用户管理
启用或禁用用户
-
注意:如果用户状态是禁用状态,那么这个用户是无法登录的
-
新添加的用户是禁用的状态
-
注意:不要将 admin 用户禁用!!!否则会导致无法操作
添加用户
打开添加用户对话框的步骤
-
1 给添加用户按钮绑定单击事件,事件中让控制对话框展示的数据为 true
-
2 在 dialog 对话框中添加一个 form 表单
-
form 表单的数据(userAddForm)
-
form 表单的验证规则(userAddRules),还要给每个表单项添加 prop
-
等等
-
-
3 点击用户添加对话框中的确定按钮,进行表单验证(this.$refs.userAddForm.validate...)
-
4 点击取消按钮,重置表单(this.$refs.userAddForm.resetFields())
打开编辑用户对话框的步骤
-
1 给表格中的编辑按钮添加单击事件,事件中完成两件事情:
-
1.1 将控制对话框展示的数据设置为 true
-
1.2 获取到当前用户数据,并展示在对话框中
-
-
2 在 dialog 对话框中添加一个 form 表单
-
编辑用户表单数据 + 验证规则
-
-
3 点击编辑用户对话框中的确定按钮,进行表单验证
-
4 点击取消按钮,重置表单
角色管理
删除角色
-
1 点击按钮弹出确认对话框
-
1.1 给按钮绑定单击事件
-
1.2 弹出确认对话框
-
-
2 点击确认按钮删除该角色
修改角色
-
1 点击修改按钮弹出修改对话框
-
2 将当前要修改的角色信息展示在对话框中
-
3 点击确定按钮修改信息
给角色分配权限
-
1 进入页面,就获取到所有的权限,并展示在tree中
-
2 点击分配权限按钮后,打开分配权限对话框
-
3 并且选中当前角色具有的权限
async 和 await 的说明
-
await 必须要出现在 async 修饰的函数中。并且 async 应该添加到 await 存在的那个方法中
权限模块
-
权限模块中涉及到以下几个内容:
权限
、角色
、用户
-
用户 -> 角色 -> 权限
-
-
权限和菜单是关联到一起,你有这个权限,才会看到对应的菜单!!!
每个用户都有自己的角色
每个角色又有自己的权限
只要用户有这个角色,那么,用户就拥有这个角色的权限
比如:
小明经过多年努力,小明当上了财务主管,此时,小明就具有了 财务主管 角色的所有权限了
在项目中:
1 用户 和 角色是一对一的关系
也就是:一个用户只能有一个角色
2 角色 和 权限是多对多的关系
也就是:一个角色可以有多个权限
一个权限也可以属于多个角色
用户 | 角色 | 权限 |
---|---|---|
小明 | 普通员工 | 查看个人信息,查看自己的薪资 |
小红 | 普通员工 | 查看个人信息,查看自己的薪资 |
老王 | 财务主管 | 查看个人信息,查看自己的薪资,查看所有员工的薪资 |
1、Promise回顾
(1)data-a.txt
aaaa
(2)data-b.txt
bbbb
(3)data-c.txt
cccc
(4)data-d.txt
dddd
(5)index.js
const fs = require('fs')
// 使用Promise封装异步操作
function readFile(path) {
const p = new Promise(function(resolve, reject) {
fs.readFile(path, function(err, data) {
if (err) {
return reject(err)
}
resolve(data.toString())
})
})
return p
}
// async 和 await 是异步编程的终极解决方案
async function fn() {
// await 的返回值是 后面Promise 成功的结果,也就是 resolve 方法的参数
try {
const a = await readFile('./data/aa.txt')
console.log(a)
} catch (err) {
console.log('出错了:', err)
}
const b = await readFile('./data/b.txt')
const c = await readFile('./data/c.txt')
const d = await readFile('./data/d.txt')
console.log(b)
console.log(c)
console.log(d)
}
fn()
console.log('abc')
// Promise是可以无限调用 then 方法的
// 每一个后面的 then ,都可以获取到前一个 then 的返回值
/*
readFile('./data/a.txt')
.then(function(data) {
console.log('a: ', data)
return readFile('./data/b.txt')
})
.then(function(data) {
console.log('b: ', data)
return readFile('./data/c.txt')
})
.then(function(data) {
console.log('c: ', data)
return readFile('./data/d.txt')
})
.then(function(data) {
console.log('d: ', data)
})
*/
/* function readFile(path, callback) {
fs.readFile(path, function(err, data) {
callback(data)
})
}
*/
02-Promise的内容补充.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
// Promise 的三种状态:
// 1 pending 等待
// 2 fulfilled(resolved) 成功
// 3 rejected 失败
// 状态只能是:
// 1 pending -> fulfilled
// 2 pending -> rejected
// 状态只要发生改变了,就不会再次改变
const p = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve()
reject()
}, 5000)
})
p.then(() => {
console.log('成功:', p)
})
.catch(() => {
console.log('失败:', p)
})
console.log(p)
</script>
</body>
</html>
3、Promise的两个常用的方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./axios.js"></script>
<script>
// 需求:有三个请求,要等到三个请求都完成之后,再执行一些操作
// Promise.all([
// axios.get('http://localhost:3000/user'),
// axios.get('http://localhost:3000/student'),
// axios.get('http://localhost:3000/teacher')
// ])
// .then(res => {
// console.log(res)
// })
// 需求:有三个请求,只要有一个请求完成了,就执行一些操作
// Promise.race([
// axios.get('http://localhost:3000/user'),
// axios.get('http://localhost:3000/student'),
// axios.get('http://localhost:3000/teacher')
// ]).then(res => {
// console.log(res)
// })
// Promise 的其他方法介绍:
// Promise.resolve() 方法用来直接获取到一个成功的Promise对象
// Promise
// .resolve('123')
// .then(res => {
// console.log(res)
// })
// 直接获取到一个失败的Promise对象
// Promise.reject()
</script>
</body>
</html>
4、总结
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
// 权限模块:
// 用户
// 角色
// 权限
// 用户 -> 角色 -> 权限
// 权限 和 菜单,是关联到一起的
// 也就是说:有这个权限,才能访问到这个权限对应的菜单
</script>
</body>
</html>
5、shop-admin
(1)Home-home.vue
<template>
<el-container class="home-wrapper">
<el-header>
<el-row>
<el-col :span="8" class="logo">
<img src="@/assets/logo.png" alt="黑马logo">
</el-col>
<el-col :span="8">
<h1 class="title">电商后台管理系统</h1>
</el-col>
<el-col :span="8">
<div class="welcome">
<span>欢迎上海前端25期星曜会员</span>
<a href="javascript:;" @click.prevent="logout">退出</a>
</div>
</el-col>
</el-row>
</el-header>
<el-container>
<el-aside width="200px">
<!--
el-menu 表示菜单组件
default-active 当前激活菜单的 index 值
@open 菜单展开事件
@close 菜单收起事件
el-sub-menu 表示一组菜单
index 是唯一的,不能重复!!!
-->
<el-menu :router="true" default-active="/home/users" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<el-submenu index="1">
<!--
template: 用来包裹一级菜单,内部指定菜单的图标和菜单名称
如果要给菜单添加 小图标,应该使用 template 来包裹整个内容
-->
<template slot="title">
<i class="el-icon-location"></i>
<span>用户管理</span>
</template>
<!-- 启用路由模式后,index就相当于 原来 router-link 中的to属性,用来指定导航的路径(哈希值) -->
<!-- 可以使用 /home/users 或者 home/users -->
<el-menu-item index="/home/users">
<template slot="title">
<i class="el-icon-menu"></i>
<span>用户列表</span>
</template>
</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-location"></i>
<span>权限管理</span>
</template>
<el-menu-item index="/home/roles">
<template slot="title">
<i class="el-icon-menu"></i>
<span>角色列表</span>
</template>
</el-menu-item>
<el-menu-item index="/home/rights">
<template slot="title">
<i class="el-icon-menu"></i>
<span>权限列表</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
<!-- 子路由出口 -->
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
methods: {
// 退出功能
logout() {
// 1 弹出确认对话框
// 2 用户点击确认
// 2.1 跳回登录页面
// 2.2 清除token
this.$confirm('您是否确认退出?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 点击确认按钮
.then(() => {
// 清除token
localStorage.removeItem('token')
// 跳回登录页面
this.$router.push('/login')
})
// // 点击取消按钮
// .catch(() => {
// this.$message({
// type: 'info',
// message: '已取消删除'
// })
// })
},
handleOpen(key, keyPath) {
console.log('open', key, keyPath)
},
handleClose(key, keyPath) {
console.log('close', key, keyPath)
}
}
}
</script>
<style scoped lang="less">
.home-wrapper {
height: 100%;
.el-header {
padding: 0;
background-color: #b3c1cd;
color: #333;
text-align: center;
.logo {
text-align: left;
}
.title {
margin: 0;
line-height: 60px;
color: #fff;
font-size: 36px;
}
.welcome {
line-height: 60px;
font-weight: bold;
text-align: right;
padding-right: 30px;
a {
color: #b07a17;
text-decoration: none;
}
}
}
.el-aside {
background-color: #545c64;
color: #333;
line-height: 200px;
}
.el-main {
background-color: #eaeef1;
color: #333;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
}
</style>
(2)login-Login.vue
<template>
<div class="login-wrapper">
<!-- 登录
<el-button type="primary">成功按钮</el-button> -->
<!--
el-form
label-position="top" 设置label的位置
:model 用来给表单设置数据模型(对象)
:rules 用来设置表单验证规则的
ref 用来引用当前的表单组件
el-form-item
label 当前表单项的名称
prop 它的值是 model 对象中的一个属性
当使用 表单验证 或者 表单重置 功能时,必须要提供该属性
el-input
v-model 实现双向数据绑定
-->
<!-- row 表示一行 -->
<el-row type="flex" class="loginForm" justify="center" align="middle">
<!-- col 表示一列 span 表示占用几份(共24份) -->
<el-col :xs="12" :sm="10" :md="8" :lg="6" :xl="4" class="login-content">
<el-form label-position="top" :model="loginForm" :rules="rules" ref="loginForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">登录</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
// 导入 axios
import axios from 'axios'
export default {
data() {
return {
loginForm: {
username: 'admin',
password: '123456'
},
rules: {
username: [
// required 是否为必填项
// message 当前规则校验失败时的提示
// trigger 表单验证的触发实际,blur表示失去焦点时触发
{ required: true, message: '用户名为必填项', trigger: 'blur' },
// min 最小长度
// max 最大长度
{
min: 3,
max: 6,
message: '用户名长度在 3 到 6 个字符',
trigger: 'blur'
}
],
password: [
{ required: true, message: '密码为必填项', trigger: 'blur' },
{
min: 3,
max: 6,
message: '密码长度在 3 到 6 个字符',
trigger: 'blur'
}
]
}
}
},
methods: {
// 登录功能的实现
/* login() {
// 使用 axios 发送请求
// http://localhost:8888/api/private/v1/login
axios
.post('http://localhost:8888/api/private/v1/login', this.loginForm)
.then(res => {
// console.log(res)
// const data = res.data.data
// const meta = res.data.meta
// ES6中的解构,意思就是从 res.data 中取出属性 data 和 meta
const { data, meta } = res.data
// console.log(data)
if (meta.status === 200) {
// console.log('登录成功')
// 将登录成功的标识(token)存储到localStorage中
localStorage.setItem('token', data.token)
// 登录成功,需要跳转到 后台管理的首页
this.$router.push('/home')
} else {
// console.log('登录失败', meta.msg)
// this.$message.error(meta.msg)
this.$message({
type: 'error',
message: meta.msg,
duration: 1000
})
}
})
}, */
async login() {
// 使用 axios 发送请求
// http://localhost:8888/api/private/v1/login
const res = await axios.post(
'http://localhost:8888/api/private/v1/login',
this.loginForm
)
console.log(res)
const { data, meta } = res.data
// console.log(data)
if (meta.status === 200) {
// console.log('登录成功')
// 将登录成功的标识(token)存储到localStorage中
localStorage.setItem('token', data.token)
// 登录成功,需要跳转到 后台管理的首页
this.$router.push('/home')
} else {
// console.log('登录失败', meta.msg)
// this.$message.error(meta.msg)
this.$message({
type: 'error',
message: meta.msg,
duration: 1000
})
}
},
submitForm() {
// ref 用在组件中,就表示当前组件
// this.$refs.loginForm
this.$refs.loginForm.validate(valid => {
// valid 表示是否校验成功,如果成功就为:true
// 如果失败就为:false
if (valid) {
// 成功:调用登录接口
// alert('submit!')
// 获取到用户名和密码
// console.log(this.loginForm)
this.login()
} else {
// 校验失败
return false
}
})
},
resetForm() {
this.$refs.loginForm.resetFields()
}
}
}
</script>
<style>
.login-wrapper,
.loginForm {
height: 100%;
}
.loginForm {
background-color: #2d434c;
}
.login-content {
min-width: 240px;
padding: 20px 35px;
border-radius: 10px;
background-color: #fff;
}
</style>
(3)rights-Rigth.vue
<template>
<div>
<el-breadcrumb separator-class="el-icon-arrow-right" class="rights-breadcrumb">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>权限管理</el-breadcrumb-item>
<el-breadcrumb-item>权限列表</el-breadcrumb-item>
</el-breadcrumb>
<el-table :data="rightsList" stripe style="width: 100%">
<!-- 添加 type="index" 表示给表格添加索引号 -->
<el-table-column type="index" width="50">
</el-table-column>
<el-table-column prop="authName" label="权限名称" width="180">
</el-table-column>
<el-table-column prop="path" label="路径" width="180">
</el-table-column>
<el-table-column prop="level" label="层级">
<template slot-scope="scope">
<span v-if="scope.row.level === '0'">一级</span>
<span v-else-if="scope.row.level === '1'">二级</span>
<span v-else>三级</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
rightsList: []
}
},
created() {
this.getRightsList()
},
methods: {
/**
* 获取权限列表数据
*/
async getRightsList() {
const res = await this.$http.get('/rights/list')
// console.log(res)
const { data, meta } = res.data
if (meta.status === 200) {
this.rightsList = data
}
}
}
}
</script>
<style>
.rights-breadcrumb {
line-height: 40px;
background-color: #d4dae0;
font-size: 18px;
padding-left: 10px;
}
</style>
(4)roles-Roles.vue
<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style.less" lang="less"></style>
(5)script.js
export default {
data() {
return {
rolesList: [],
// 修改对话框的展示和隐藏
roleEditDialog: false,
roleEditForm: {
id: -1,
roleName: '',
roleDesc: ''
},
// 分配权限对话框的展示和隐藏
rightsDialog: false,
// 所有的权限(树形结构)
rightsTree: [],
defaultProps: {
// children 用来指定使用哪个属性来指定子节点
children: 'children',
// label 用来指定使用数据中的哪个属性展示树形控制中每个节点的名字
label: 'authName'
},
// 当前分配权限的角色id
curRoleId: -1
}
},
created() {
this.getRolesList()
this.getAllRightsTree()
},
methods: {
/**
* 获取到所有的权限树形结构数据
*/
async getAllRightsTree() {
const res = await this.$http.get('/rights/tree')
const { data, meta } = res.data
if (meta.status === 200) {
this.rightsTree = data
}
},
/**
* 获取角色列表数据
*/
async getRolesList() {
const res = await this.$http.get('/roles')
// console.log(res)
const { data, meta } = res.data
if (meta.status === 200) {
this.rolesList = data
}
},
/**
* 根据角色id删除角色
* @param {number} id 要删除角色的id
*/
async delRolesById(id) {
try {
// 等待用户点击确定或取消按钮
await this.$confirm('确认删除该角色吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// console.log('点击确定删除')
// 如果点击是确定按钮,就会执行以下代码:
// 发送请求,删除当前角色
const res = await this.$http.delete(`/roles/${id}`)
if (res.data.meta.status === 200) {
const index = this.rolesList.findIndex(item => item.id === id)
this.rolesList.splice(index, 1)
}
} catch (err) {
// 如果点击的取消按钮,就会执行以下代码:
// 相当于处理 Promise 的catch()
// console.log('点击取消')
this.$message({
type: 'info',
message: '已取消删除'
})
}
/* this.$confirm('确认删除该角色吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
// 发送请求,删除当前角色
const res = await this.$http.delete(`/roles/${id}`)
if (res.data.meta.status === 200) {
const index = this.rolesList.findIndex(item => item.id === id)
this.rolesList.splice(index, 1)
}
})
.catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
}) */
},
/**
* 展示修改角色对话框
*/
showRoleEditDailog(curRole) {
this.roleEditDialog = true
// 获取到当前角色的数据
for (const key in this.roleEditForm) {
this.roleEditForm[key] = curRole[key]
}
},
/**
* 修改角色信息
*/
async editRole() {
const { id, roleName, roleDesc } = this.roleEditForm
const res = await this.$http.put(`/roles/${id}`, {
roleName,
roleDesc
})
const { data, meta } = res.data
if (meta.status === 200) {
// 关闭对话框
this.roleEditDialog = false
// 更新列表数据
const editRole = this.rolesList.find(item => item.id === id)
editRole.roleName = data.roleName
editRole.roleDesc = data.roleDesc
}
},
/**
* 删除指定角色的权限
* @param {number} roleId 角色id
* @param {number} rightId 权限id
*/
async delRoleRightById(roleId, rightId) {
// console.log('删除', roleId, rightId)
const res = await this.$http.delete(`roles/${roleId}/rights/${rightId}`)
// console.log(res)
const { data, meta } = res.data
if (meta.status === 200) {
const curRole = this.rolesList.find(item => item.id === roleId)
curRole.children = data
}
},
showRightsDialog(curRoleRights, id) {
// 展示对话框
this.rightsDialog = true
// 暂存当前角色id
this.curRoleId = id
// v-if 和 v-show
// v-if 控制展示和隐藏,在隐藏的时候, Vue 是不会渲染这个DOM或组件的!!!
// 由此,我们知道 visible 属性,是通过 v-if 来控制展示和隐藏
// 因为tree是包裹在 dialog 中的,而 dialog 一开始是隐藏的
// 并且,dialog 隐藏的时候, Vue 是不会渲染这个 dialog 组件
// 因此,无法直接通过 $refs 来获取到tree
// 如何获取到???
// 在 $nextTick 回调函数中,就可以获取到的tree。
// 因为上面通过设置 rightsDialog = true,将对话框展示出来,但是,Vue中的DOM更新
// 是异步的,所以,必须等到DOM更新完成后,才能获取到的 tree
// 当 nextTick 的回调函数执行的时候,DOM就已经完成更新了
this.$nextTick(() => {
// 三级菜单id数组
const level3Ids = []
curRoleRights.forEach(level1 => {
level1.children.forEach(level2 => {
level2.children.forEach(level3 => {
level3Ids.push(level3.id)
})
})
})
// 指定选中节点的id数组
this.$refs.rightsTree.setCheckedKeys(level3Ids)
})
},
/**
* 给角色分配权限
*/
async assignRights() {
// 1 获取到当前角色选中的权限id数组
// 获取全选项
const checkedKeys = this.$refs.rightsTree.getCheckedKeys()
// 获取半选项
const halfCheckedKeys = this.$refs.rightsTree.getHalfCheckedKeys()
// 将全选的和半选的合并到一起
const allCheckedIds = [...checkedKeys, ...halfCheckedKeys]
const res = await this.$http.post(`/roles/${this.curRoleId}/rights`, {
rids: allCheckedIds.join(',')
})
if (res.data.meta.status === 200) {
this.rightsDialog = false
// 注意:需要重新获取角色列表
this.getRolesList()
}
}
}
}
(6)style.less
.level1-row {
padding: 20px 0;
border-bottom: 1px dotted #ccc;
&:last-child {
border-bottom: 0;
}
}
.level2-row {
padding-bottom: 10px;
}
.level3-tag {
margin-right: 3px;
}
(7)template.html
<div>
<!-- 列表 -->
<el-table :data="rolesList" stripe style="width: 100%">
<!-- type="expand" 表示展开表格列 -->
<el-table-column type="expand">
<template slot-scope="scope">
<el-row v-if="scope.row.children.length === 0">
<el-col>
暂无权限,请分配
</el-col>
</el-row>
<!-- 遍历一级菜单 -->
<el-row v-else class="level1-row" v-for="level1 in scope.row.children" :key="level1.id">
<!-- 展示一级菜单的名字 -->
<el-col :span="4">
<!-- closable 带有关闭x号 type 表示颜色类型 -->
<el-tag closable @close="delRoleRightById(scope.row.id, level1.id)">
{{ level1.authName }}
</el-tag>
<i class="el-icon-arrow-right"></i>
</el-col>
<el-col :span="20">
<!-- 遍历二级菜单 -->
<el-row class="level2-row" v-for="level2 in level1.children" :key="level2.id">
<el-col :span="4">
<!-- 展示二级菜单的名字 -->
<el-tag closable type="success" @close="delRoleRightById(scope.row.id, level2.id)">
{{ level2.authName }}
</el-tag>
<i class="el-icon-arrow-right"></i>
</el-col>
<!-- 遍历三级菜单 -->
<el-col :span="20">
<!-- 展示三级菜单的名字 -->
<el-tag class="level3-tag" closable type="warning" v-for="level3 in level2.children" :key="level3.id"
@close="delRoleRightById(scope.row.id, level3.id)">
{{ level3.authName }}
</el-tag>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
</el-table-column>
<el-table-column type="index"></el-table-column>
<el-table-column prop="roleName" label="角色名称" width="180">
</el-table-column>
<el-table-column prop="roleDesc" label="描述" width="180">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" plain size="mini" icon="el-icon-edit" @click="showRoleEditDailog(scope.row)"></el-button>
<el-button type="danger" plain size="mini" icon="el-icon-delete" @click="delRolesById(scope.row.id)"></el-button>
<el-button type="success" icon="el-icon-check" plain size="mini" @click="showRightsDialog(scope.row.children, scope.row.id)">分配权限</el-button>
</template>
</el-table-column>
</el-table>
<!-- 修改角色对话框 -->
<el-dialog title="修改角色" :visible.sync="roleEditDialog">
<el-form :model="roleEditForm">
<el-form-item label="角色名称" label-width="120px">
<el-input v-model="roleEditForm.roleName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="角色描述" label-width="120px">
<el-input v-model="roleEditForm.roleDesc" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="roleEditDialog = false">取 消</el-button>
<el-button type="primary" @click="editRole">确 定</el-button>
</div>
</el-dialog>
<!-- 分配权限对话框 -->
<el-dialog title="分配权限" :visible.sync="rightsDialog">
<!--
树形控件
data 用来指定数据
show-checkbox 表示显示checkbox
node-key 每个节点的唯一标识,使用 id 即可
default-expand-all 默认展开所有的节点
props 是一个对象,用来设置 子节点以及展示文字 的属性名称
-->
<el-tree ref="rightsTree" :data="rightsTree" show-checkbox node-key="id" :default-expand-all="true" :props="defaultProps">
</el-tree>
<div slot="footer" class="dialog-footer">
<el-button @click="rightsDialog = false">取 消</el-button>
<el-button type="primary" @click="assignRights">确 定</el-button>
</div>
</el-dialog>
</div>
(8)users-Users.vue
<template>
<div>
<el-breadcrumb separator-class="el-icon-arrow-right" class="user-breadcrumb">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
<el-breadcrumb-item>用户列表</el-breadcrumb-item>
</el-breadcrumb>
<el-row :gutter="20">
<el-col :span="6">
<el-input placeholder="请输入用户名" v-model="queryStr" class="input-with-select">
<el-button slot="append" icon="el-icon-search" @click="queryUserList"></el-button>
</el-input>
</el-col>
<el-col :span="4">
<el-button type="success" plain @click="showUserAddDialog">添加用户</el-button>
</el-col>
</el-row>
<!--
el-table 表格组件
data 用来给表格组件提供数据
stripe 添加改属性后,启用隔行变色效果
el-table-column 表格中的每一列
label 每一列的标题名称
width 每一列的宽度
prop 表示数据中的属性名(字段名称)
userList = [
{}, {}, {}
]
-->
<el-table :data="userList" stripe>
<el-table-column prop="username" label="姓名" width="180">
</el-table-column>
<el-table-column prop="email" label="邮箱" width="180">
</el-table-column>
<el-table-column prop="mobile" label="电话" width="180">
</el-table-column>
<el-table-column label="用户状态">
<!-- scope.row 表示当前行的数据 -->
<template slot-scope="scope">
<!--
v-model 用来绑定数据
active-color="#409EFF" 启用时的颜色
inactive-color="#C0CCDA" 禁用时的颜色
-->
<el-switch v-model="scope.row.mg_state" @change="changeUserState(scope.row.id, scope.row.mg_state)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" plain size="mini" icon="el-icon-edit" @click="showUserEditDailog(scope.row)"></el-button>
<el-button type="danger" plain size="mini" icon="el-icon-delete" @click="delUserById(scope.row.id)"></el-button>
<el-button type="success" icon="el-icon-check" plain size="mini" @click="showUserAssignDialog">分配角色</el-button>
</template>
</el-table-column>
</el-table>
<!--
分页组件
background 背景色
layout 分页显示的内容
total 总条数
给 current-page 属性添加 .sync 修饰符后, 就可以设置当前页
-->
<el-pagination background layout="prev, pager, next" :total="total" :page-size="pageSize" :current-page.sync="curPage" @current-change="changePage">
</el-pagination>
<!-- 添加用户对话框 -->
<el-dialog title="添加用户" :visible.sync="userAddDialog" @close="closeUserAddDialog">
<el-form :model="userAddForm" :rules="userAddRules" ref="userAddForm">
<el-form-item prop="username" label="用户名" label-width="120px">
<el-input v-model="userAddForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码" label-width="120px">
<el-input v-model="userAddForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="email" label="邮箱" label-width="120px">
<el-input v-model="userAddForm.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="mobile" label="手机" label-width="120px">
<el-input v-model="userAddForm.mobile" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="userAddDialog = false">取 消</el-button>
<el-button type="primary" @click="addUser">确 定</el-button>
</div>
</el-dialog>
<!-- 编辑用户对话框 -->
<el-dialog title="编辑用户" :visible.sync="userEditDialog" @close="closeUserEditDialog">
<el-form :model="userEditForm" :rules="userEditRules" ref="userEditForm">
<el-form-item prop="username" label="用户名" label-width="120px">
<el-input disabled :value="userEditForm.username"></el-input>
</el-form-item>
<el-form-item prop="email" label="邮箱" label-width="120px">
<el-input v-model="userEditForm.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="mobile" label="手机" label-width="120px">
<el-input v-model="userEditForm.mobile" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="userEditDialog = false">取 消</el-button>
<el-button type="primary" @click="editUser">确 定</el-button>
</div>
</el-dialog>
<!-- 给用户分配角色 -->
<el-dialog title="分配角色" :visible.sync="userAssignDialog">
<el-form :model="userAssignForm">
<el-form-item label="用户名" label-width="120px">
<el-input v-model="userAssignForm.username" disabled></el-input>
</el-form-item>
<el-form-item label="角色列表" label-width="120px">
<el-select v-model="userAssignForm.roles" placeholder="请选择角色">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="userAssignDialog = false">取 消</el-button>
<el-button type="primary" @click="userAssignDialog = false">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
// import axios from 'axios'
export default {
created() {
// console.log('axios: ', this.$http === axios)
// 发送请求,获取数据
this.getUserList()
},
data() {
return {
userList: [],
// 每页大小
pageSize: 3,
// 当前页码
curPage: 1,
// 总条数
total: 0,
// 搜索内容
queryStr: '',
// 控制用户添加对话框的展示和隐藏
userAddDialog: false,
userAddForm: {
username: '',
password: '',
email: '',
mobile: ''
},
userAddRules: {
username: [
{ required: true, message: '用户名为必填项', trigger: 'blur' },
{
min: 3,
max: 6,
message: '用户名长度在 3 到 6 个字符',
trigger: 'blur'
}
],
password: [
{ required: true, message: '密码为必填项', trigger: 'blur' },
{
min: 3,
max: 6,
message: '密码长度在 3 到 6 个字符',
trigger: 'blur'
}
]
},
// 控制编辑用户对话框的展示和隐藏
userEditDialog: false,
userEditForm: {
id: -1,
username: '',
email: '',
mobile: ''
},
userEditRules: {
mobile: [
{
pattern: /^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/,
message: '手机号码格式不正确',
// 如果需要在值改变或者失去焦点的时候,都触发验证,可以传递两个
// trigger: 'change, blur'
// 当前值改变,就会触发
trigger: 'change'
}
]
},
// 分配角色
userAssignDialog: false,
userAssignForm: {
// 用户id
id: -1,
// 用户角色id
rid: -1,
// 用户名
username: '',
// 角色列表
roles: []
}
}
},
methods: {
// 获取用户列表数据
// curPage = 1 给参数添加默认值
/* getUserList(curPage = 1) {
this.$http
.get('/users', {
params: {
// 当前页
pagenum: curPage,
// 每页展示多少条数据
pagesize: 3,
// 查询条件
query: this.queryStr || ''
}
// 将 token 作为请求头,传递给服务器接口
// 这样,才能正确的调用这个接口
// headers: {
// Authorization: localStorage.getItem('token')
// }
})
.then(res => {
console.log('请求成功:', res)
const { data, meta } = res.data
if (meta.status === 200) {
// 获取数据成功
this.userList = data.users
this.total = data.total
this.curPage = data.pagenum
}
})
}, */
async getUserList(curPage = 1) {
const res = await this.$http.get('/users', {
params: {
// 当前页
pagenum: curPage,
// 每页展示多少条数据
pagesize: 3,
// 查询条件
query: this.queryStr || ''
}
})
const { data, meta } = res.data
if (meta.status === 200) {
// 获取数据成功
this.userList = data.users
this.total = data.total
this.curPage = data.pagenum
}
},
/**
* 分页获取数据
* 参数 cruPage 表示当前点击的页码
*/
changePage(curPage) {
// console.log('当前页为:', curPage)
this.getUserList(curPage)
},
// 搜索
queryUserList() {
// console.log(this.queryStr)
this.curPage = 1
this.getUserList()
},
// 启用或禁用用户
async changeUserState(id, curState) {
// console.log(id, curState)
const res = await this.$http.put(`/users/${id}/state/${curState}`)
const { data, meta } = res.data
if (meta.status === 200) {
this.$message({
type: 'success',
message: data.mg_state === 0 ? '禁用成功' : '启用成功',
duration: 1000
})
} else {
this.$message({
type: 'error',
message: meta.msg,
duration: 1000
})
}
},
// 展示用户添加对话框
showUserAddDialog() {
this.userAddDialog = true
},
// 关闭对话框重置表单
closeUserAddDialog() {
// console.log('对话框关闭了')
this.$refs.userAddForm.resetFields()
},
// 添加用户
addUser() {
this.$refs.userAddForm.validate(valid => {
if (valid) {
// 成功
// console.log('验证成功')
this.$http.post('/users', this.userAddForm).then(res => {
// console.log(res)
const { meta } = res.data
if (meta.status === 201) {
// 1 关闭对话框
// 2 重置表单(只要关闭对话框,就会自动触发对话框的关闭事件来重置表单)
this.userAddDialog = false
// 3 重新获取列表数据
this.total += 1
const lastPage = Math.ceil(this.total / this.pageSize)
this.getUserList(lastPage)
}
})
} else {
// console.log('验证失败')
return false
}
})
},
// 根据用户id删除用户
delUserById(id) {
// console.log(id)
this.$confirm('确认删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
this.$http.delete(`users/${id}`).then(res => {
// console.log(res)
const { meta } = res.data
if (meta.status === 200) {
this.$message({
type: 'success',
message: meta.msg
})
const index = this.userList.findIndex(item => item.id === id)
this.userList.splice(index, 1)
if (this.userList.length === 0) {
this.getUserList(--this.curPage)
}
}
})
})
.catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
// 展示编辑对话框
showUserEditDailog(curUser) {
// console.log(curUser)
// 先获取到当前用户的数据
// 数据交给 userEditForm 后,就会展示在编辑对话框中
for (const key in this.userEditForm) {
this.userEditForm[key] = curUser[key]
}
// 打开用户编辑对话框
this.userEditDialog = true
},
// 关闭用户编辑对话框
closeUserEditDialog() {
this.$refs.userEditForm.resetFields()
},
// 点击确定按钮,修改用户数据
editUser() {
this.$refs.userEditForm.validate(valid => {
if (valid) {
const { id, email, mobile } = this.userEditForm
// console.log('表单验证成功')
this.$http
.put(`/users/${id}`, {
email,
mobile
})
.then(res => {
// console.log(res)
const { data, meta } = res.data
if (meta.status === 200) {
// 更新该用户的数据
const editUser = this.userList.find(item => item.id === id)
editUser.email = data.email
editUser.mobile = data.mobile
// 关闭对话
this.userEditDialog = false
}
})
} else {
// console.log('表单验证失败')
return false
}
})
},
/**
* 展示用户分配角色对话框
*/
showUserAssignDialog() {
this.userAssignDialog = true
}
}
}
</script>
<style>
.user-breadcrumb {
line-height: 40px;
background-color: #d4dae0;
font-size: 18px;
padding-left: 10px;
}
</style>
(9)router-index.js
import Vue from 'vue'
import Router from 'vue-router'
// 导入 Login 组件(注意,不要添加 .vue 后缀)
import Login from '@/components/login/Login'
// 导入首页组件
import Home from '@/components/home/Home'
// 导入用户列表组件
import Users from '@/components/users/Users'
// 导入权限列表组件
import Rights from '@/components/rights/Rights'
// 导入角色列表组件
import Roles from '@/components/roles/Roles'
Vue.use(Router)
const router = new Router({
routes: [
// children 用来配置子路由,将来匹配的组件会展示在 Home 组件的 router-view 中
{
path: '/home',
component: Home,
children: [
{ path: 'users', component: Users },
{ path: 'rights', component: Rights },
{ path: 'roles', component: Roles }
]
},
{ path: '/login', component: Login }
]
})
// 全局导航守卫
// 所有的路由都会先走守卫
// 添加导航守卫之后,不管是访问哪个路由,都会执行beforeEach回调函数中的代码
// 因为所有的路由,都会经过该导航守卫,所以,就可以在这个导航守卫的回调函数中
// 判断有没有登录了
router.beforeEach((to, from, next) => {
// console.log('导航守卫在看门', to)
// ...
if (to.path === '/login') {
// 如果访问的是login页面,直接放行,也就是任何人不管有没有登录
// 都可以访问登录页面
// 直接调用 next() 方法,表示:访问的是哪个页面,就展示这个页面的内容
next()
} else {
// 访问的不是登录页面
// 判断用户是否登录成功,
// 1 当用户登录成功,直接调用 next() 方法放行
// 2 当用户没有登录,应该调用 next('/login') 跳转到登录页面,让用户登录
// 通过登录成功时候保存的token,来作为有没有登录成功的条件
const token = localStorage.getItem('token')
if (token) {
// 有,登录成功
next()
} else {
// 没有,登录失败
next('/login')
}
}
})
export default router
(10)App.vue
<template>
<div id="app">
<!-- 路由出口 -->
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100%;
}
</style>
(11)main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
// 导入全局样式
import '@/assets/index.css'
// 导入elementui - js
import ElementUI from 'element-ui'
// 导入elementui - css
import 'element-ui/lib/theme-chalk/index.css'
// 导入axios
import axios from 'axios'
// 配置公共路径
// 配置好公共路径后, 每次使用 axios 发送请求, 只需要写当前接口的路径(比如: /users) 就可以了
// axios 在发送请求之前, 会将 baseUrl + '/users' 得到完整路径, 才会发送请求
axios.defaults.baseURL = 'http://localhost:8888/api/private/v1'
// 只要配置了拦击器, 那么所有的请求都会走拦截器
// 因此,可以在拦截器中统一处理 headers
// 请求拦截器
axios.interceptors.request.use(function(config) {
// 在请求发送之前做一些事情
// endsWith 字符串的方法,用来判断是不是以参数为结尾,如果是返回值为true
// 判断如果是登录接口,就不需要添加 Authorization 请求头
if (!config.url.endsWith('/login')) {
config.headers['Authorization'] = localStorage.getItem('token')
}
// console.log('请求拦截器', config)
return config
})
// 响应拦截器
axios.interceptors.response.use(function(response) {
// 在获取到响应数据的时候做一些事情
// console.log('响应拦截器', response)
if (response.data.meta.status === 401) {
// 因为现在不是在组件中,因此无法通过 this.$router 来访问到路由实例
// 但是,可以直接通过上面导入的路由模块中的 router (路由实例)来访问到路由对象
router.push('/login')
localStorage.removeItem('token')
}
return response
})
// 将 axios 添加到Vue的原型中
// 实例对象可以直接使用原型对象中的属性或方法
// 所有的组件都是Vue的实例
// 说明: 只要是像 axios 这样的第三方库(与Vue没有任何关系),都应该通过这种方式来统一导入
Vue.prototype.$http = axios
// 安装插件
Vue.use(ElementUI)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
// 注册 App 组件为当前实例的局部组件,然后,才可以在template中使用该组件
components: { App },
template: '<App/>'
})