❤️springboot+vue2+springsecurity权限管理系统
一、项目的介绍
1、更多内容在线阅读的文档
2、关注下面的公众号 小明的学习圈子 回复 20231014权限管理,获取本项目的源码和sql

3、项目演示地址
环境:
win10、idea22023.2、vscode1.7、maven3.5、jdk8、Redis
技术:
- springboot2.5.9
- springsecurity
- Redis
- mybatis plus
- mysql5.7.38
- swagger
- jwt
- vue2
- element ui
- vuex
- vue-router
- axios
- mockjs
- echarts
- less
模块:
登录页面
首页
权限管理
1.用户管理
2.角色管理
3.菜单管理
功能要点:
- 从0到1实现项目前后端搭建
- vuex实现面包屑和Tag功能
- vuex+localStorage动态路由和菜单(重点)
- 封装一个ECharts组件
- 封装Table表格
- 树形表格和CheckBox的使用
- 封装统一返回类
- 两种方式实现代码生成,解放双手
- jwt、swagger、Redis封装
- 权限管理(重点)
二、脚手架的搭建
1、vue 环境搭建
参照博客文章:vue安装node以及nrm、nrm配置,路由安装Vue CLI2初始化项目vue2
2、注意版本号

3、这里选择vue2

4、安装完成

5、启动

6、游览器访问

三、引入element-ui
1、npm安装
npm i element-ui -S
- 网页直接使用
2、脚手架中使用
全部引入 官网:[https://element.eleme.cn/#/zh-CN/component/quickstart] 在 main.js 中写入以下内容:
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';//引入element-ui
import 'element-ui/lib/theme-chalk/index.css';//引入element-ui的样式
Vue.config.productionTip = false
Vue.use(ElementUI);//全局注册
new Vue({
render: h => h(App),
}).$mount('#app')
App.vue加入element-ui的按钮
<template>
<div id="app">
<el-row>
<el-button>默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="info">信息按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">危险按钮</el-button>
</el-row>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
游览器效果 
四、路由的使用
1、路由官网:https://v3.router.vuejs.org/zh/installation.html
这里使用v3.x的版本 npm.jscom查看vue-router3的最新版本https://www.npmjs.com/package/vue-router?activeTab=versions
2、npm安装
安装最新版(install 可以简写为i)
npm install vue-router
安装指定版本
npm i vue-router@3.6.5
3、在src目录下新建router文件夹和index.js文件

4、如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装路由功能:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
如果使用全局的 script 标签,则无须如此 (手动安装)。 官网:https://v3.router.vuejs.org/zh/guide/#javascript
5、index.js配置
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/User.vue'
Vue.use(VueRouter)
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/home', component: Home },
{ path: '/user', component: User }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。挂载到main.js的根节点
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
// const app = new Vue({
// router
// }).$mount('#app')
export default router
6、main.js挂载路由
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import router from './router';//如果router文件夹下有index.js就不需要'./router/index.js'
Vue.config.productionTip = false
Vue.use(ElementUI);
new Vue({
router,//挂载路由
render: h => h(App),
}).$mount('#app')
7、路由出口
路由匹配到的组件将渲染在这里
<router-view></router-view>
8、App.vue配置路由出口
<template>
<div id="app">
<el-row>
<el-button>默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="info">信息按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">危险按钮</el-button>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-row>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
9、相关文件目录

10、启动服务,如果报错如下
解决办法,在vue.config.js文件里添加lintOnSave:false关闭eslint的校验。 
11、访问


12、嵌套路由
官网:https://v3.router.vuejs.org/zh/guide/essentials/nested-routes.html index.js和Main.vue配置如下
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/User.vue'
import Main from '@/views/Main.vue'
Vue.use(VueRouter)
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
//主路由
{
path: '/',
component: Main,
children: [
//子路由
{ path: 'home', component: Home },
{ path: 'user', component: User }
]
},
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。挂载到main.js的根节点
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
// const app = new Vue({
// router
// }).$mount('#app')
export default router
1、Main.vue需要配置路由出口
<template>
<div>
<h1>main</h1>
<!-- 子路由出口 -->
<!-- 子路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</template>
<script>
export default {
//name:"Home",
data() {
return {};
},
};
</script>
2、目录
3、访问效果 

五、首页架子的搭建
1、container的容器
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
2、Main.vue
<template>
<div>
<el-container>
<el-aside width="auto"><common-aside/></el-aside>
<el-container>
<el-header><common-header /></el-header>
<el-main>
main
<!-- 子路由出口 -->
<!-- 子路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import CommonAside from "@/components/CommonAside.vue";
import CommonHeader from "@/components/CommonHeader.vue";
export default {
name: "Main",
components: {
CommonAside,
CommonHeader,
},
data() {
return {};
},
};
</script>
<style scoped>
.el-header {
padding: 0;
}
</style>
3、侧边栏的实现
<template>
<div>
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>通用后台管理系统</h3>
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.path"
@click="clickMenu(item)"
>
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<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-group
v-for="subItem in item.children"
:key="subItem.path"
>
<el-menu-item :index="subItem.path" @click="clickMenu(subItem)">{{
subItem.label
}}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</template>
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu {
//height: calc(100vh - 0px);
border-right:0px;
height: 100vh;
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400px;
}
}
</style>
<script>
export default {
data() {
return {
isCollapse: false,
menuData: [
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
},
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
url: "Other/PageTwo",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
url: "Other/PageTwo",
},
],
},
],
};
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item) {
//当页面的路由和跳转的路由不一致才允许跳转
//this.$route.path 上一个路径,item.path下一个路径
if (this.$route.path !== item.path && !(this.$route.path ==='/home' && (item.path ==='/'))) {
this.$router.push(item.path);
}
},
},
computed: {
//没有子菜单
noChildren() {
return this.menuData.filter((item) => !item.children);
},
//有子菜单
hasChildren() {
return this.menuData.filter((item) => item.children);
},
},
};
</script>
4、less安装
npm i less@4.1.2
npm i less-loader@6.0.0
5、Main.vue
<template>
<div>
<el-container>
<el-aside width="auto"><CommonAside></CommonAside></el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>
main
<!-- 子路由出口 -->
<!-- 子路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import CommonAside from '@/components/CommonAside.vue';
export default {
//name:"Main",
components:{
CommonAside
},
data() {
return {};
},
};
</script>
6、CommonAside.vue
<template>
<div>
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>通用后台管理系统</h3>
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.name"
@click="clickMenu(item)"
>
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<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-group
v-for="subItem in item.children"
:key="subItem.path"
>
<el-menu-item :index="subItem.path" @click="clickMenu(subItem)">{{ subItem.label }}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</template>
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu {
//height: calc(100vh - 0px);
height: 100vh;
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400px;
}
}
</style>
<script>
export default {
data() {
return {
isCollapse: false,
menuData: [
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
},
{
label: "权限管理",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
url: "Other/PageTwo",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
url: "Other/PageTwo",
},
],
},
],
};
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item){
this.$router.push(item.path)
console.log(item)
},
},
computed: {
//没有子菜单
noChildren() {
return this.menuData.filter((item) => !item.children);
},
//有子菜单
hasChildren() {
return this.menuData.filter((item) => item.children);
},
},
};
</script>
7、App.vue清除默认样式
<template>
<div id="app">
<el-row>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-row>
</div>
</template>
<script>
export default {
name: "App",
};
</script>
<style lang="less">
//清除默认样式
html,
body,h3 {
margin: 0;
padding: 0;
}
</style>>
效果如下 
8、出现的小问题
连续点击同一个菜单多次,出现这个错误,虽然不影响路由跳转,但是为了完美,解决方法如下 
9、解决办法
1、方法判断
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item) {
//当页面的路由和跳转的路由不一致才允许跳转
//this.$route.path 上一个路径,item.path下一个路径
if (this.$route.path !== item.path
&& !(this.$route.path ==='/home' && (item.path ==='/'))) {
this.$router.push(item.path);
}
},
},
2、路由的index文件添加如下配置
// 防止连续点击多次路由报错
// 获取原型对象push函数
const originalPush = VueRouter.prototype.push
// 获取原型对象replace函数
const originalReplace = VueRouter.prototype.replace
// 修改原型对象中的push函数
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 修改原型对象中的replace函数
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
10、CommonHeader组件的实现
新增CommonHeader.vue组件
<template>
<div class="header-main">
<div class="l-content">
<el-button icon="el-icon-menu" size="mini"></el-button>
<!-- 面包屑 -->
<span class="text">首页</span>
</div>
<div class="r-content">
<el-dropdown>
<img src="@/assets/img/user.jpg" class="user">
<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>
export default {
data() {
return {};
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
},
};
</script>
<style lang="less" scoped>
.header-main {
padding: 0 20px;
background-color: #333;
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
.text {
color: #ffff;
font-size: 14px;
margin-left: 10px;
}
.r-content{
.user{
width:40px;
height: 40px;
border-radius: 50%;
}
}
}
</style>
main.vue
<template>
<div>
<el-container>
<el-aside width="auto"><common-aside/></el-aside>
<el-container>
<el-header><common-header /></el-header>
<el-main>
main
<!-- 子路由出口 -->
<!-- 子路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import CommonAside from "@/components/CommonAside.vue";
import CommonHeader from "@/components/CommonHeader.vue";
export default {
name: "Main",
components: {
CommonAside,
CommonHeader,
},
data() {
return {};
},
};
</script>
<style scoped>
.el-header {
padding: 0;
}
</style>
11、Vuex实现左侧折叠
官网:https://v3.vuex.vuejs.org/zh/ 1、下载依赖
npm i vuex@3.6.2
2、新建store文件夹并在此文件夹下新建index.js和menu.js文件 index
import Vue from 'vue'
import Vuex from 'vuex'
import menu from './menu'
Vue.use(Vuex)
//创建vuex的实例
export default new Vuex.Store({
modules: {
menu,
}
})
3、menu
export default {
state: {
isCollapse: false //用于控制菜单的展开收起
},
mutations: {
//用于控制菜单的展开收起
handleCollapse(state) {
state.isCollapse = !state.isCollapse
}
}
}
4、CommonHeader新增handleIsCollapse方法
<template>
<div class="header-main">
<div class="l-content">
<el-button
icon="el-icon-menu"
@click="handleIsCollapse"
size="mini"
></el-button>
<!-- 面包屑 -->
<span class="text">首页</span>
</div>
<div class="r-content">
<el-dropdown>
<img src="@/assets/img/user.jpg" class="user" />
<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>
export default {
data() {
return {};
},
methods: {
handleIsCollapse() {
this.$store.commit("handleCollapse");
},
},
};
</script>
<style lang="less" scoped>
.header-main {
padding: 0 20px;
background-color: #333;
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
.text {
color: #ffff;
font-size: 14px;
margin-left: 10px;
}
.r-content {
.user {
width: 40px;
height: 40px;
border-radius: 50%;
}
}
}
</style>
5、CommonAside新增isCollapse计算属性,注意计算属性和data里不能同事存在isCollapse。
<template>
<div>
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>{{ isCollapse ? "后台" : "通用后台管理系统" }}</h3>
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.path"
@click="clickMenu(item)"
>
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<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-group
v-for="subItem in item.children"
:key="subItem.path"
>
<el-menu-item :index="subItem.path" @click="clickMenu(subItem)">{{
subItem.label
}}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</template>
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu {
//height: calc(100vh - 0px);
border-right: 0px;
height: 100vh;
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400px;
}
}
</style>
<script>
export default {
data() {
return {
//isCollapse: false,
menuData: [
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
},
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
url: "Other/PageTwo",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
url: "Other/PageTwo",
},
],
},
],
};
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item) {
//当页面的路由和跳转的路由不一致才允许跳转
//this.$route.path 上一个路径,item.path下一个路径
if (
this.$route.path !== item.path &&
!(this.$route.path === "/home" && item.path === "/")
) {
this.$router.push(item.path);
}
},
},
computed: {
//没有子菜单
noChildren() {
return this.menuData.filter((item) => !item.children);
},
//有子菜单
hasChildren() {
return this.menuData.filter((item) => item.children);
},
//vuex取得菜单展开收起的值,但是data里不能重复定义
isCollapse() {
return this.$store.state.menu.isCollapse;
},
},
};
</script>
6、main.js挂载vuex
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import router from './router';//如果router文件夹下有index.js就不需要'./router/index.js'
import store from './store'
Vue.config.productionTip = false
Vue.use(ElementUI);
new Vue({
router,//挂载路由
store,
render: h => h(App),
}).$mount('#app')
效果如下
收起 
12、Home布局
1、用户信息
<template>
<el-row>
<el-col :span="8"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
</div></el-col
>
<el-col :span="16"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>
</template>
<script>
export default {
name: "Home",
data() {
return {};
},
};
</script>
<style lang="less" scoped>
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
</style>
效果如下
2、列表,这里封装了动态的表格列
<template>
<div class="main">
<el-row>
<el-col :span="8"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 20px">
<el-table :data="tableData" style="width: 100%">
<el-table-column
v-for="(val, key) in tableLabel"
:prop="key"
:label="val"
:key="key"
/>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table></el-card
>
</div></el-col
>
<el-col :span="16"
><div class="grid-content bg-purple-light"></div
></el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
tableLabel: {
name: "姓名",
date: "日期",
address: "地址",
},
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: " 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: " 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: " 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: " 1516 弄",
},
],
};
},
};
</script>
<style lang="less" scoped>
.main {
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
}
</style>
3、效果如下
4、首页右半部分
<template>
<div class="main">
<el-row>
<el-col :span="8"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 20px">
<el-table :data="tableData" style="width: 100%">
<el-table-column
v-for="(val, key) in tableLabel"
:prop="key"
:label="val"
:key="key"
/>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table></el-card
>
</div></el-col
>
<el-col :span="16">
<div class="home-right-info">
<el-card
v-for="item in countData"
:key="item.name"
:body-style="{ display: 'flex' }"
>
<i
class="icon"
:class="`el-icon-${item.icon}`"
:style="{ background: item.color }"
></i>
<div class="detail-info">
<p class="price">{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
countData: [
{
name: "样式1",
value: "样式1",
icon: "success",
color: "#2ec7c9",
},
{
name: "样式2",
value: "样式2",
icon: "star-on",
color: "#ffb980",
},
{
name: "样式3",
value: "样式3",
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "样式4",
value: "样式4",
icon: "user",
color: "#2ec7c9",
},
{
name: "样式5",
value: "样式5",
icon: "s-home",
color: "#ffb980",
},
{
name: "样式6",
value: "样式6",
icon: "folder",
color: "#5ab1ef",
},
],
tableLabel: {
name: "姓名",
date: "日期",
address: "地址",
},
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: " 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: " 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: " 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: " 1516 弄",
},
],
};
},
};
</script>
<style lang="less" scoped>
.main {
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
.home-right-info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.icon {
width: 80px;
height: 80px;
font-size: 30px;
text-align: center;
line-height: 80px;
color: #fff;
}
.detail-info {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.price {
font-size: 30px;
margin-bottom: 10px;
line-height: 30px;
height: 30px;
text-align: left;
}
.desc {
font-size: 14px;
color: #999;
text-align: left;
}
}
.el-card {
width: 32%;
margin-bottom: 20px;
}
}
}
</style>
5、效果如下 
13、Axios的使用
官网:https://www.axios-http.cn/docs/intro 1、npm安装 npm install axios
2、src目录下新建utils文件夹并在utils下新建request.js文件
import axios from "axios";
const http = axios.create({
baseURL: '/api',//通用请求的地址
timeout: 10000,//超时时间,10000毫秒,10秒
});
// 添加请求拦截器
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
3、src下新建api文件夹并新建index.js文件
import http from "@/utils/request";
//请求首页的数据
export const getData = () => {
return http.get('/home/getData');
}
4、在Home.vue首页使用
<template>
<div class="main">
<el-row>
<el-col :span="8"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 20px">
<el-table :data="tableData" style="width: 100%">
<el-table-column
v-for="(val, key) in tableLabel"
:prop="key"
:label="val"
:key="key"
/>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table></el-card
>
</div></el-col
>
<el-col :span="16">
<div class="home-right-info">
<el-card
v-for="item in countData"
:key="item.name"
:body-style="{ display: 'flex' }"
>
<i
class="icon"
:class="`el-icon-${item.icon}`"
:style="{ background: item.color }"
></i>
<div class="detail-info">
<p class="price">{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getData } from "@/api";
export default {
name: "Home",
data() {
return {
countData: [
{
name: "样式1",
value: "样式1",
icon: "success",
color: "#2ec7c9",
},
{
name: "样式2",
value: "样式2",
icon: "star-on",
color: "#ffb980",
},
{
name: "样式3",
value: "样式3",
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "样式4",
value: "样式4",
icon: "user",
color: "#2ec7c9",
},
{
name: "样式5",
value: "样式5",
icon: "s-home",
color: "#ffb980",
},
{
name: "样式6",
value: "样式6",
icon: "folder",
color: "#5ab1ef",
},
],
tableLabel: {
name: "姓名",
date: "日期",
address: "地址",
},
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: " 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: " 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: " 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: " 1516 弄",
},
],
};
},
mounted() {
getData().then((res) => {
console.log(res);
});
},
};
</script>
<style lang="less" scoped>
.main {
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
.home-right-info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.icon {
width: 80px;
height: 80px;
font-size: 30px;
text-align: center;
line-height: 80px;
color: #fff;
}
.detail-info {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.price {
font-size: 30px;
margin-bottom: 10px;
line-height: 30px;
height: 30px;
text-align: left;
}
.desc {
font-size: 14px;
color: #999;
text-align: left;
}
}
.el-card {
width: 32%;
margin-bottom: 20px;
}
}
}
</style>
5、游览器效果 因为没有后端,也没有用mock,所以这个是正常的 
14、Mock.js模拟数据
官网:http://mockjs.com/ 1、npm安装 npm install mockjs 2、在src下的api目录下新建mock.js文件
import Mock from 'mockjs'
import homeApi from './mockServerData/home';
// Mock.mock('/api/home/getData',function(){
// //拦截到請求後的處理邏輯
// console.log('攔截到了')
// })
Mock.mock('/api/home/getData', homeApi.getResultData)
3、在src下的api下新建mockServerData文件夹并新建home.js
// mock数据模拟
import Mock from 'mockjs'
// 图表数据
let List = []
export default {
getResultData: () => {
//Mock.Random.float 产生随机数100到8000之间 保留小数 最小0位 最大0位
for (let i = 0; i < 7; i++) {
List.push(
Mock.mock({
test1: Mock.Random.float(100, 8000, 0, 0),
test2: Mock.Random.float(100, 8000, 0, 0),
test3: Mock.Random.float(100, 8000, 0, 0),
test4: Mock.Random.float(100, 8000, 0, 0),
test5: Mock.Random.float(100, 8000, 0, 0),
test6: Mock.Random.float(100, 8000, 0, 0)
})
)
}
return {
code: 20000,
data: {
// 饼图
videoData: [
{
name: '小米',
value: 999
},
{
name: '苹果',
value: 1999
},
{
name: 'vivo',
value: 1800
},
{
name: 'oppo',
value: 5999
},
{
name: '魅族',
value: 4500
},
{
name: '三星',
value: 5500
}
],
// 柱状图
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: '王小虎1',
address: '北京1',
date: '2016-05-03',
},
{
name: '王小虎2',
address: '北京2',
date: '2016-05-03',
},
{
name: '王小虎3',
address: '北京3',
date: '2016-05-03',
},
{
name: '王小虎4',
address: '北京4',
date: '2016-05-03',
},
{
name: '王小虎5',
address: '北京5',
date: '2016-05-03',
},
{
name: '王小虎6',
address: '北京6',
date: '2016-05-03',
}
]
}
}
}
}
4、在main.js里导入
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import router from './router';//如果router文件夹下有index.js就不需要'./router/index.js'
import store from './store'
import '@/api/mock'
Vue.config.productionTip = false
Vue.use(ElementUI);
new Vue({
router,//挂载路由
store,
render: h => h(App),
}).$mount('#app')
5、效果如下 
15、可视化图表
1、页面布局 Home.vue
<template>
<div class="main">
<el-row>
<el-col :span="8" style="padding-right: 10px"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 20px">
<el-table :data="tableData" style="width: 100%">
<el-table-column
v-for="(val, key) in tableLabel"
:prop="key"
:label="val"
:key="key"
/>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table></el-card
>
</div></el-col
>
<el-col :span="16" style="padding-left: 10px">
<div class="home-right-info">
<el-card
v-for="item in countData"
:key="item.name"
:body-style="{ display: 'flex' }"
>
<i
class="icon"
:class="`el-icon-${item.icon}`"
:style="{ background: item.color }"
></i>
<div class="detail-info">
<p class="price">{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
<el-card style="height: 280px">
<!-- 折线图 -->
</el-card>
<div class="home-right-bottom">
<el-card></el-card>
<el-card></el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getData } from "@/api";
export default {
name: "Home",
data() {
return {
countData: [
{
name: "样式1",
value: "样式1",
icon: "success",
color: "#2ec7c9",
},
{
name: "样式2",
value: "样式2",
icon: "star-on",
color: "#ffb980",
},
{
name: "样式3",
value: "样式3",
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "样式4",
value: "样式4",
icon: "user",
color: "#2ec7c9",
},
{
name: "样式5",
value: "样式5",
icon: "s-home",
color: "#ffb980",
},
{
name: "样式6",
value: "样式6",
icon: "folder",
color: "#5ab1ef",
},
],
tableLabel: {
name: "姓名",
date: "日期",
address: "地址",
},
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: " 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: " 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: " 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: " 1516 弄",
},
],
};
},
mounted() {
getData().then((res) => {
this.tableData = res.data.data.tableData;
console.log(res);
});
},
};
</script>
<style lang="less" scoped>
.main {
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
.home-right-info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.icon {
width: 80px;
height: 80px;
font-size: 30px;
text-align: center;
line-height: 80px;
color: #fff;
}
.detail-info {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.price {
font-size: 30px;
margin-bottom: 10px;
line-height: 30px;
height: 30px;
text-align: left;
}
.desc {
font-size: 14px;
color: #999;
text-align: left;
}
}
.el-card {
width: 32%;
margin-bottom: 20px;
}
}
.home-right-bottom {
display: flex;
justify-content: space-between;
.el-card {
width: 48%;
height: 260px;
margin-top: 20px;
}
}
}
</style>
2、main.vue样式调整 添加如下样式(添加这个主要是电脑的分辨率不同页面的样式可能会错乱,就是左边菜单和首页的高度不在一个水平线)
.el-main {
height: calc(100vh - 70px);
}
完整文件
<template>
<div>
<el-container>
<el-aside width="auto"><common-aside /></el-aside>
<el-container>
<el-header><common-header /></el-header>
<el-main>
<!-- 子路由出口 -->
<!-- 子路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import CommonAside from "@/components/CommonAside.vue";
import CommonHeader from "@/components/CommonHeader.vue";
export default {
name: "Main",
components: {
CommonAside,
CommonHeader,
},
data() {
return {};
},
};
</script>
<style scoped>
.el-header {
padding: 0;
}
.el-main {
height: calc(100vh - 70px);
}
</style>
3、效果如下 
16、Echarts
1、官网:https://echarts.apache.org/zh/index.html 快速上手:https://echarts.apache.org/handbook/zh/get-started 2、npm安装 注意指定版本
npm install echarts@5.1.2
3、home.vue页面引入 import * as echarts from 'echarts' 4、编写代码
<template>
<div class="main">
<el-row>
<el-col :span="8" style="padding-right: 10px"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 20px">
<el-table :data="tableData" style="width: 100%">
<el-table-column
v-for="(val, key) in tableLabel"
:prop="key"
:label="val"
:key="key"
/>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table></el-card
>
</div></el-col
>
<el-col :span="16" style="padding-left: 10px">
<div class="home-right-info">
<el-card
v-for="item in countData"
:key="item.name"
:body-style="{ display: 'flex' }"
>
<i
class="icon"
:class="`el-icon-${item.icon}`"
:style="{ background: item.color }"
></i>
<div class="detail-info">
<p class="price">{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
<el-card style="height: 280px">
<!-- 折线图 -->
<div ref="echartsLine" style="height:280px;"></div>
</el-card>
<div class="home-right-bottom">
<el-card></el-card>
<el-card></el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getData } from "@/api";
import * as echarts from "echarts";
export default {
name: "Home",
data() {
return {
countData: [
{
name: "样式1",
value: "样式1",
icon: "success",
color: "#2ec7c9",
},
{
name: "样式2",
value: "样式2",
icon: "star-on",
color: "#ffb980",
},
{
name: "样式3",
value: "样式3",
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "样式4",
value: "样式4",
icon: "user",
color: "#2ec7c9",
},
{
name: "样式5",
value: "样式5",
icon: "s-home",
color: "#ffb980",
},
{
name: "样式6",
value: "样式6",
icon: "folder",
color: "#5ab1ef",
},
],
tableLabel: {
name: "姓名",
date: "日期",
address: "地址",
},
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: " 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: " 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: " 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: " 1516 弄",
},
],
};
},
mounted() {
getData().then((res) => {
this.tableData = res.data.data.tableData;
//需要定义一个<div ref="echartsLine" style="height:280px;"></div>
// 1、基于准备好的dom,初始化echarts实例
const myChartLine = echarts.init(this.$refs.echartsLine);
//2、 指定图表的配置项和数据
var optionLine = {};
//3、获取数据 xAxis
const { orderData } = res.data.data;
const xAxisLine = Object.keys(orderData.data[0]);
const xAxisData = { data: xAxisLine };
optionLine.xAxis = xAxisData;
optionLine.yAxis = {};
optionLine.legend = xAxisData;
optionLine.series = [];
//封装series
xAxisLine.forEach((key) => {
optionLine.series.push({
name: key,
data: orderData.data.map((item) => item[key]),
type: "line",
});
});
console.log(optionLine);
//4、使用刚指定的配置项和数据显示图表。
myChartLine.setOption(optionLine);
});
},
};
</script>
<style lang="less" scoped>
.main {
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
.home-right-info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.icon {
width: 80px;
height: 80px;
font-size: 30px;
text-align: center;
line-height: 80px;
color: #fff;
}
.detail-info {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.price {
font-size: 30px;
margin-bottom: 10px;
line-height: 30px;
height: 30px;
text-align: left;
}
.desc {
font-size: 14px;
color: #999;
text-align: left;
}
}
.el-card {
width: 32%;
margin-bottom: 20px;
}
}
.home-right-bottom {
display: flex;
justify-content: space-between;
.el-card {
width: 48%;
height: 260px;
margin-top: 20px;
}
}
}
</style>
5、如果报错如下,需要给div定义一个高度
Can't get DOM width or height. Please check dom.clientWidth and dom.clientHeight. They should not be 0.For example, you may need to call this in the callback of window.onload.
6、数据结构
7、官网例子代码 注意官网的这个是柱状图,通过type=bar指定,这个项目上边用的是折线图,type:=line, 在绘图前我们需要为 ECharts 准备一个定义了高宽的 DOM 容器。在刚才的例子 head 之后,添加:
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
</body>
然后就可以通过 echarts.init 方法初始化一个 echarts 实例并通过 setOption 方法生成一个简单的柱状图,下面是完整代码。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
这样你的第一个图表就诞生了! 
8、本项目的效果
9、柱状图和饼状图
<template>
<div class="main">
<el-row>
<el-col :span="8" style="padding-right: 10px"
><div class="grid-content bg-purple">
<el-card class="box-card">
<div class="user-content">
<img src="@/assets/img/user.jpg" class="user-img" />
<div class="user-info">
<p class="user-name">Admin</p>
<p class="user-access">超级管理员</p>
</div>
</div>
<div class="login-content">
<p>上次登录的时间:<span>2023-02-25</span></p>
<p>上次登录的地点:<span>西安</span></p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 20px">
<el-table :data="tableData" style="width: 100%">
<el-table-column
v-for="(val, key) in tableLabel"
:prop="key"
:label="val"
:key="key"
/>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table></el-card
>
</div></el-col
>
<el-col :span="16" style="padding-left: 10px">
<div class="home-right-info">
<el-card
v-for="item in countData"
:key="item.name"
:body-style="{ display: 'flex' }"
>
<i
class="icon"
:class="`el-icon-${item.icon}`"
:style="{ background: item.color }"
></i>
<div class="detail-info">
<p class="price">{{ item.value }}</p>
<p class="desc">{{ item.name }}</p>
</div>
</el-card>
</div>
<el-card style="height: 280px">
<!-- 折线图 -->
<div ref="echartsLine" style="height: 280px"></div>
</el-card>
<div class="home-right-bottom">
<el-card style="height: 260px">
<!-- 柱状图 -->
<div ref="echartsBar1" style="height: 260px"></div>
</el-card>
<el-card style="height: 260px">
<!-- 柱状图 -->
<div ref="echartsPie" style="height: 240px"></div>
</el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getData } from "@/api";
import * as echarts from "echarts";
export default {
name: "Home",
data() {
return {
countData: [
{
name: "样式1",
value: "样式1",
icon: "success",
color: "#2ec7c9",
},
{
name: "样式2",
value: "样式2",
icon: "star-on",
color: "#ffb980",
},
{
name: "样式3",
value: "样式3",
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "样式4",
value: "样式4",
icon: "user",
color: "#2ec7c9",
},
{
name: "样式5",
value: "样式5",
icon: "s-home",
color: "#ffb980",
},
{
name: "样式6",
value: "样式6",
icon: "folder",
color: "#5ab1ef",
},
],
tableLabel: {
name: "姓名",
date: "日期",
address: "地址",
},
tableData: [
{
date: "2016-05-02",
name: "王小虎",
address: " 1518 弄",
},
{
date: "2016-05-04",
name: "王小虎",
address: " 1517 弄",
},
{
date: "2016-05-01",
name: "王小虎",
address: " 1519 弄",
},
{
date: "2016-05-03",
name: "王小虎",
address: " 1516 弄",
},
],
};
},
mounted() {
getData().then((res) => {
this.tableData = res.data.data.tableData;
//折线图start
// 1、基于准备好的dom,初始化echarts实例
const myChartLine = echarts.init(this.$refs.echartsLine);
//2、 指定图表的配置项和数据
var optionLine = {};
//3、获取数据 xAxis
const { orderData, userData, videoData } = res.data.data;
const xAxisLine = Object.keys(orderData.data[0]);
const xAxisData = { data: xAxisLine };
optionLine.xAxis = xAxisData;
optionLine.yAxis = {};
optionLine.legend = xAxisData;
optionLine.series = [];
xAxisLine.forEach((key) => {
optionLine.series.push({
name: key,
data: orderData.data.map((item) => item[key]),
type: "line",
});
});
console.log(optionLine);
//4、使用刚指定的配置项和数据显示图表。
myChartLine.setOption(optionLine);
//折线图end
//柱状图1start
// 1、基于准备好的dom,初始化echarts实例
const myChartBar1 = echarts.init(this.$refs.echartsBar1);
//2、 指定图表的配置项和数据
var optionBar1 = {
legend: {
// 图例文字颜色
textStyle: {
color: "#333",
},
},
grid: {
left: "20%",
},
// 提示框
tooltip: {
trigger: "axis",
},
xAxis: {
type: "category", // 类目轴
data: [],
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) => {
return item.new;
}),
type: "bar",
},
{
name: "活跃用户",
data: userData.map((item) => {
return item.active;
}),
type: "bar",
},
// {
// name: "新增用户",
// data: userData.map((item) => item.new),
// type: "bar",
// },
// {
// name: "活跃用户",
// data: userData.map((item) => item.active),
// type: "bar",
// },
],
};
//4、使用刚指定的配置项和数据显示图表。
myChartBar1.setOption(optionBar1);
//柱状图1end
//饼状图1start
// 1、基于准备好的dom,初始化echarts实例
const myChartPie = echarts.init(this.$refs.echartsPie);
//2、 指定图表的配置项和数据
//饼状图1end
const optionPie = {
tooltip: {
trigger: "item",
},
color: [
"#0f78f4",
"#dd536b",
"#9462e5",
"#a6a6a6",
"#e1bb22",
"#39c362",
"#3ed1cf",
],
series: [{ data: videoData, type: "pie" }],
};
myChartPie.setOption(optionPie);
});
},
};
</script>
<style lang="less" scoped>
.main {
.user-content {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 1px solid#ccc;
display: flex;
align-items: center; //垂直居中
.user-img {
width: 150px;
height: 150px;
border-radius: 50%;
margin-right: 40px;
}
.user-info {
.user-name {
font-size: 32px;
margin-bottom: 10px;
}
.user-access {
color: #999999;
}
}
}
.login-content {
p {
line-height: 28px;
font-size: 14px;
color: #999999;
span {
color: #666666;
margin-left: 60px;
}
}
}
.home-right-info {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.icon {
width: 80px;
height: 80px;
font-size: 30px;
text-align: center;
line-height: 80px;
color: #fff;
}
.detail-info {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.price {
font-size: 30px;
margin-bottom: 10px;
line-height: 30px;
height: 30px;
text-align: left;
}
.desc {
font-size: 14px;
color: #999;
text-align: left;
}
}
.el-card {
width: 32%;
margin-bottom: 20px;
}
}
.home-right-bottom {
display: flex;
justify-content: space-between;
.el-card {
width: 48%;
height: 260px;
margin-top: 20px;
}
}
}
</style>
10、效果
17、面包屑
1、分析 1.1、默认有一个首页 1.2、点击菜单添加一个面包屑 1.3、选择已有数据,不会重复添加 1.4、使用vuex管理面包屑 2、实现 官网:https://element.eleme.cn/#/zh-CN/component/breadcrumb 2.1、CommonAside.vue点击左侧菜单的事件里触发vuex的menu.js的breadcrumbChange的mutations。
<template>
<div>
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>{{ isCollapse ? "后台" : "通用后台管理系统" }}</h3>
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.path"
@click="clickMenu(item)"
>
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<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-group
v-for="subItem in item.children"
:key="subItem.path"
>
<el-menu-item :index="subItem.path" @click="clickMenu(subItem)">{{
subItem.label
}}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</template>
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu {
//height: calc(100vh - 0px);
border-right: 0px;
height: 100vh;
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400px;
}
}
</style>
<script>
export default {
data() {
return {
//isCollapse: false,
menuData: [
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
},
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
url: "Other/PageTwo",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
url: "Other/PageTwo",
},
],
},
],
};
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item) {
//当页面的路由和跳转的路由不一致才允许跳转
//this.$route.path 上一个路径,item.path下一个路径
if (
this.$route.path !== item.path &&
!(this.$route.path === "/home" && item.path === "/")
) {
this.$router.push(item.path);
}
this.$store.commit("breadcrumbChange", item);
},
},
computed: {
//没有子菜单
noChildren() {
return this.menuData.filter((item) => !item.children);
},
//有子菜单
hasChildren() {
return this.menuData.filter((item) => item.children);
},
//vuex取得菜单展开收起的值,但是data里不能重复定义
isCollapse() {
return this.$store.state.menu.isCollapse;
},
},
};
</script>
2.2、在menu.js里新增breadcrumbList的state和breadcrumbChange的mutations。
export default {
state: {
isCollapse: false, //用于控制菜单的展开收起
breadcrumbList: [
//默认的数据
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
}
]//面包屑的数据
},
mutations: {
//用于控制菜单的展开收起
handleCollapse(state) {
state.isCollapse = !state.isCollapse
},
//更新面包屑数据
breadcrumbChange(state, val) {
//判断添加的数据是否是首页
if (val.name !== 'name') {
const index = state.breadcrumbList.findIndex(item => item.name === val.name)
//如果不存在就添加道面包屑数组里
if (index === -1) {
state.breadcrumbList.push(val)
}
}
}
}
}
2.3、CommonHeader.vue添加面包屑和计算属性breadcrumbList,从vuex获取breadcrumbList
<template>
<div class="header-main">
<div class="l-content">
<el-button
icon="el-icon-menu"
@click="handleIsCollapse"
size="mini"
></el-button>
<!-- 面包屑 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="item in breadcrumbList"
:key="item.path"
:to="{ path: item.path }"
>{{ item.label }}</el-breadcrumb-item
>
</el-breadcrumb>
</div>
<div class="r-content">
<el-dropdown>
<img src="@/assets/img/user.jpg" class="user" />
<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>
import { mapState } from "vuex";
export default {
data() {
return {};
},
methods: {
handleIsCollapse() {
this.$store.commit("handleCollapse");
},
},
computed: {
...mapState({
breadcrumbList: (state) => state.menu.breadcrumbList,
}),
},
};
</script>
<style lang="less" scoped>
.header-main {
padding: 0 20px;
background-color: #333;
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
.text {
color: #ffff;
font-size: 14px;
margin-left: 10px;
}
.r-content {
.user {
width: 40px;
height: 40px;
border-radius: 50%;
}
}
.l-content {
display: flex;
align-items: center;
///deep/样式穿刺
/deep/.el-breadcrumb__item {
.el-breadcrumb__inner {
font-weight: normal;
//设置面包屑字体为灰色,最后一个除外
//<span role="link" class="el-breadcrumb__inner is-link">菜单管理</span>
&.is-link {
color: #666;
}
}
//设置最后一个面包屑字体为白色
//<span role="link" class="el-breadcrumb__inner is-link">用户管理</span>
&:last-child {
.el-breadcrumb__inner {
color: #fff;
}
}
}
}
}
</style>
2.4、页面样式
2.6、效果 
18、Tag 标签
1、官网:https://element.eleme.cn/#/zh-CN/component/tag 2、新建组件CommonTag.vue
<template>
<div class="tag-content">
<el-tag
v-for="item in tagList"
:key="item.label"
:closable="item.name !== 'home'"
:effect="item.name === $route.name ? 'dark' : 'plain'"
>
{{ item.label }}
</el-tag>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "CommonTag",
data() {
return {};
},
computed: {
...mapState({
tagList: (state) => state.menu.breadcrumbList,//这里实际和面包屑是一个数据
}),
},
};
</script>
3、main.vue引入tag组件
<template>
<div>
<el-container>
<el-aside width="auto"><common-aside /></el-aside>
<el-container>
<el-header><common-header /></el-header>
<common-tag/>
<el-main>
<!-- 子路由出口 -->
<!-- 子路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import CommonAside from "@/components/CommonAside.vue";
import CommonHeader from "@/components/CommonHeader.vue";
import CommonTag from "@/components/CommonTag.vue"
export default {
name: "Main",
components: {
CommonAside,
CommonHeader,
CommonTag,
},
data() {
return {};
},
};
</script>
<style scoped>
.el-header {
padding: 0;
}
.el-main {
height: calc(100vh - 70px);
}
</style>
4、route下的index.js文件的路由加上name属性,否则tag页面的判断无效
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/User.vue'
import Main from '@/views/Main.vue'
Vue.use(VueRouter)
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
//主路由
{
path: '/',
component: Main,
redirect: '/home',
children: [
//子路由
{ path: 'home', name: "home", component: Home },//首页
{ path: 'user', name: "user", component: User },//用户管理
{ path: 'role', name: "role", component: () => import("@/views/Role.vue") },//角色管理
{ path: 'menu', name: "menu", component: () => import("@/views/Menu.vue") },//菜单管理管理
]
},
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。挂载到main.js的根节点
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
// const app = new Vue({
// router
// }).$mount('#app')
// 获取原型对象push函数
const originalPush = VueRouter.prototype.push
// 获取原型对象replace函数
const originalReplace = VueRouter.prototype.replace
// 修改原型对象中的push函数
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 修改原型对象中的replace函数
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
export default router
加入以下内容是为了解决多次点击面包屑跳转控制台报错的问题
// 获取原型对象push函数
const originalPush = VueRouter.prototype.push
// 获取原型对象replace函数
const originalReplace = VueRouter.prototype.replace
// 修改原型对象中的push函数
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 修改原型对象中的replace函数
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
5、效果
6、tag点击跳转、删除、样式 6.1、menu.js定义删除指定的tag的mutations的closeTag方法
export default {
state: {
isCollapse: false, //用于控制菜单的展开收起
breadcrumbList: [
//默认的数据
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
}
]//面包屑的数据
},
mutations: {
//删除指定的tag
closeTag(state, item) {
const index = state.breadcrumbList.findIndex(val => { return val.name === item.name })
state.breadcrumbList.splice(index, 1)
},
//用于控制菜单的展开收起
handleCollapse(state) {
state.isCollapse = !state.isCollapse
},
//更新面包屑数据
breadcrumbChange(state, val) {
//判断添加的数据是否是首页
if (val.name !== 'name') {
const index = state.breadcrumbList.findIndex(item => item.name === val.name)
//如果不存在就添加道面包屑数组里
if (index === -1) {
state.breadcrumbList.push(val)
}
}
}
}
}
6.2、tag页面定义changeTag的click事件和close的handleClose事件 6.3、使用mapMutations辅助函数 6.4、删除逻辑
//点击删除标签
handleClose(item, index) {
//
//const length = this.tagList.length -1
//this.$store.commit('')
//调用store中的mutations
this.closeTag(item);
const length = this.tagList.length;
//删除之后的跳转功能
//1、如果要删除的tag不是当前高亮选中的路由,不做处理
if (item.name !== this.$route.name) {
return;
}
//2、如果要删除的tag和length相等,表示删除的是最后一项,自动向左移动,跳转到前一级
if (index === length) {
this.$router.push({ name: this.tagList[index - 1].name });
}
//3、如果要删除的tag是中间的,自动向后移动,跳转到后一级
else {
this.$router.push({ name: this.tagList[index].name });
}
}
6.5、完整代码
<template>
<div class="tag-content">
<el-tag
size="small"
v-for="(item, index) in tagList"
:key="item.label"
:closable="item.name !== 'home'"
:effect="item.name === $route.name ? 'dark' : 'plain'"
@click="changeTag(item)"
@close="handleClose(item, index)"
>
{{ item.label }}
</el-tag>
</div>
</template>
<script>
import { mapState, mapMutations } from "vuex";
export default {
name: "CommonTag",
data() {
return {};
},
methods: {
...mapMutations(["closeTag"]),
//点击删除标签
handleClose(item, index) {
//
//const length = this.tagList.length -1
//this.$store.commit('')
//调用store中的mutations
this.closeTag(item);
const length = this.tagList.length;
//删除之后的跳转功能
//1、如果要删除的tag不是当前高亮选中的路由,不做处理
if (item.name !== this.$route.name) {
return;
}
//2、如果要删除的tag和length相等,表示删除的是最后一项,自动向左移动,跳转到前一级
if (index === length) {
this.$router.push({ name: this.tagList[index - 1].name });
}
//3、如果要删除的tag是中间的,自动向后移动,跳转到后一级
else {
this.$router.push({ name: this.tagList[index].name });
}
},
//点击tag跳转页面
changeTag(item) {
//this.$router.push('/home')
this.$router.push({ name: item.name });
},
},
computed: {
...mapState({
tagList: (state) => state.menu.breadcrumbList, //这里实际和面包屑是一个数据
}),
},
};
</script>
<style lang="less" scoped>
.tag-content {
padding: 20px;
.el-tag {
margin-right: 15px;
cursor: pointer;
}
}
</style>
7、样式
<style lang="less" scoped>
.tag-content {
padding: 20px;
.el-tag {
margin-right: 15px;
cursor: pointer;
}
}
</style>
8、效果 
六、用户管理
1、用户表单新增页面(这个页面后边会有调整,可以参考下)
<template>
<div class="user">
<el-dialog :title="title" :visible.sync="dialogVisible" width="35%">
<!-- 用户表单信息 -->
<el-form
ref="model"
:model="model"
:inline="true"
label-width="80px"
:rules="rules"
>
<el-form-item label="用户名称" prop="userName">
<el-input v-model="model.userName"></el-input>
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="model.nickName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="model.password"></el-input>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="model.sex" placeholder="性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="生日" prop="birthday">
<el-date-picker
v-model="model.birthday"
type="date"
placeholder="选择日期"
>
</el-date-picker>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submit">确 定</el-button>
</span>
</el-dialog>
<div class="user-header">
<el-button type="primary" @click="dialogVisible = true">+新增</el-button>
</div>
</div>
</template>
<script>
export default {
name: "user",
data() {
return {
title: "新增",
model: {
userName: "",
password: "",
nickName: "",
sex: "",
birthday: "",
},
dialogVisible: false,
rules: {
userName: [
{ required: true, message: "请输入用户名称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
password: [
{ required: true, message: "请输入密码", trigger: "change" },
],
nickName: [
{ required: true, message: "请输入用户昵称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
sex: [{ required: true, message: "请选择性别", trigger: "change" }],
birthday: [
{
type: "date",
required: true,
message: "请选择日期",
trigger: "change",
},
],
},
};
},
methods: {
submit() {
this.$refs.model.validate((valid) => {
if (valid) {
//验证通过才进行数据请求
this.model
}
});
},
},
};
</script>
<style lang="less" scoped>
.el-el-input {
width: 80%;
}
</style>
2、页面效果
点击新增 
七、SpringBoot框架搭建
软件版本
- jdk 1.8
- mysql5.7+
1、如图新建springboot项目


2、developer tools选择lombok,简化代码
3、web选择spring web
4、sql选择mybatis和mysql

5、点击finish

6、生成后的项目

7、pom.xml文件的版本号修改为2.5.9,mysql驱动换了,最好替换成下边的,版本一致
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
8、配置阿里云,让maven下载依赖更快一些,注意位置,这里是配置阿里云的代码,上边是完整的代码。
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
9、重新加载maven

10、删除多余的文件
删除后的目录 
11、启动项目

12、报错如下,这个是因为没有连接数据库,在此之前需要在mysql里新建一个数据库(文末会给sql)
"C:\Program Files\Java\jdk1.8.0\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2021.1.1\lib\idea_rt.jar=10755:D:\Program Files\JetBrains\IntelliJ IDEA 2021.1.1\bin" -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar;E:\20230406WorkSpace\20230407MyServer\target\classes;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-web\2.5.9\spring-boot-starter-web-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter\2.5.9\spring-boot-starter-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot\2.5.9\spring-boot-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-autoconfigure\2.5.9\spring-boot-autoconfigure-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-logging\2.5.9\spring-boot-starter-logging-2.5.9.jar;E:\ZXJYRepository\ch\qos\logback\logback-classic\1.2.10\logback-classic-1.2.10.jar;E:\ZXJYRepository\ch\qos\logback\logback-core\1.2.10\logback-core-1.2.10.jar;E:\ZXJYRepository\org\apache\logging\log4j\log4j-to-slf4j\2.17.1\log4j-to-slf4j-2.17.1.jar;E:\ZXJYRepository\org\apache\logging\log4j\log4j-api\2.17.1\log4j-api-2.17.1.jar;E:\ZXJYRepository\org\slf4j\jul-to-slf4j\1.7.33\jul-to-slf4j-1.7.33.jar;E:\ZXJYRepository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;E:\ZXJYRepository\org\yaml\snakeyaml\1.28\snakeyaml-1.28.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-json\2.5.9\spring-boot-starter-json-2.5.9.jar;E:\ZXJYRepository\com\fasterxml\jackson\core\jackson-databind\2.12.6\jackson-databind-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\core\jackson-annotations\2.12.6\jackson-annotations-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\core\jackson-core\2.12.6\jackson-core-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.12.6\jackson-datatype-jdk8-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.12.6\jackson-datatype-jsr310-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.12.6\jackson-module-parameter-names-2.12.6.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-tomcat\2.5.9\spring-boot-starter-tomcat-2.5.9.jar;E:\ZXJYRepository\org\apache\tomcat\embed\tomcat-embed-core\9.0.56\tomcat-embed-core-9.0.56.jar;E:\ZXJYRepository\org\apache\tomcat\embed\tomcat-embed-el\9.0.56\tomcat-embed-el-9.0.56.jar;E:\ZXJYRepository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.56\tomcat-embed-websocket-9.0.56.jar;E:\ZXJYRepository\org\springframework\spring-web\5.3.15\spring-web-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-beans\5.3.15\spring-beans-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-webmvc\5.3.15\spring-webmvc-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-aop\5.3.15\spring-aop-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-context\5.3.15\spring-context-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-expression\5.3.15\spring-expression-5.3.15.jar;E:\ZXJYRepository\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.3.1\mybatis-spring-boot-starter-2.3.1.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-jdbc\2.5.9\spring-boot-starter-jdbc-2.5.9.jar;E:\ZXJYRepository\com\zaxxer\HikariCP\4.0.3\HikariCP-4.0.3.jar;E:\ZXJYRepository\org\springframework\spring-jdbc\5.3.15\spring-jdbc-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-tx\5.3.15\spring-tx-5.3.15.jar;E:\ZXJYRepository\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.3.1\mybatis-spring-boot-autoconfigure-2.3.1.jar;E:\ZXJYRepository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar;E:\ZXJYRepository\org\mybatis\mybatis-spring\2.1.1\mybatis-spring-2.1.1.jar;E:\ZXJYRepository\mysql\mysql-connector-java\8.0.28\mysql-connector-java-8.0.28.jar;E:\ZXJYRepository\org\projectlombok\lombok\1.18.22\lombok-1.18.22.jar;E:\ZXJYRepository\org\slf4j\slf4j-api\1.7.33\slf4j-api-1.7.33.jar;E:\ZXJYRepository\org\springframework\spring-core\5.3.15\spring-core-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-jcl\5.3.15\spring-jcl-5.3.15.jar" com.stu.myserver.Application
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.9)
2023-06-13 20:30:16.228 INFO 2524 --- [ main] com.stu.myserver.Application : Starting Application using Java 1.8.0 on konglx with PID 2524 (E:\20230406WorkSpace\20230407MyServer\target\classes started by 小明的学习圈子 in E:\20230406WorkSpace\20230407MyServer)
2023-06-13 20:30:16.228 INFO 2524 --- [ main] com.stu.myserver.Application : No active profile set, falling back to default profiles: default
2023-06-13 20:30:18.603 WARN 2524 --- [ main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.stu.myserver]' package. Please check your configuration.
2023-06-13 20:30:20.071 INFO 2524 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-06-13 20:30:20.149 INFO 2524 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-06-13 20:30:20.149 INFO 2524 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.56]
2023-06-13 20:30:20.368 INFO 2524 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-06-13 20:30:20.368 INFO 2524 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 3983 ms
2023-06-13 20:30:21.071 WARN 2524 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
2023-06-13 20:30:21.071 INFO 2524 --- [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2023-06-13 20:30:21.149 INFO 2524 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-06-13 20:30:21.274 ERROR 2524 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Process finished with exit code 1
13、解决办法(2种解放方式)
1、可以在启动类添加配置@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)先排除数据库
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
2、配置yml或者properties文件
properties
server.port=9090
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/2023Java?serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=study
yml
server:
port: 9090
spring:
profiles:
## 环境设置
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/2023Java?serverTimezone=Asia/Shanghai
username: root
password: study
## 响应 json 的全局时间格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
##mybatis 日志设置
mybatis-plus:
configuration:
## 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
serverTimezone这个时区要设置好,不然会出现时差, 如果你设置serverTimezone=UTC,连接不报错, 但是我们在用java代码插入到数据库时间的时候却出现了问题。 比如在java代码里面插入的时间为:2021-06-24 17:29:56 但是在数据库里面显示的时间却为:2021-06-24 09:29:56 有了8个小时的时差 UTC代表的是全球标准时间 ,但是我们使用的时间是北京时区也就是东八区,领先UTC八个小时。 //北京时间==东八区时间!=北京当地时间 serverTimezone=GMT%2B8 //或者使用上海时间 serverTimezone=Asia/Shanghai
14、再次启动,启动正常
"C:\Program Files\Java\jdk1.8.0\bin\java.exe" -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2021.1.1\lib\idea_rt.jar=10807:D:\Program Files\JetBrains\IntelliJ IDEA 2021.1.1\bin" -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar;E:\20230406WorkSpace\20230407MyServer\target\classes;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-web\2.5.9\spring-boot-starter-web-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter\2.5.9\spring-boot-starter-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot\2.5.9\spring-boot-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-autoconfigure\2.5.9\spring-boot-autoconfigure-2.5.9.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-logging\2.5.9\spring-boot-starter-logging-2.5.9.jar;E:\ZXJYRepository\ch\qos\logback\logback-classic\1.2.10\logback-classic-1.2.10.jar;E:\ZXJYRepository\ch\qos\logback\logback-core\1.2.10\logback-core-1.2.10.jar;E:\ZXJYRepository\org\apache\logging\log4j\log4j-to-slf4j\2.17.1\log4j-to-slf4j-2.17.1.jar;E:\ZXJYRepository\org\apache\logging\log4j\log4j-api\2.17.1\log4j-api-2.17.1.jar;E:\ZXJYRepository\org\slf4j\jul-to-slf4j\1.7.33\jul-to-slf4j-1.7.33.jar;E:\ZXJYRepository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;E:\ZXJYRepository\org\yaml\snakeyaml\1.28\snakeyaml-1.28.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-json\2.5.9\spring-boot-starter-json-2.5.9.jar;E:\ZXJYRepository\com\fasterxml\jackson\core\jackson-databind\2.12.6\jackson-databind-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\core\jackson-annotations\2.12.6\jackson-annotations-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\core\jackson-core\2.12.6\jackson-core-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.12.6\jackson-datatype-jdk8-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.12.6\jackson-datatype-jsr310-2.12.6.jar;E:\ZXJYRepository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.12.6\jackson-module-parameter-names-2.12.6.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-tomcat\2.5.9\spring-boot-starter-tomcat-2.5.9.jar;E:\ZXJYRepository\org\apache\tomcat\embed\tomcat-embed-core\9.0.56\tomcat-embed-core-9.0.56.jar;E:\ZXJYRepository\org\apache\tomcat\embed\tomcat-embed-el\9.0.56\tomcat-embed-el-9.0.56.jar;E:\ZXJYRepository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.56\tomcat-embed-websocket-9.0.56.jar;E:\ZXJYRepository\org\springframework\spring-web\5.3.15\spring-web-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-beans\5.3.15\spring-beans-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-webmvc\5.3.15\spring-webmvc-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-aop\5.3.15\spring-aop-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-context\5.3.15\spring-context-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-expression\5.3.15\spring-expression-5.3.15.jar;E:\ZXJYRepository\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.3.1\mybatis-spring-boot-starter-2.3.1.jar;E:\ZXJYRepository\org\springframework\boot\spring-boot-starter-jdbc\2.5.9\spring-boot-starter-jdbc-2.5.9.jar;E:\ZXJYRepository\com\zaxxer\HikariCP\4.0.3\HikariCP-4.0.3.jar;E:\ZXJYRepository\org\springframework\spring-jdbc\5.3.15\spring-jdbc-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-tx\5.3.15\spring-tx-5.3.15.jar;E:\ZXJYRepository\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.3.1\mybatis-spring-boot-autoconfigure-2.3.1.jar;E:\ZXJYRepository\org\mybatis\mybatis\3.5.13\mybatis-3.5.13.jar;E:\ZXJYRepository\org\mybatis\mybatis-spring\2.1.1\mybatis-spring-2.1.1.jar;E:\ZXJYRepository\mysql\mysql-connector-java\8.0.28\mysql-connector-java-8.0.28.jar;E:\ZXJYRepository\org\projectlombok\lombok\1.18.22\lombok-1.18.22.jar;E:\ZXJYRepository\org\slf4j\slf4j-api\1.7.33\slf4j-api-1.7.33.jar;E:\ZXJYRepository\org\springframework\spring-core\5.3.15\spring-core-5.3.15.jar;E:\ZXJYRepository\org\springframework\spring-jcl\5.3.15\spring-jcl-5.3.15.jar" com.stu.myserver.Application
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.9)
2023-06-13 20:33:11.392 INFO 14472 --- [ main] com.stu.myserver.Application : Starting Application using Java 1.8.0 on konglx with PID 14472 (E:\20230406WorkSpace\20230407MyServer\target\classes started by 小明的学习圈子 in E:\20230406WorkSpace\20230407MyServer)
2023-06-13 20:33:11.392 INFO 14472 --- [ main] com.stu.myserver.Application : The following profiles are active: dev
2023-06-13 20:33:11.814 WARN 14472 --- [ main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.stu.myserver]' package. Please check your configuration.
2023-06-13 20:33:12.079 INFO 14472 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9090 (http)
2023-06-13 20:33:12.095 INFO 14472 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-06-13 20:33:12.095 INFO 14472 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.56]
2023-06-13 20:33:12.142 INFO 14472 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-06-13 20:33:12.142 INFO 14472 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 719 ms
2023-06-13 20:33:12.516 INFO 14472 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9090 (http) with context path ''
2023-06-13 20:33:12.516 INFO 14472 --- [ main] com.stu.myserver.Application : Started Application in 1.441 seconds (JVM running for 1.766)
15、代码生成器(本文有两种方案,后边还有一种更详细的,但是pom的依赖只能使用一个,使用后边的需要吧这个pom依赖注释掉)
1、pom.xml加入以下依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
2、新建CodeGenerator类
3、代码
package generator;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
public class CodeGenerator {
@Test
public void genCode() {
// 前缀
String prefix = "2023";
String moduleName = "java";
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("公众号:小明的学习圈子");
gc.setOpen(false); // 生成后是否打开资源管理器
gc.setFileOverride(false); // 重新成时文件是否覆盖
gc.setServiceName("%sService"); // 去掉Service接口的首字母I
gc.setIdType(IdType.ASSIGN_ID); // 主键策略
gc.setDateType(DateType.ONLY_DATE);// 定义生成的实体类中日期类型
gc.setSwagger2(true);// 开启 Swagger2 模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/" + prefix + moduleName + "?serverTimezone=UTC");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("study");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.stu");
pc.setModuleName("myserver"); //模块名
pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
// 生成哪些表
strategy.setInclude("acl_user" );
// 数据库表映射到实体的命名策略
strategy.setNaming(NamingStrategy.underline_to_camel);
//如果是同时生成多个表用逗号隔开 //strategy.setInclude("acl_permission","acl_role","acl_role_permission","acl_user","acl_user_role");
// 设置表前缀不生成
strategy.setTablePrefix(moduleName + "_");
// 数据库表字段映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setEntityLombokModel(true);
// 逻辑删除字段名
strategy.setLogicDeleteFieldName("is_deleted");
// 去掉布尔值的is_前缀
strategy.setEntityBooleanColumnRemoveIsPrefix(true);
// 自动填充
TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT);
TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(gmtCreate);
tableFills.add(gmtModified);
strategy.setTableFillList(tableFills);
// restful api风格控制器
strategy.setRestControllerStyle(true);
// url 中驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}
4、执行genCode方法,生成的代码如下
5、数据库截图
6、Application
package com.stu.myserver;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
// @ComponentScan(basePackages = "com.stu")
// @MapperScan("com.stu.*.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
7、yml文件添加mybatisplus的配置
server:
port: 9090
spring:
profiles:
## 环境设置
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/2023Java?serverTimezone=Asia/Shanghai
username: root
password: study
## 响应 json 的全局时间格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
##mybatis 日志设置
mybatis-plus:
configuration:
## 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:/mapper/*.xml
global-config:
db-config:
logic-delete-field: isDeleted ## 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 ## 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 ## 逻辑未删除值(默认为 0)
8、userController添加测试接口index
package com.stu.myserver.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author 公众号:小明的学习圈子
* @since 2023-06-14
*/
@RestController
@RequestMapping("/myserver/user")
public class UserController {
@RequestMapping
public String index(){
return "index";
}
}
9、游览器访问
10、连接数据库测试
package com.stu.myserver.controller;
import com.stu.myserver.entity.User;
import com.stu.myserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 公众号:小明的学习圈子
* @since 2023-06-14
*/
@RestController
@RequestMapping("/myserver/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping
public String index(){
return "index";
}
@GetMapping("getUserList")
public List<User> getUserList(){
List<User> list = userService.list();
return list;
}
}
11、启动项目报错 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.stu.myserver.mapper.UserMapper.selectList 解决参照:https://www.cnblogs.com/konglxblog/p/14864807.html 12、pom.xml文件添加mybatis-plus等配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.4.0</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.9.2</swagger.version>
<mybatis-spring-boot-starter.version>2.3.1</mybatis-spring-boot-starter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
13、添加配置类MybatisPlusConfig 分页插件地址:https://baomidou.com/pages/2976a3/#spring
package com.stu.myserver.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/******************************
* 用途说明:
* 作者姓名: 小明的学习圈子
* 创建时间: 2023-02-09 22:02
******************************/
@Configuration
@MapperScan("com.stu.**.mapper")
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
14、添加Swagger2Config
package com.stu.myserver.config;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/******************************
* 用途说明:
* 作者姓名: 小明的学习圈子
* 创建时间: 2023-02-09 22:02
******************************/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket webApiConfig() {
return new Docket(DocumentationType.SWAGGER_2)
// 分组名
.groupName("WebApi")
.apiInfo(apiInfo())
.select()
// .paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("test")
.description("小明的学习圈子")
.version("1.0")
.contact(new Contact("test", "https://stu.stucoding.com", "xxxx.@xxx.com"))
.build();
}
}
15、测试
package com.stu.myserver.controller;
import com.stu.myserver.entity.User;
import com.stu.myserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 公众号:小明的学习圈子
* @since 2023-06-14
*/
@RestController
@RequestMapping("/myserver/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> index(){
List<User> list = userService.list();
return list;
}
}
16、访问成功 
16、分页postman测试
postman下载 https://app.getpostman.com/app/download/win64 UserController新增page方法
package com.stu.myserver.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.stu.myserver.entity.User;
import com.stu.myserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 公众号:小明的学习圈子
* @since 2023-06-14
*/
@RestController
@RequestMapping("/myserver/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> index(){
List<User> list = userService.list();
return list;
}
/******************************
* 用途说明:
* 作者姓名: 更多干货 公众号 小明的学习圈子
* 创建时间: 2023-02-27
******************************/
@GetMapping("/page")
public Page<User> findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize){
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.orderByDesc("create_time");
return userService.page(new Page<>(pageNum,pageSize),queryWrapper);
}
}
postman测试 
17、整合Swagger2
pom.xml添加依赖,上边已经添加完
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
定义配置类上边已经添加完
package com.stu.myserver.config;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/******************************
* 用途说明:
* 作者姓名: 小明的学习圈子
* 创建时间: 2023-02-09 22:02
******************************/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket webApiConfig() {
return new Docket(DocumentationType.SWAGGER_2)
// 分组名
.groupName("WebApi")
.apiInfo(apiInfo())
.select()
// .paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("test")
.description("描述服务端的 api 接口定义")
.version("1.0")
.contact(new Contact("test", "http://www.baidu.com", "xxxx.@xxx.com"))
.build();
}
}
启动项目,访问,默认端口是8080,因为我修改为了9090,所以我访问是9090. http://localhost:9090/swagger-ui.html


八、用户管理使用axios完善列表和新增页面
1、为了方便在views下新建sys文件夹,在sys下新建user的文件夹,把User.vue放进去,路由页面引入的路径需要修改
2、request.js的baseUrl之前的/api修改为真实的后端请求地 址,'http://localhost:9090/'
3、mock.js文件的请求的/api换成'http://localhost:9090',否则首页的折线图没有数据,因为后端java接口没有折线图的接口
4、新增user.js的请求接口
import http from "@/utils/request";
//请求首页的数据
export const getData = () => {
return http.get('/home/getData');
}
5、user.vue使用
<template>
<div class="user">
<el-dialog :title="title" :visible.sync="dialogVisible" width="35%">
<!-- 用户表单信息 -->
<el-form
ref="model"
:model="model"
:inline="true"
label-width="80px"
:rules="rules"
>
<el-form-item label="用户名称" prop="userName">
<el-input v-model="model.userName"></el-input>
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="model.nickName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="model.password"></el-input>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="model.sex" placeholder="性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="创建日期" prop="createTime">
<el-date-picker
v-model="model.createTime"
type="date"
placeholder="选择日期" format="yyyy-MM-dd HH:mm:ss"
>
</el-date-picker>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submit">确 定</el-button>
</span>
</el-dialog>
<div class="user-header">
<el-button type="primary" @click="handeleAdd">+新增</el-button>
</div>
<el-card class="box-card" style="margin-top: 20px">
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
border
stripe
@selection-change="handleSelectionChange"
>
<!-- <div v-for="(val, key) in tableLabel" :key="key">
<div v-if="key === 'a'">
<el-table-column type="selection" :prop="key" :label="val" />
</div>
<div v-else><el-table-column :prop="key" :label="val" /></div>
</div> -->
<el-table-column
v-for="(val, key) in tableLabel"
:type="key === 'index' ? 'selection' : null"
:prop="key"
:label="val"
:key="key"
/>
<el-table-column fixed="right" label="操作" width="100">
<template slot-scope="scope">
<el-button
@click="handleDetails(scope.row)"
type="text"
size="small"
>查看</el-button
>
<el-button @click="handleEdit(scope.row)" type="text" size="small"
>编辑</el-button
>
</template>
</el-table-column>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table>
<div class="block">
<span class="demonstration"></span>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination></div
></el-card>
</div>
</template>
<script>
//import { getData } from "@/api/user.js";
import http from "@/utils/request";
export default {
name: "User",
data() {
return {
title: "新增",
multipleSelection: [],
total: 0,
pageNum: 0,
pageSize: 5,
model: {
userName: "",
password: "",
nickName: "",
sex: "",
createTime: "",
},
tableLabel: {
index: "",
nickName: "昵称",
userName: "姓名",
phone: "电话",
address: "地址",
createTime: "创建日期",
},
tableData: [
// {
// date: "2016-05-02",
// name: "王小虎",
// address: " 1518 弄",
// },
// {
// date: "2016-05-04",
// name: "王小虎",
// address: " 1517 弄",
// },
// {
// date: "2016-05-01",
// name: "王小虎",
// address: " 1519 弄",
// },
// {
// date: "2016-05-03",
// name: "王小虎",
// address: " 1516 弄",
// },
],
dialogVisible: false,
rules: {
userName: [
{ required: true, message: "请输入用户名称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
password: [
{ required: true, message: "请输入密码", trigger: "change" },
],
nickName: [
{ required: true, message: "请输入用户昵称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
sex: [{ required: true, message: "请选择性别", trigger: "change" }],
createTime: [
{
type: "date",
required: true,
message: "请选择日期",
trigger: "change",
},
],
},
};
},
created() {
this.getData();
},
methods: {
getData() {
http
.get(
"/myserver/user/page?pageNum=" +
this.pageNum +
"&pageSize=" +
this.pageSize
)
.then((res) => {
console.log(res);
this.tableData = res.data.records;
this.total = res.data.total;
});
},
submit() {
this.$refs.model.validate((valid) => {
if (valid) {
//验证通过才进行数据请求
this.model;
}
});
},
handleSizeChange(val) {
this.pageSize = val;
this.getData();
},
handleCurrentChange(val) {
this.pageNum = val;
this.getData();
},
//当选择项发生变化时会触发该事件,获取选中行的数据
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleDetails(val) {
this.model = Object.assign({}, val);
this.dialogVisible = true;
},
handleEdit(val) {
this.model = Object.assign({}, val);
debugger
this.dialogVisible = true;
},
handeleAdd() {
this.dialogVisible = true;
},
},
};
</script>
<style lang="less" scoped>
.el-el-input {
width: 80%;
}
</style>
6、跨域问题
java新增CorsConfig配置类
package com.stu.myserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/******************************
* 用途说明: 跨域
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
7、代码生成器(新版)以后的代码基于新版代码生成器编写,后边会给sql
1、官网:https://baomidou.com/pages/779a6e/ 添加依赖(注意修改mybatis-plus-generator和velocity-engine-core的版本为3.5.1,否则不支持新版,并且要把旧版的代码生成器的依赖和代码注释掉,否则报错)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
2、完整代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.9.2</swagger.version>
<mybatis-spring-boot-starter.version>2.3.1</mybatis-spring-boot-starter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<!-- <dependency>-->
<!-- <groupId>org.apache.velocity</groupId>-->
<!-- <artifactId>velocity-engine-core</artifactId>-->
<!-- <version>${velocity.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- spring boot redis缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lecttuce 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
3、新增生成代码类
package com.stu.myserver.utils;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import java.util.Collections;
/******************************
* 用途说明:
* 作者姓名:公众号:小明的学习圈子
* 创建时间: 2023-06-23 14:52
******************************/
public class CodeGenerator {
public static void main(String[] args) {
generate();
}
private static void generate() {
String projectPath = System.getProperty("user.dir");
FastAutoGenerator.create("jdbc:mysql://localhost:3306/2023Java?serverTimezone=Asia/Shanghai", "root", "study")
.globalConfig(builder -> {
builder.author("小明的学习圈子") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir(projectPath + "/src/main/java"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.stu.myserver") // 设置父包名
.moduleName(null) // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, projectPath + "/src/main/resources/mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.entityBuilder().enableLombok();
// builder.mapperBuilder().enableMapperAnnotation().build();
builder.controllerBuilder().enableHyphenStyle() // 开启驼峰转连字符
.enableRestStyle(); // 开启生成@RestController 控制器
builder.addInclude("acl_user") // 设置需要生成的表名
.addTablePrefix("acl_", "sys_"); // 设置过滤表前缀
})
// .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
4、这样已经可以生成代码了,但是发现controller里没有内容,在类里加一些CRUD的常用方法
5、重写默认的controller.java.vm,放到resources下的templates下 

package ${package.Controller};
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import ${package.Service}.${table.serviceName};
import ${package.Entity}.${entity};
#if(${restControllerStyle})
import org.springframework.web.bind.annotation.RestController;
#else
import org.springframework.stereotype.Controller;
#end
#if(${superControllerClassPackage})
import ${superControllerClassPackage};
#end
/**
* <p>
* $!{table.comment} 前端控制器
* </p>
*
* @author ${author}
* @since ${date}
*/
#if(${restControllerStyle})
@RestController
#else
@Controller
#end
@RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end")
#if(${kotlin})
class ${table.controllerName}#if(${superControllerClass}) : ${superControllerClass}()#end
#else
#if(${superControllerClass})
public class ${table.controllerName} extends ${superControllerClass} {
#else
public class ${table.controllerName} {
#end
@Resource
private ${table.serviceName} ${table.entityPath}Service;
// 新增或者更新
/***********************************
* 用途说明:
* @param ${table.entityPath}
* 返回值说明:
* @return boolean
***********************************/
@PostMapping("/save")
public boolean save(@RequestBody ${entity} ${table.entityPath}){
return ${table.entityPath}Service.saveOrUpdate(${table.entityPath});
}
/***********************************
* 用途说明:
* @param id
* 返回值说明:
* @return Boolean
***********************************/
@DeleteMapping("/{id}")
public Boolean delete(@PathVariable Integer id){
return ${table.entityPath}Service.removeById(id);
}
/***********************************
* 用途说明:
* @param ids
* 返回值说明:
* @return boolean
***********************************/
@PostMapping("/del/batch")
public boolean deleteBatch(@RequestBody List<Integer> ids){
return ${table.entityPath}Service.removeByIds(ids);
}
/***********************************
* 用途说明:
* @param:
* 返回值说明:
* @return List<${entity}>
***********************************/
@GetMapping
public List<${entity}> findAll(){
return ${table.entityPath}Service.list();
}
/***********************************
* 用途说明:
* @param id
* 返回值说明:
* @return ${entity}
***********************************/
@GetMapping("/{id}")
public ${entity} findOne(@PathVariable Integer id){
return ${table.entityPath}Service.getById(id);
}
/***********************************
* 用途说明:
* @param pageNum pageSize ${table.entityPath}
* 返回值说明:
* @return Page<${entity}>
***********************************/
@PostMapping("/page")
public Page<${entity}> findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestBody ${entity} ${table.entityPath}){
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.orderByDesc("id");
return ${table.entityPath}Service.page(new Page<>(pageNum,pageSize),queryWrapper);
}
}
#end
5、user.vue页面请求方式参数和列表属性调整
<template>
<div class="user">
<el-dialog :title="title" :visible.sync="dialogVisible" width="35%">
<!-- 用户表单信息 -->
<el-form
ref="model"
:model="model"
:inline="true"
label-width="80px"
:rules="rules"
>
<el-form-item label="用户名称" prop="userName">
<el-input v-model="model.userName"></el-input>
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="model.nickName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="model.password"></el-input>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="model.sex" placeholder="性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="创建日期" prop="createTime">
<el-date-picker
v-model="model.createTime"
type="date"
placeholder="选择日期"
format="yyyy-MM-dd HH:mm:ss"
>
</el-date-picker>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submit">确 定</el-button>
</span>
</el-dialog>
<div class="user-header">
<el-button type="primary" @click="handeleAdd">+新增</el-button>
</div>
<el-card class="box-card" style="margin-top: 20px">
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
border
stripe
@selection-change="handleSelectionChange"
>
<!-- <div v-for="(val, key) in tableLabel" :key="key">
<div v-if="key === 'a'">
<el-table-column type="selection" :prop="key" :label="val" />
</div>
<div v-else><el-table-column :prop="key" :label="val" /></div>
</div> -->
<el-table-column
v-for="(val, key) in tableLabel"
:type="key === 'index' ? 'selection' : null"
:prop="key"
:label="val"
:key="key"
/>
<el-table-column fixed="right" label="操作" width="100">
<template slot-scope="scope">
<el-button
@click="handleDetails(scope.row)"
type="text"
size="small"
>查看</el-button
>
<el-button @click="handleEdit(scope.row)" type="text" size="small"
>编辑</el-button
>
</template>
</el-table-column>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table>
<div class="block">
<span class="demonstration"></span>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination></div
></el-card>
</div>
</template>
<script>
//import { getData } from "@/api/user.js";
import http from "@/utils/request";
export default {
name: "User",
data() {
return {
title: "新增",
multipleSelection: [],
total: 0,
pageNum: 0,
pageSize: 5,
model: {
userName: "",
password: "",
nickName: "",
sex: "",
createTime: "",
},
tableLabel: {
index: "",
nickName: "昵称",
username: "姓名",
// phone: "电话",
// address: "地址",
gmtCreate: "创建日期",
},
tableData: [
// {
// date: "2016-05-02",
// name: "王小虎",
// address: " 1518 弄",
// },
// {
// date: "2016-05-04",
// name: "王小虎",
// address: " 1517 弄",
// },
// {
// date: "2016-05-01",
// name: "王小虎",
// address: " 1519 弄",
// },
// {
// date: "2016-05-03",
// name: "王小虎",
// address: " 1516 弄",
// },
],
dialogVisible: false,
rules: {
userName: [
{ required: true, message: "请输入用户名称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
password: [
{ required: true, message: "请输入密码", trigger: "change" },
],
nickName: [
{ required: true, message: "请输入用户昵称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
sex: [{ required: true, message: "请选择性别", trigger: "change" }],
createTime: [
{
type: "date",
required: true,
message: "请选择日期",
trigger: "change",
},
],
},
};
},
created() {
this.getData();
},
methods: {
getData() {
http
.post(
"/user/page?pageNum=" + this.pageNum + "&pageSize=" + this.pageSize,
{ user: {} }
)
.then((res) => {
console.log(res);
this.tableData = res.data.records;
this.total = res.data.total;
});
},
submit() {
this.$refs.model.validate((valid) => {
if (valid) {
//验证通过才进行数据请求
this.model;
}
});
},
handleSizeChange(val) {
this.pageSize = val;
this.getData();
},
handleCurrentChange(val) {
this.pageNum = val;
this.getData();
},
//当选择项发生变化时会触发该事件,获取选中行的数据
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleDetails(val) {
this.model = Object.assign({}, val);
this.dialogVisible = true;
},
handleEdit(val) {
this.model = Object.assign({}, val);
debugger;
this.dialogVisible = true;
},
handeleAdd() {
this.dialogVisible = true;
},
},
};
</script>
<style lang="less" scoped>
.el-el-input {
width: 80%;
}
</style>
6、页面访问 
九、动态路由和菜单(这里只是实现了假数据的动态路由和菜单,第十三章会从后台获取真正实现动态路由和菜单)
1、把权限菜单的页面统一放到sys文件下,后边这里结构需要修改,菜单、用户、角色会分别在sys下新建3个文件夹,然后再各自的文件夹下放文件
2、store下的menu.js新增menuList,setMenuList
export default {
state: {
menuList: [],//动态路由
isCollapse: false, //用于控制菜单的展开收起
breadcrumbList: [
//默认的数据
{
path: "/",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
}
]//面包屑的数据
},
mutations: {
setMenuList(state, val) {
state.menuList = val
},
//删除指定的tag
closeTag(state, item) {
const index = state.breadcrumbList.findIndex(val => { return val.name === item.name })
state.breadcrumbList.splice(index, 1)
},
//用于控制菜单的展开收起
handleCollapse(state) {
state.isCollapse = !state.isCollapse
},
//更新面包屑数据
breadcrumbChange(state, val) {
//判断添加的数据是否是首页
if (val.name !== 'name') {
const index = state.breadcrumbList.findIndex(item => item.name === val.name)
//如果不存在就添加道面包屑数组里
if (index === -1) {
state.breadcrumbList.push(val)
}
}
}
}
}
3、router下的index.js文件实现动态添加路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/sys/User.vue'
import Main from '@/views/Main.vue'
import store from '@/store'
Vue.use(VueRouter)
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
//主路由
// {
// path: '/',
// component: Main,
// redirect: '/home',
// children: [
// //子路由
// { path: 'home', name: "home", component: Home },//首页
// // { path: 'user', name: "user", component: User },//用户管理
// // { path: 'role', name: "role", component: () => import("@/views/sys/Role.vue") },//角色管理
// // { path: 'menu', name: "menu", component: () => import("@/views/sys/Menu.vue") },//菜单管理管理
// ]
// },
// {
// path: '/user',
// name: 'User',
// component: User
// },
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。挂载到main.js的根节点
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
// const app = new Vue({
// router
// }).$mount('#app')
// 获取原型对象push函数
const originalPush = VueRouter.prototype.push
// 获取原型对象replace函数
const originalReplace = VueRouter.prototype.replace
// 修改原型对象中的push函数
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 修改原型对象中的replace函数
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
const menuData = [
// {
// path: "/",
// name: "home",
// label: "首页",
// icon: "s-home",
// url: "Home/Home",
// },
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
component: "sys/User",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
component: "sys/Role",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
component: "sys/Menu",
},
],
},
]
// 拼装动态路由
const manageRoute = {
path: '/',
component: Main,
redirect: '/home',
children: [
//子路由
{ path: 'home', name: "home", component: Home },//首页
]
}
//获取数据
//菜单
store.commit('setMenuList', menuData)
//权限用户
localStorage.setItem("menus", JSON.stringify(menuData))
// 注意:刷新页面会导致页面路由重置
const setRoutes = () => {
const storeMenus = localStorage.getItem("menus");
if (storeMenus) {
// 获取当前的路由对象名称数组
const currentRouteNames = router.getRoutes().map(v => v.name)
if (!currentRouteNames.includes('home')) {
const menus = JSON.parse(storeMenus)
menus.forEach(item => {
if (item.path) { // 当且仅当path不为空的时候才去设置路由
let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.component + '.vue') }
manageRoute.children.push(itemMenu)
} else if (item.children.length) {
item.children.forEach(item => {
if (item.path) {
let itemMenu = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.component + '.vue') }
manageRoute.children.push(itemMenu)
}
})
}
})
// 动态添加到现在的路由对象中去
router.addRoute(manageRoute)
}
}
}
// 重置我就再set一次路由
setRoutes()
//转成路由
// const menuToRoute = (item) => {
// if (!item.component) {
// return null
// }
// // let route = {
// // name: item.name,
// // path: item.path.replace("/", ""),
// // meta: {
// // icon: item.icon,
// // title: item.title,
// // }
// // }
// // route.component= () => import('../views/' + item.component + '.vue')
// // route.component=()=>import('@/views/'+item.component+'.vue');
// // route.component = () => import('@/views/' + item.component + '.vue')
// let route = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.component + '.vue') }
// return route
// }
export default router
4、CommonAside.vue添加计算属性menuData,循环遍历菜单,注意这里遍历菜单的控件和获取菜单的方法有改变
<template>
<div>
<!-- <el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>{{ isCollapse ? "后台" : "通用后台管理系统" }}</h3>
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.path"
@click="clickMenu(item)"
>
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<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-group
v-for="subItem in item.children"
:key="subItem.path"
>
<el-menu-item :index="subItem.path" @click="clickMenu(subItem)">{{
subItem.label
}}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu> -->
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<router-link to="/home">
<el-menu-item
index="Index"
@click="clickMenu({ name: 'home', title: '首页', path: '/' })"
>
<template slot="title">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</template>
</el-menu-item>
</router-link>
<el-submenu :index="menu.label" v-for="menu in menuData" :key="menu.name">
<template slot="title">
<i :class="`el-icon-${menu.icon}`"></i>
<span>{{ menu.label }}</span>
</template>
<router-link
:to="item.path"
v-for="item in menu.children"
:key="item.name"
>
<el-menu-item :index="item.name" @click="clickMenu(item)">
<template slot="title">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</template>
</el-menu-item>
</router-link>
</el-submenu>
</el-menu>
</div>
</template>
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu {
//height: calc(100vh - 0px);
border-right: 0px;
height: 100vh;
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400px;
}
}
</style>
<script>
export default {
data() {
return {
//isCollapse: false,
menuData11: [
// {
// path: "/",
// name: "home",
// label: "首页",
// icon: "s-home",
// url: "Home/Home",
// },
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
url: "Other/PageTwo",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
url: "Other/PageTwo",
},
],
},
],
};
},
created() {
this.menuData;
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item) {
//当页面的路由和跳转的路由不一致才允许跳转
//this.$route.path 上一个路径,item.path下一个路径
if (
this.$route.path !== item.path &&
!(this.$route.path === "/home" && item.path === "/")
) {
this.$router.push(item.path);
}
this.$store.commit("breadcrumbChange", item);
},
},
computed: {
// //没有子菜单
// noChildren() {
// return this.menuData.filter((item) => !item.children);
// },
// //有子菜单
// hasChildren() {
// return this.menuData.filter((item) => item.children);
// },
//vuex取得菜单展开收起的值,但是data里不能重复定义
isCollapse() {
return this.$store.state.menu.isCollapse;
},
// menuData11: {
// get() {
// return this.$store.state.menu.menuList;
// },
// },
menuData() {
return this.$store.state.menu.menuList;
},
},
};
</script>
十、用户管理
1、用户界面和用户列表
从这开始,将进行前后端数据整合后的记录。

1.1、用户列表主要代码
列表部分是放在card里,并且列表是动态的列,并且列表支持分页功能。列表分页是element ui里的,可以去看官网。
<el-card class="box-card" style="margin-top: 20px">
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
border
stripe
@selection-change="handleSelectionChange"
>
<!-- <div v-for="(val, key) in tableLabel" :key="key">
<div v-if="key === 'a'">
<el-table-column type="selection" :prop="key" :label="val" />
</div>
<div v-else><el-table-column :prop="key" :label="val" /></div>
</div> -->
<el-table-column
v-for="(val, key) in tableLabel"
:type="key === 'index' ? 'selection' : null"
:prop="key"
:label="val"
:key="key"
/>
<el-table-column fixed="right" label="操作" >
<template slot-scope="scope">
<el-button @click="doAssign(scope.row)" type="text" size="small" style="margin-left:5px;"
>分配角色</el-button
>
<el-button @click="removeById(scope.row)" type="text" size="small"
>刪除</el-button
>
<el-button @click="handleEdit(scope.row)" type="text" size="small" v-if="hasAuth('user.add')"
>编辑</el-button
>
</template>
</el-table-column>
<!-- <el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column> -->
</el-table>
<div class="block">
<span class="demonstration"></span>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination></div
></el-card>
1.2、动态的表格列
tableLabel: {
index: "",
nickName: "昵称",
username: "姓名",
// phone: "电话",
// address: "地址",
gmtCreate: "创建日期",
},
1.3、分页的两个方法
handleSizeChange(val) {
this.pageSize = val;
this.getData();
},
handleCurrentChange(val) {
this.pageNum = val;
this.getData();
},
1.4、其中分页请求方法用了两种方式去请求后台接口
第一种就是pageList
首先在src的api文件夹下新建一个user.js
里面定义pageList方法去请求后台的分页接口
import http from "@/utils/request";
const apiName = '/admin/sys/user';
//请求首页的数据
export const getData = () => {
return http.get('/home/getData');
}
export const pageList = (page, limit, user) => {
return http({
url: `${apiName}/pageList/${page}/${limit}`,
method: 'get',
params: user
})
}
然后再User.vue页面引入这个方法
getData,
pageList
} from "@/api/user.js";
1.5、获取分页的数据的api
//获得分页数据
getData() {
pageList(this.pageNum, this.pageSize, {
user: JSON.stringify(this.model),
})
.then((res) => {
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "查询成功: ",
});
this.tableData = res.data.list;
this.total = res.data.total;
} else {
this.$message({
type: "info",
message: "查询失败: ",
});
}
})
.catch((e) => {
this.$message({
type: "info",
message: "查询异常",
});
});
//
// http
// .post(
// "/admin/sys/user/page?pageNum=" + this.pageNum + "&pageSize=" + this.pageSize,
// { user: JSON.stringify(this.model) }
// )
// .then((res) => {
//
// console.log(res);
// this.tableData = res.records;
// this.total = res.total;
// }).catch(error => {
//
// });
},
1.6、第二种,不需要定义user.js,直接导入上边定义封装好的request.js
导入
import http from "@/utils/request";
使用
//获得分页数据
getData() {
http
.post(
"/admin/sys/user/page?pageNum=" + this.pageNum + "&pageSize=" + this.pageSize,
{ user: JSON.stringify(this.model) }
)
.then((res) => {
console.log(res);
this.tableData = res.records;
this.total = res.total;
}).catch(error => {
});
},
1.7、后端接口
package com.stu.myserver.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.stu.myserver.entity.User;
import com.stu.myserver.service.IRoleService;
import com.stu.myserver.service.IUserService;
import com.stu.myserver.utils.MD5;
import com.stu.myserver.utils.R;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/******************************
* 用途说明:
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@RestController
@RequestMapping("/admin/sys/user")
public class UserController {
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
/***********************************
* 用途说明:删除角色
* 返回值说明: com.stu.service.base.result.R
***********************************/
@DeleteMapping("deleteUser/{id}")
public R deleteUser(@PathVariable String id) {
/* if (true) {
throw new CustomException(ResultCodeEnum.DIVIDE_ZERO);
}*/
boolean result = userService.removeById(id);
if (result) {
return R.ok().message("删除成功");
}
return R.error().message("删除失败");
}
/***********************************
* 用途说明:修改角色
* 返回值说明: com.stu.service.base.result.R
***********************************/
@PostMapping("updateUser")
public R updateUser(@RequestBody User user) {
boolean result = userService.updateById(user);
if (result) {
return R.ok().message("更新成功");
}
return R.error().message("更新失败");
}
/***********************************
* 用途说明:添加角色
* 返回值说明: com.stu.service.base.result.R
***********************************/
// @PreAuthorize("hasAuthority('sys:user:lista')")
@ApiOperation("添加角色")
@PostMapping("addUser")
public R addUser(@ApiParam("角色对象") @RequestBody User user) {
user.setPassword(MD5.encrypt("111111"));
boolean result = userService.save(user);
if (result) {
return R.ok().message("保存成功");
}
return R.error().message("保存失败");
}
/***********************************
* 用途说明:查询角色表所有数据
* 返回值说明: com.stu.service.base.result.R
***********************************/
@GetMapping("findAll")
public R findAllUser() {
List<User> list = userService.list();
return R.ok().data("list", list);
}
/***********************************
* 用途说明:查询
* 返回值说明: com.stu.service.edu.entity.User
***********************************/
@GetMapping("get/{id}")
public R getUser(@PathVariable String id) {
return R.ok().data("dataInfo", userService.getById(id));
}
/***********************************
* 用途说明:查询角色表所有数据
* 返回值说明: com.stu.service.base.result.R
***********************************/
// @PreAuthorize("hasRole('admin')")
@GetMapping("pageList/{page}/{limit}")
public R pageList(@PathVariable long page, @PathVariable long limit, User user) {
Page<User> pageParam = new Page<>(page, limit);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(user.getUsername())) {
queryWrapper.like("user_name", user.getUsername());
}
userService.page(pageParam, queryWrapper);
Map<String, Object> map = new HashMap<String, Object>();
long total = pageParam.getTotal();
List<User> list = pageParam.getRecords();
map.put("total", total);
map.put("list", list);
return R.ok().data(map);
}
/***********************************
* 用途说明:
* @param pageNum pageSize user
* 返回值说明:
* @return Page<User>
***********************************/
//@PreAuthorize("hasRole('test')")
@PostMapping("/page")
public Page<User> findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestBody User user) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("id");
return userService.page(new Page<>(pageNum, pageSize), queryWrapper);
}
/***********************************
* 用途说明:根据用户获取角色
* @param userId
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("toAssign/{userId}")
public R toAssign(@PathVariable String userId) {
return R.ok().data("data", roleService.findRoleByUserId(userId));
}
/***********************************
* 用途说明:给用户分配角色权限
* @param userId
* @param permissionIds
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@PostMapping("doAssign")
public R doAssign(String userId, String[] permissionIds) {
boolean result = roleService.saveUserRelationShip(userId, permissionIds);
if (result) {
return R.ok();
}
return R.error();
}
// // 新增或者更新
// /***********************************
// * 用途说明:
// * @param user
// * 返回值说明:
// * @return boolean
// ***********************************/
// @PostMapping("/save")
// public boolean save(@RequestBody User user){
// return userService.saveOrUpdate(user);
// }
//
// /***********************************
// * 用途说明:
// * @param id
// * 返回值说明:
// * @return Boolean
// ***********************************/
// @DeleteMapping("/{id}")
// public Boolean delete(@PathVariable Integer id){
// return userService.removeById(id);
// }
//
// /***********************************
// * 用途说明:
// * @param ids
// * 返回值说明:
// * @return boolean
// ***********************************/
// @PostMapping("/del/batch")
// public boolean deleteBatch(@RequestBody List<Integer> ids){
// return userService.removeByIds(ids);
// }
//
// /***********************************
// * 用途说明:
// * @param:
// * 返回值说明:
// * @return List<User>
// ***********************************/
// @GetMapping
// public List<User> findAll(){
// return userService.list();
// }
//
// /***********************************
// * 用途说明:
// * @param id
// * 返回值说明:
// * @return User
// ***********************************/
// @GetMapping("/{id}")
// public User findOne(@PathVariable Integer id){
// return userService.getById(id);
// }
//
}
2、新增、编辑用户
2.1、新增、编辑界面。定义user.js
新增、编辑界面和列表界面是在同一个vue页面,新增和编辑页面是dialog实现的并且是一个dialog,通过变量dialogVisible控制新增、编辑界面的显示和隐藏,通过title控制显示新增还是编辑的title。
在user.js里定义save和update方法,调用后台的接口,使用的时候需要在User.vue页面引入
import http from "@/utils/request";
const apiName = '/admin/sys/user';
//请求首页的数据
export const getData = () => {
return http.get('/home/getData');
}
export const pageList = (page, limit, user) => {
return http({
url: `${apiName}/pageList/${page}/${limit}`,
method: 'get',
params: user
})
}
export const save = (user) => {
return http({
url: `${apiName}/addUser`,
method: 'post',
data: user
})
}
export const update = (user) => {
return http({
url: `${apiName}/updateUser`,
method: 'post',
data: user
})
}
新增和编辑的diglog,新增和编辑提交的时候会有验证,验证规则就是element ui的rules规则,不明白的可以去看官网。
<el-dialog :title="title" :visible.sync="dialogVisible" width="35%">
<!-- 用户表单信息 -->
<el-form
ref="model"
:model="model"
:inline="true"
label-width="80px"
:rules="rules"
:disabled="disabled"
>
<el-form-item label="用户名称" prop="username">
<el-input v-model="model.username"></el-input>
</el-form-item>
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="model.nickName"></el-input>
</el-form-item>
<!-- <el-form-item label="密码" prop="password">
<el-input type="password" v-model="model.password"></el-input>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="model.sex" placeholder="性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="创建日期" prop="createTime">
<el-date-picker
v-model="model.createTime"
type="date"
placeholder="选择日期"
format="yyyy-MM-dd HH:mm:ss"
>
</el-date-picker>
</el-form-item> -->
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</span>
</el-dialog>
2.2、点击新增和编辑
新增和编辑控件
<div class="user-header">
<el-button type="primary" @click="handeleAdd">+新增</el-button>
</div>
<el-table-column fixed="right" label="操作" >
<template slot-scope="scope">
<el-button @click="doAssign(scope.row)" type="text" size="small" style="margin-left:5px;"
>分配角色</el-button
>
<el-button @click="removeById(scope.row)" type="text" size="small"
>刪除</el-button
>
<el-button @click="handleEdit(scope.row)" type="text" size="small" v-if="hasAuth('user.add')"
>编辑</el-button
>
</template>
</el-table-column>
新增方法,disabled是控制页面控件可不可编辑的,因为我没做详情,所有这个属性可以不设置。
handeleAdd() {
this.disabled = false;
this.title = "新增";
this.dialogVisible = true;//diglog显示
this.model = Object.assign({}, {});//新增的时候清空model的属性值,编辑的时候才显示数据
},
编辑
handleEdit(val) {
this.title = "编辑";
this.disabled = false;
this.model = Object.assign({}, val);
this.dialogVisible = true;
},
2.3、新增和编辑的提交事件
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</span>
这里根据id判断是新增还是修改,id有值的是修改,没有的是新增,新增和修改成功后关闭diglog弹框,并调用分页接口刷新页面。
saveOrUpdate() {
if (this.model.id) {
this.update();
} else {
this.save();
}
},
update() {
this.$refs.model.validate((valid) => {
if (valid) {
update(this.model).then((res) => {
this.dialogVisible = false;
this.getData();
});
}
});
},
save() {
this.$refs.model.validate((valid) => {
if (valid) {
save(this.model).then((res) => {
this.dialogVisible = false;
this.getData();
});
}
});
},
2.4、UserController后台接口
因为这个只是单表操作,并且使用的是mybatis plus框架,所以只在controller里操作就行。
修改是根据id修改,新增就是新增,新增的时候需要给用户一个默认的密码,并且是通过MD5加密的。也就是说以后只要你不修改这里的111111,默认的新增的用户的登录密码就是6个1.
/***********************************
* 用途说明:修改角色
* 返回值说明: com.stu.service.base.result.R
***********************************/
@PostMapping("updateUser")
public R updateUser(@RequestBody User user) {
boolean result = userService.updateById(user);
if (result) {
return R.ok().message("更新成功");
}
return R.error().message("更新失败");
}
/***********************************
* 用途说明:添加角色
* 返回值说明: com.stu.service.base.result.R
***********************************/
// @PreAuthorize("hasAuthority('sys:user:lista')")
@ApiOperation("添加角色")
@PostMapping("addUser")
public R addUser(@ApiParam("角色对象") @RequestBody User user) {
user.setPassword(MD5.encrypt("111111"));
boolean result = userService.save(user);
if (result) {
return R.ok().message("保存成功");
}
return R.error().message("保存失败");
}
3、删除操作
在user.js里定义删除方法,记得在使用的页面引入
export const removeById = (id) => {
return http({
url: `${apiName}/deleteUser/${id}`,
method: 'delete'
})
}
user.vue删除
//删除
removeById(data) {
this.$confirm(
"此操作将永久删除" + data.username + ", 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
//删除api
removeById(data.id).then((res) => {
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "刪除成功 ",
});
this.getData();
} else {
this.$message({
type: "info",
message: "刪除失败: ",
});
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
后端UserController的deleteUser接口
/***********************************
* 用途说明:删除角色
* 返回值说明: com.stu.service.base.result.R
***********************************/
@DeleteMapping("deleteUser/{id}")
public R deleteUser(@PathVariable String id) {
/* if (true) {
throw new CustomException(ResultCodeEnum.DIVIDE_ZERO);
}*/
boolean result = userService.removeById(id);
if (result) {
return R.ok().message("删除成功");
}
return R.error().message("删除失败");
}
4、分配角色,页面初始化
分配角色是一个单独的页面
4.1、分配角色的按钮部分
<el-table-column fixed="right" label="操作" >
<template slot-scope="scope">
<el-button @click="doAssign(scope.row)" type="text" size="small" style="margin-left:5px;"
>分配角色</el-button
>
<el-button @click="removeById(scope.row)" type="text" size="small"
>刪除</el-button
>
<el-button @click="handleEdit(scope.row)" type="text" size="small" v-if="hasAuth('user.add')"
>编辑</el-button
>
</template>
</el-table-column>
4.2、实现页面跳转,这里需要把用户的id传到分配角色页面
//分配角色
doAssign(row) {
this.$router.push(`userForm/${row.id}`);
},
其中分配角色的跳转地址路由是/userForm/:id
分配角色的项目地址是sys/user/userForm
4.3、index.js路由的定义结构如下
{
path: '/userForm/:id',
name: '角色权限',
component: 'sys/user/userForm',
meta: { title: '角色权限', icon: 'table' },
hidden: true
}
4.4、分配角色的页面userForm.vue
<template>
<div class="app-container">
<el-button
:disabled="saveBtnDisabled"
type="primary"
size="mini"
@click="save"
>保存</el-button
>
<el-checkbox-group v-model="checkedRoles" style="margin-top: 20px">
<el-checkbox v-for="role in roles" :key="role.id" :label="role.id">{{
role.roleName
}}</el-checkbox>
</el-checkbox-group>
</div>
</template>
<script>
import { toAssign, doAssign } from "@/api/user.js";
export default {
data() {
return {
saveBtnDisabled: false,
checkedRoles: [],
roles: [],
};
},
watch: {
$route(to, from) {
this.init();
},
},
created() {
this.init();
},
methods: {
init() {
if (this.$route.params && this.$route.params.id) {
this.roleId = this.$route.params.id;
this.getDataList(this.roleId);
}
},
getDataList(roleId) {
toAssign(roleId).then((res) => {
if (res.code === 20000 && res.data.data) {
this.roles = res.data.data.allRoleList;
var jsonObject = res.data.data.assignRoles;
this.checkedRoles = this.getJsonList(jsonObject, "id");
}
});
},
//获取所有的角色id列表
getJsonList(json, key) {
var list = JSON.parse(JSON.stringify(json));
var strText = [];
for (var i = 0; i < list.length; i++) {
strText.push(list[i][key]);
}
return strText;
},
save() {
this.saveBtnDisabled = true;
var ids = this.checkedRoles.join(",");
if (ids === ",") {
ids = [];
}
doAssign(this.roleId, ids).then((res) => {
if (res.code === 20000 && res.success) {
this.$message({
type: "info",
message: "保存成功!",
});
this.$router.push({ path: "/user" });
} else {
this.$message({
type: "info",
message: "保存失败!",
});
}
});
},
},
};
</script>
<style>
</style>
4.5、数据初始化

4.6、分配的角色使用的是element ui的多选框组
控件如下
<el-checkbox-group v-model="checkedRoles" style="margin-top: 20px">
<el-checkbox v-for="role in roles" :key="role.id" :label="role.id">{{
role.roleName
}}</el-checkbox>
</el-checkbox-group>
4.7、方法roles是表示所有角色,checkedRoles表示的是已有的角色,这样页面就会默认勾选CheckBox了。
return {
saveBtnDisabled: false,
checkedRoles: [],
roles: [],
};
},
watch: {
$route(to, from) {
this.init();
},
},
created() {
this.init();
},
methods: {
init() {
if (this.$route.params && this.$route.params.id) {
this.roleId = this.$route.params.id;
this.getDataList(this.roleId);
}
},
getDataList(roleId) {
toAssign(roleId).then((res) => {
if (res.code === 20000 && res.data.data) {
this.roles = res.data.data.allRoleList;
var jsonObject = res.data.data.assignRoles;
this.checkedRoles = this.getJsonList(jsonObject, "id");
}
});
},
//获取所有的角色id列表
getJsonList(json, key) {
var list = JSON.parse(JSON.stringify(json));
var strText = [];
for (var i = 0; i < list.length; i++) {
strText.push(list[i][key]);
}
return strText;
},
4.8、allRoleList和assignRoles数据

4.9、checkedRoles

4.10、页面效果

4.11、controller
@GetMapping("toAssign/{userId}")
public R toAssign(@PathVariable String userId) {
return R.ok().data("data", roleService.findRoleByUserId(userId));
}
4.12、RoleServiceImpl
public Map<String, Object> findRoleByUserId(String userId) {
//获取所有角色
List<Role> allRoleList = baseMapper.selectList(new QueryWrapper<>());
//根据用户id获取角色列表
List<UserRole> existUserRoleList = userRoleService
.list(new QueryWrapper<UserRole>().eq("user_id", userId).select("role_id"));
//遍历所有菜单,获取每一项,看是否在权限列表,如果在,就标记
List<String> existRoleLists = existUserRoleList.stream().map(e -> e.getRoleId()).collect(Collectors.toList());
List<Role> assignRoles = new ArrayList<>();
allRoleList.forEach(role -> {
if (existRoleLists.contains(role.getId())) {
assignRoles.add(role);
}
});
Map<String, Object> roleMap = new HashMap<>();
roleMap.put("assignRoles", assignRoles);
roleMap.put("allRoleList", allRoleList);
return roleMap;
}
5、分配角色提交事件
5.1、方法
save() {
this.saveBtnDisabled = true;
var ids = this.checkedRoles.join(",");
if (ids === ",") {
ids = [];
}
doAssign(this.roleId, ids).then((res) => {
if (res.code === 20000 && res.success) {
this.$message({
type: "info",
message: "保存成功!",
});
this.$router.push({ path: "/user" });
} else {
this.$message({
type: "info",
message: "保存失败!",
});
}
});
},
5.2、UserController
@PostMapping("doAssign")
public R doAssign(String userId, String[] permissionIds) {
boolean result = roleService.saveUserRelationShip(userId, permissionIds);
if (result) {
return R.ok();
}
return R.error();
}
5.3、RoleServiceImpl
public boolean saveUserRelationShip(String userId, String[] roleIds) {
//删除旧的所有角色权限
userRoleService.remove(new QueryWrapper<UserRole>().eq("user_id", userId));
List<UserRole> list = new ArrayList<>();
for (String id : roleIds) {
UserRole rolePermission = new UserRole();
rolePermission.setRoleId(id);
rolePermission.setUserId(userId);
list.add(rolePermission);
}
return userRoleService.saveBatch(list);
}
十一、角色管理
1、角色管理页面

2、Role.vue
<template>
<div class="app-container">
<!--查询表单-->
<el-form :inline="true">
<el-form-item>
<el-input v-model="searchData.roleName" placeholder="角色" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="pageList()"
>查询</el-button
>
<el-button type="default" @click="resetData()">清空</el-button>
</el-form-item>
</el-form>
<div>
<el-button type="primary" size="mini" @click="add()">添加</el-button>
<!-- <el-button type="danger" size="mini" @click="batchRemove()"
>批量删除</el-button
> -->
</div>
<!-- 表格 -->
<el-table
:data="list"
border
stripe
style="margin-top: 20px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" />
<el-table-column label="ID" width="50">
<!-- 使用连续的序号 -->
<template slot-scope="scope">
{{ (currentPage - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="名称" width="200" />
<el-table-column label="操作" width="250">
<template slot-scope="scope">
<router-link :to="'/sys/role/distribution/' + scope.row.id">
<el-button type="info" size="mini" icon="el-icon-info"></el-button
></router-link>
<el-button
@click="handleEdit(scope.row)"
type="primary"
size="mini"
icon="el-icon-edit"
style="margin-left: 10px"
>编辑</el-button
>
<!-- <router-link :to="'/acl/role/update/' + scope.row.id">
<el-button
type="primary"
size="mini"
icon="el-icon-edit"
style="margin-left:10px;margin-right:10px;"
></el-button
></router-link> -->
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeById(scope.row)"
></el-button>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<el-pagination
:current-page="currentPage"
:page-size="limit"
:total="total"
:page-sizes="[5, 10, 15, 20]"
style="padding: 12px 8px; text-align: center"
layout="sizes, prev, pager, next, jumper, ->, total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-dialog :title="title" :visible.sync="saveBtnDisabled" width="35%">
<!-- 用户表单信息 -->
<el-form
ref="model"
:model="model"
:inline="true"
label-width="80px"
:rules="rules"
>
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="model.roleName"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="saveBtnDisabled = false">取 消</el-button>
<el-button type="primary" @click="save">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import roleApi from "@/api/role";
export default {
data() {
return {
list: [], //列表数据
total: 0,
currentPage: 1, //当前页
limit: 10, //每页记录数
searchData: {},
idList: null,
saveBtnDisabled: false,
//讲师对象
model: {
roleName: "",
},
};
},
created() {
this.pageList();
},
methods: {
handleEdit(val) {
this.title = "编辑";
this.saveBtnDisabled = true;
this.model = Object.assign({}, val);
this.dialogVisible = true;
},
//新增
save() {
roleApi.save(this.model).then((res) => {
this.saveBtnDisabled = false;
this.pageList();
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "添加成功",
});
this.$router.push({ path: "/role" });
} else {
this.$message({
type: "info",
message: "添加失败",
});
}
});
},
//修改
update() {
roleApi.update(this.model).then((res) => {
this.saveBtnDisabled = false;
this.pageList();
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "修改成功",
});
this.$router.push({ path: "/role" });
} else {
this.$message({
type: "info",
message: "修改失败",
});
}
});
},
//跳转到修改页面
add() {
this.saveBtnDisabled = true;
//this.$router.push({ path: `/acl/role/form/` });
},
// //批量删除
// batchRemove() {
// if (!this.idList || this.idList.length <= 0) {
// this.$message({
// type: "info",
// message: "请先选择要删除的数据!",
// });
// return false;
// }
// let arrayIds = [];
// this.idList.forEach((element) => {
// arrayIds.push(element.id);
// });
// this.$confirm("此操作将永久删除, 是否继续?", "提示", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// type: "warning",
// })
// .then(() => {
// //删除api
// roleApi.batchRemove(arrayIds).then((res) => {
// this.pageList();
// if (res.code === 20000 && res.data) {
// this.$message({
// type: "info",
// message: "刪除成功: ",
// });
// this.pageList();
// } else {
// this.$message({
// type: "info",
// message: "刪除失败: ",
// });
// }
// });
// })
// .catch(() => {
// this.$message({
// type: "info",
// message: "已取消删除",
// });
// });
// },
//多选
handleSelectionChange(idList) {
this.idList = idList;
},
//删除
removeById(data) {
this.$confirm(
"此操作将永久删除" + data.roleName + ", 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
//删除api
roleApi.removeById(data.id).then((res) => {
this.pageList();
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "刪除成功: ",
});
this.pageList();
} else {
this.$message({
type: "info",
message: "刪除失败: ",
});
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
resetData() {
this.searchData = {};
this.pageList();
},
//改变数量
handleSizeChange(size) {
this.limit = size;
this.pageList();
},
//改变页码
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.pageList();
},
//分页查询
pageList() {
roleApi
.pageList(this.currentPage, this.limit, this.searchData)
.then((res) => {
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "查询成功: ",
});
this.list = res.data.list;
this.total = res.data.total;
} else {
this.$message({
type: "info",
message: "查询失败: ",
});
}
})
.catch((e) => {
this.$message({
type: "info",
message: "查询异常",
});
});
},
},
};
</script>
3、定义的api接口role.js,使用的时候需要在页面引入
import request from '@/utils/request'
const apiName = '/admin/sys/role';
export default {
pageList(page, limit, Role) {
return request({
url: `${apiName}/pageList/${page}/${limit}`,
method: 'get',
params: Role
})
},
removeById(id) {
return request({
url: `${apiName}/deleteRole/${id}`,
method: 'delete'
})
},
save(role) {
return request({
url: `${apiName}/addRole`,
method: 'post',
data: role
})
},
//角色详情
getDetail(id) {
return request({
url: `${apiName}/get/${id}`,
method: 'get'
})
},
update(role) {
return request({
url: `${apiName}/updateRole`,
method: 'post',
data: role
})
},
//取得角色的集合
listAllRoles() {
return request({
url: '${apiName}/findAll',
method: 'get'
})
},
}
4、列表分页数据
4.1、Role.vue
<el-table
:data="list"
border
stripe
style="margin-top: 20px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" />
<el-table-column label="ID" width="50">
<!-- 使用连续的序号 -->
<template slot-scope="scope">
{{ (currentPage - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="名称" width="200" />
<el-table-column label="操作" width="250">
<template slot-scope="scope">
<router-link :to="'/sys/role/distribution/' + scope.row.id">
<el-button type="info" size="mini" icon="el-icon-info"></el-button
></router-link>
<el-button
@click="handleEdit(scope.row)"
type="primary"
size="mini"
icon="el-icon-edit"
style="margin-left: 10px"
>编辑</el-button
>
<!-- <router-link :to="'/acl/role/update/' + scope.row.id">
<el-button
type="primary"
size="mini"
icon="el-icon-edit"
style="margin-left:10px;margin-right:10px;"
></el-button
></router-link> -->
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeById(scope.row)"
></el-button>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<el-pagination
:current-page="currentPage"
:page-size="limit"
:total="total"
:page-sizes="[5, 10, 15, 20]"
style="padding: 12px 8px; text-align: center"
layout="sizes, prev, pager, next, jumper, ->, total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
4.2、handleSizeChange和handleCurrentChange
//改变数量
handleSizeChange(size) {
this.limit = size;
this.pageList();
},
//改变页码
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.pageList();
},
//分页查询
pageList() {
roleApi
.pageList(this.currentPage, this.limit, this.searchData)
.then((res) => {
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "查询成功: ",
});
this.list = res.data.list;
this.total = res.data.total;
} else {
this.$message({
type: "info",
message: "查询失败: ",
});
}
})
.catch((e) => {
this.$message({
type: "info",
message: "查询异常",
});
});
},
4.3、RoleController
@GetMapping("pageList/{page}/{limit}")
public R pageList(@PathVariable long page, @PathVariable long limit, Role role) {
Page<Role> pageParam = new Page<>(page, limit);
QueryWrapper<Role> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(role.getRoleName())) {
queryWrapper.like("role_name", role.getRoleName());
}
roleService.page(pageParam, queryWrapper);
Map<String, Object> map = new HashMap<String, Object>();
long total = pageParam.getTotal();
List<Role> list = pageParam.getRecords();
map.put("total", total);
map.put("list", list);
return R.ok().data(map);
}
5、新增和修改
5.1、vue页面
<el-dialog :title="title" :visible.sync="saveBtnDisabled" width="35%">
<!-- 用户表单信息 -->
<el-form
ref="model"
:model="model"
:inline="true"
label-width="80px"
:rules="rules"
>
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="model.roleName"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="saveBtnDisabled = false">取 消</el-button>
<el-button type="primary" @click="save">确 定</el-button>
</span>
</el-dialog>
5.2、新增和修改按钮
//跳转到修改页面
add() {
this.saveBtnDisabled = true;
this.title = "新增";
this.model = Object.assign({}, {});
//this.$router.push({ path: `/acl/role/form/` });
},
handleEdit(val) {
this.title = "编辑";
this.saveBtnDisabled = true;
this.model = Object.assign({}, val);
this.dialogVisible = true;
},
5.3、新增和修改的提交,这里部分新增和修改,由后台控制,有id是修改,没有id是新增,后台接口自动判断
save() {
roleApi.save(this.model).then((res) => {
this.saveBtnDisabled = false;
this.pageList();
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "添加成功",
});
this.$router.push({ path: "/role" });
} else {
this.$message({
type: "info",
message: "添加失败",
});
}
});
},
5.4、RoleController
由于用的mybatis plus,框架saveOrUpdate封装了方法,根据id判断是新增还是修改
@PostMapping("addRole")
public R addRole(@ApiParam("角色对象") @RequestBody Role role) {
boolean result = roleService.saveOrUpdate(role);
if (result) {
return R.ok().message("保存成功");
}
return R.error().message("保存失败");
}
6、删除
6.1、js方法
//删除
removeById(data) {
this.$confirm(
"此操作将永久删除" + data.roleName + ", 是否继续?",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
)
.then(() => {
//删除api
roleApi.removeById(data.id).then((res) => {
this.pageList();
if (res.code === 20000 && res.data) {
this.$message({
type: "info",
message: "刪除成功: ",
});
this.pageList();
} else {
this.$message({
type: "info",
message: "刪除失败: ",
});
}
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
6.2、RoleController
@DeleteMapping("deleteRole/{id}")
public R deleteRole(@PathVariable String id) {
/* if (true) {
throw new CustomException(ResultCodeEnum.DIVIDE_ZERO);
}*/
boolean result = roleService.removeById(id);
if (result) {
return R.ok().message("删除成功");
}
return R.error().message("删除失败");
}
7、角色权限
7.1、页面
角色权限实际上就是菜单,结构也是一样的,所以这里也要用到递归,当然不用递归也可以实现树形结构

7.2、角色列表跳转到权限的路由
<el-table-column label="操作" width="250">
<template slot-scope="scope">
<router-link :to="'/sys/role/distribution/' + scope.row.id">
<el-button type="info" size="mini" icon="el-icon-info"></el-button
></router-link>
<el-button
@click="handleEdit(scope.row)"
type="primary"
size="mini"
icon="el-icon-edit"
style="margin-left: 10px"
>编辑</el-button
>
<!-- <router-link :to="'/acl/role/update/' + scope.row.id">
<el-button
type="primary"
size="mini"
icon="el-icon-edit"
style="margin-left:10px;margin-right:10px;"
></el-button
></router-link> -->
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeById(scope.row)"
></el-button>
</template>
</el-table-column>
7.3、index路由
{
path: '/sys/role/distribution/:id',
name: '角色权限',
component: 'sys/role/roleForm',
meta: { title: '角色权限', icon: 'table' },
hidden: true
},
7.4、权限vue页面
<template>
<div style="margin: 29px">
<el-tree
ref="tree"
:data="data"
show-checkbox
default-expand-all
node-key="id"
highlight-current
:props="defaultProps"
/>
<el-button :disabled="saveBtnDisabled" type="primary" @click="save"
>保存</el-button
>
</div>
</template>
<script>
import {
toAssign,
doAssign,
} from "@/api/permission";
export default {
data() {
return {
saveBtnDisabled: false,
data: [],
roleId: "",
defaultProps: {
children: "children",
label: "name",
},
};
},
watch: {
$route(to, from) {
this.init();
},
},
created() {
this.init();
},
methods: {
init() {
if (this.$route.params && this.$route.params.id) {
this.roleId = this.$route.params.id;
this.getDataList(this.roleId);
}
},
getDataList(roleId) {
toAssign(roleId).then((res) => {
if (res.code === 20000 && res.data.data) {
this.data = res.data.data;
var list = [];
var jsonList = JSON.parse(JSON.stringify(this.data));
var llll = JSON.stringify(this.data);
console.log("this.data========================= " + this.data);
console.log("jsonList========================= " + jsonList);
console.log(
"JSON.stringify(this.data)========================= " +
JSON.stringify(this.data)
);
this.getJsonList(list, jsonList[0]["children"]);
this.$refs.tree.setCheckedKeys(list);
}
});
},
//获取所有的角色的权限
getJsonList(list, jsonList) {
for (var i = 0; i < jsonList.length; i++) {
if (
jsonList[i]["hasSelect"] == true &&
jsonList[i]["children"].length === 0
) {
list.push(jsonList[i]["id"]);
}
if (jsonList[i]["children"].length > 0) {
this.getJsonList(list, jsonList[i]["children"]);
}
}
},
save() {
this.saveBtnDisabled = true;
var beforeIds = this.$refs.tree.getHalfCheckedKeys().join(",");
var afterIds = this.$refs.tree.getCheckedKeys().join(",");
var ids = beforeIds + "," + afterIds;
if (ids === ",") {
ids = [];
}
console.log(ids);
doAssign(this.roleId, ids).then((res) => {
if (res.code === 20000 && res.success) {
this.$message({
type: "info",
message: "保存成功!",
});
this.$router.push({ path: "/role" });
} else {
this.$message({
type: "info",
message: "保存失败!",
});
}
});
},
},
};
</script>
<style>
</style>
7.5、方法
watch: {
$route(to, from) {
this.init();
},
},
created() {
this.init();
},
methods: {
init() {
if (this.$route.params && this.$route.params.id) {
this.roleId = this.$route.params.id;
this.getDataList(this.roleId);
}
},
getDataList(roleId) {
toAssign(roleId).then((res) => {
if (res.code === 20000 && res.data.data) {
this.data = res.data.data;
var list = [];
var jsonList = JSON.parse(JSON.stringify(this.data));
var llll = JSON.stringify(this.data);
console.log("this.data========================= " + this.data);
console.log("jsonList========================= " + jsonList);
console.log(
"JSON.stringify(this.data)========================= " +
JSON.stringify(this.data)
);
this.getJsonList(list, jsonList[0]["children"]);
this.$refs.tree.setCheckedKeys(list);
}
});
},
//获取所有的角色的权限
getJsonList(list, jsonList) {
for (var i = 0; i < jsonList.length; i++) {
if (
jsonList[i]["hasSelect"] == true &&
jsonList[i]["children"].length === 0
) {
list.push(jsonList[i]["id"]);
}
if (jsonList[i]["children"].length > 0) {
this.getJsonList(list, jsonList[i]["children"]);
}
}
},
},
7.6、接口返回的res.data.data数据结构
[
{
"id": "1",
"pid": "0",
"name": "全部数据",
"type": 0,
"permissionValue": null,
"path": null,
"component": null,
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:13:06",
"gmtModified": "2019-11-15 17:13:06",
"level": 1,
"children": [
{
"id": "1195268474480156673",
"pid": "1",
"name": "权限管理",
"type": 1,
"permissionValue": null,
"path": "",
"component": "",
"icon": "monitor",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:14:23",
"gmtModified": "2023-09-01 22:14:23",
"level": 2,
"children": [
{
"id": "1195268616021139457",
"pid": "1195268474480156673",
"name": "用户管理",
"type": 1,
"permissionValue": null,
"path": "/user",
"component": "sys/user/User",
"icon": "user",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:04:29",
"gmtModified": "2023-09-01 22:04:29",
"level": 3,
"children": [
{
"id": "1195269143060602882",
"pid": "1195268616021139457",
"name": "查看",
"type": 2,
"permissionValue": "user.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:15:45",
"gmtModified": "2019-11-17 21:57:16",
"level": 4,
"children": [ ],
"hasSelect": true
},
{
"id": "1195269295926206466",
"pid": "1195268616021139457",
"name": "添加",
"type": 2,
"permissionValue": "user.add",
"path": "user/add",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:16:22",
"gmtModified": "2019-11-15 17:16:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269473479483394",
"pid": "1195268616021139457",
"name": "修改",
"type": 2,
"permissionValue": "user.update",
"path": "user/update/:id",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:04",
"gmtModified": "2019-11-15 17:17:04",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269547269873666",
"pid": "1195268616021139457",
"name": "删除",
"type": 2,
"permissionValue": "user.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:22",
"gmtModified": "2019-11-15 17:17:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1196301740985311234",
"pid": "1195268616021139457",
"name": "分配角色",
"type": 2,
"permissionValue": "user.assgin",
"path": "/userForm/:id",
"component": "sys/user/userForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:44:21",
"gmtModified": "2023-09-01 18:44:21",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": true
},
{
"id": "1195268788138598401",
"pid": "1195268474480156673",
"name": "角色管理",
"type": 1,
"permissionValue": null,
"path": "/role",
"component": "sys/role/Role",
"icon": "s-check",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:10:24",
"gmtModified": "2023-09-01 22:10:24",
"level": 3,
"children": [
{
"id": "1195269821262782465",
"pid": "1195268788138598401",
"name": "修改",
"type": 2,
"permissionValue": "role.update",
"path": "role/update/:id",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:27",
"gmtModified": "2019-11-15 17:19:53",
"level": 4,
"children": [ ],
"hasSelect": true
},
{
"id": "1195269903542444034",
"pid": "1195268788138598401",
"name": "查看",
"type": 2,
"permissionValue": "role.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:47",
"gmtModified": "2019-11-15 17:18:47",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270037005197313",
"pid": "1195268788138598401",
"name": "添加",
"type": 2,
"permissionValue": "role.add",
"path": "role/add",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:19:19",
"gmtModified": "2019-11-18 11:05:42",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270442602782721",
"pid": "1195268788138598401",
"name": "删除",
"type": 2,
"permissionValue": "role.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:20:55",
"gmtModified": "2019-11-15 17:20:55",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270621548568578",
"pid": "1195268788138598401",
"name": "角色权限",
"type": 2,
"permissionValue": "role.distribution",
"path": "/sys/role/distribution/:id",
"component": "sys/role/roleForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:45:51",
"gmtModified": "2023-09-01 18:45:51",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": true
},
{
"id": "1195268893830864898",
"pid": "1195268474480156673",
"name": "菜单管理",
"type": 1,
"permissionValue": null,
"path": "/menu",
"component": "sys/menu/Menu",
"icon": "menu",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:06:13",
"gmtModified": "2023-09-01 22:06:13",
"level": 3,
"children": [
{
"id": "1195270744097742849",
"pid": "1195268893830864898",
"name": "查看",
"type": 2,
"permissionValue": "permission.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:07",
"gmtModified": "2019-11-15 17:22:07",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270810560684034",
"pid": "1195268893830864898",
"name": "添加",
"type": 2,
"permissionValue": "permission.add",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:23",
"gmtModified": "2019-11-15 17:22:23",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270862100291586",
"pid": "1195268893830864898",
"name": "修改",
"type": 2,
"permissionValue": "permission.update",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:35",
"gmtModified": "2019-11-15 17:22:35",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270887933009922",
"pid": "1195268893830864898",
"name": "删除",
"type": 2,
"permissionValue": "permission.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:41",
"gmtModified": "2019-11-15 17:22:41",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
}
],
"hasSelect": true
}
],
"hasSelect": true
}
]
7.7、jsonList0children的数据结构
[
{
"id": "1195268474480156673",
"pid": "1",
"name": "权限管理",
"type": 1,
"permissionValue": null,
"path": "",
"component": "",
"icon": "monitor",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:14:23",
"gmtModified": "2023-09-01 22:14:23",
"level": 2,
"children": [
{
"id": "1195268616021139457",
"pid": "1195268474480156673",
"name": "用户管理",
"type": 1,
"permissionValue": null,
"path": "/user",
"component": "sys/user/User",
"icon": "user",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:04:29",
"gmtModified": "2023-09-01 22:04:29",
"level": 3,
"children": [
{
"id": "1195269143060602882",
"pid": "1195268616021139457",
"name": "查看",
"type": 2,
"permissionValue": "user.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:15:45",
"gmtModified": "2019-11-17 21:57:16",
"level": 4,
"children": [ ],
"hasSelect": true
},
{
"id": "1195269295926206466",
"pid": "1195268616021139457",
"name": "添加",
"type": 2,
"permissionValue": "user.add",
"path": "user/add",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:16:22",
"gmtModified": "2019-11-15 17:16:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269473479483394",
"pid": "1195268616021139457",
"name": "修改",
"type": 2,
"permissionValue": "user.update",
"path": "user/update/:id",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:04",
"gmtModified": "2019-11-15 17:17:04",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269547269873666",
"pid": "1195268616021139457",
"name": "删除",
"type": 2,
"permissionValue": "user.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:22",
"gmtModified": "2019-11-15 17:17:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1196301740985311234",
"pid": "1195268616021139457",
"name": "分配角色",
"type": 2,
"permissionValue": "user.assgin",
"path": "/userForm/:id",
"component": "sys/user/userForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:44:21",
"gmtModified": "2023-09-01 18:44:21",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": true
},
{
"id": "1195268788138598401",
"pid": "1195268474480156673",
"name": "角色管理",
"type": 1,
"permissionValue": null,
"path": "/role",
"component": "sys/role/Role",
"icon": "s-check",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:10:24",
"gmtModified": "2023-09-01 22:10:24",
"level": 3,
"children": [
{
"id": "1195269821262782465",
"pid": "1195268788138598401",
"name": "修改",
"type": 2,
"permissionValue": "role.update",
"path": "role/update/:id",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:27",
"gmtModified": "2019-11-15 17:19:53",
"level": 4,
"children": [ ],
"hasSelect": true
},
{
"id": "1195269903542444034",
"pid": "1195268788138598401",
"name": "查看",
"type": 2,
"permissionValue": "role.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:47",
"gmtModified": "2019-11-15 17:18:47",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270037005197313",
"pid": "1195268788138598401",
"name": "添加",
"type": 2,
"permissionValue": "role.add",
"path": "role/add",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:19:19",
"gmtModified": "2019-11-18 11:05:42",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270442602782721",
"pid": "1195268788138598401",
"name": "删除",
"type": 2,
"permissionValue": "role.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:20:55",
"gmtModified": "2019-11-15 17:20:55",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270621548568578",
"pid": "1195268788138598401",
"name": "角色权限",
"type": 2,
"permissionValue": "role.distribution",
"path": "/sys/role/distribution/:id",
"component": "sys/role/roleForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:45:51",
"gmtModified": "2023-09-01 18:45:51",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": true
},
{
"id": "1195268893830864898",
"pid": "1195268474480156673",
"name": "菜单管理",
"type": 1,
"permissionValue": null,
"path": "/menu",
"component": "sys/menu/Menu",
"icon": "menu",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:06:13",
"gmtModified": "2023-09-01 22:06:13",
"level": 3,
"children": [
{
"id": "1195270744097742849",
"pid": "1195268893830864898",
"name": "查看",
"type": 2,
"permissionValue": "permission.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:07",
"gmtModified": "2019-11-15 17:22:07",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270810560684034",
"pid": "1195268893830864898",
"name": "添加",
"type": 2,
"permissionValue": "permission.add",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:23",
"gmtModified": "2019-11-15 17:22:23",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270862100291586",
"pid": "1195268893830864898",
"name": "修改",
"type": 2,
"permissionValue": "permission.update",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:35",
"gmtModified": "2019-11-15 17:22:35",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270887933009922",
"pid": "1195268893830864898",
"name": "删除",
"type": 2,
"permissionValue": "permission.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:41",
"gmtModified": "2019-11-15 17:22:41",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
}
],
"hasSelect": true
}
]
7.8、树形结构数据讲解
this.data = res.data.data;是设置树形结构的全部节点
this.$refs.tree.setCheckedKeys(list);是设置树形结构的选择的节点,也就是角色已经拥有的权限
7.9、树形结构已有权限的数据通过递归取得
//获取所有的角色的权限
getJsonList(list, jsonList) {
for (var i = 0; i < jsonList.length; i++) {
if (
jsonList[i]["hasSelect"] == true &&
jsonList[i]["children"].length === 0
) {
list.push(jsonList[i]["id"]);
}
if (jsonList[i]["children"].length > 0) {
this.getJsonList(list, jsonList[i]["children"]);
}
}
},
7.10、PermissionController获取权限的接口
@GetMapping("toAssign/{roleId}")
public R toAssign(@PathVariable String roleId) {
return R.ok().data("data", permissionService.listAllMenu(roleId));
}
7.11、根据角色获取菜单
/***********************************
* 用途说明:根據角色獲取菜單
* @param id
* 返回值说明:
* @return java.util.List<com.stu.service.acl.entity.Permission>
***********************************/
@Override
public List<Permission> listAllMenu(String id) {
//获取所有菜单
List<Permission> allPermissionList = baseMapper.selectList(new QueryWrapper<>());
//根据角色id呼气角色权限列表
List<RolePermission> rolePermissionsList = rolePermissionService
.list(new QueryWrapper<RolePermission>().eq("role_id", id));
//遍历所有菜单,获取每一项,看是否在权限列表,如果在,就标记
List<String> permissionIdList = rolePermissionsList.stream().map(e -> e.getPermissionId()).collect(Collectors.toList());
allPermissionList.forEach(permission -> {
if (permissionIdList.contains(permission.getId())) {
permission.setHasSelect(true);
} else {
permission.setHasSelect(false);
}
});
/*for (int i = 0; i < allPermissionList.size(); i++) {
Permission permission = allPermissionList.get(i);
for (int m = 0; m < rolePermissionList.size(); m++) {
RolePermission rolePermission = rolePermissionList.get(m);
if(rolePermission.getPermissionId().equals(permission.getId())) {
permission.setSelect(true);
}
}
}*/
return bulidPermission(allPermissionList);
}
/***********************************
* 用途说明:把返回所有菜单list集合进行封装的方法
* @param list
* 返回值说明:
* @return java.util.List<com.stu.service.acl.entity.Permission>
***********************************/
private List<Permission> bulidPermission(List<Permission> list) {
//创建list集合,用于数据最终封装
List<Permission> finalNode = new ArrayList<>();
//把所有菜单list集合遍历,得到顶层菜单 pid=0菜单,设置level是1
for (Permission permission : list) {
//得到顶层菜单 pid=0菜单
if ("0".equals(permission.getPid())) {
permission.setLevel(1);
//根据顶层菜单,向里面进行查询子菜单,封装到finalNode里面
finalNode.add(selectChildren(permission, list));
}
}
return finalNode;
}
/***********************************
* 用途说明:递归查询下级菜单
* @param permission
* @param list
* 返回值说明:
* @return com.stu.service.acl.entity.Permission
***********************************/
private Permission selectChildren(Permission permission, List<Permission> list) {
//1 因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
permission.setChildren(new ArrayList<Permission>());
//2 遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
for (Permission child : list) {
if (permission.getId().equals(child.getPid())) {
child.setLevel(permission.getLevel() + 1);
if (child.getChildren() == null) {
child.setChildren(new ArrayList<>());
}
// permission.getChildren().add(child);
// selectChildren(child,list);
permission.getChildren().add(selectChildren(child, list));
}
}
return permission;
}
7.12、数据库数据

8、角色权限提交
8.1、vue页面
save() {
this.saveBtnDisabled = true;
var beforeIds = this.$refs.tree.getHalfCheckedKeys().join(",");
var afterIds = this.$refs.tree.getCheckedKeys().join(",");
var ids = beforeIds + "," + afterIds;
if (ids === ",") {
ids = [];
}
console.log(ids);
doAssign(this.roleId, ids).then((res) => {
if (res.code === 20000 && res.success) {
this.$message({
type: "info",
message: "保存成功!",
});
this.$router.push({ path: "/role" });
} else {
this.$message({
type: "info",
message: "保存失败!",
});
}
});
},
8.2、PermissionController
@PostMapping("doAssign")
public R doAssign(String roleId, String[] permissionId) {
boolean result = permissionService.saveRolePermissionrelationShip(roleId, permissionId);
if (result) {
return R.ok();
}
return R.error();
}
8.3、PermissionServiceImpl
/***********************************
* 用途说明:给角色分配菜单权限
* @param roleId
* @param permissionId
* 返回值说明:
* @return boolean
***********************************/
@Override
public boolean saveRolePermissionrelationShip(String roleId, String[] permissionId) {
//删除旧的权限
boolean result = rolePermissionService.remove(new QueryWrapper<RolePermission>().eq("role_id", roleId));
if (null != permissionId && permissionId.length > 0) {
List<RolePermission> list = new ArrayList<>();
for (String id : permissionId) {
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(id);
list.add(rolePermission);
}
return rolePermissionService.saveBatch(list);
}
return result;
}
十二、菜单管理
1、页面

2、Menu.vue
<template>
<div>
<el-table
:data="tableData"
style="width: 100%; margin-bottom: 20px"
row-key="id"
border
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="name" label="名称" sortable min-width="20%">
</el-table-column>
<el-table-column prop="path" label="访问路径" sortable min-width="15%">
</el-table-column>
<el-table-column
prop="component"
label="组件路径"
sortable
min-width="15%"
>
</el-table-column>
<el-table-column
prop="permissionValue"
label="权限值"
sortable
min-width="15%"
>
</el-table-column>
<el-table-column prop="status" label="状态" sortable min-width="10%">
</el-table-column>
<el-table-column prop="icon" label="图标" sortable min-width="10%">
</el-table-column>
<el-table-column label="操作" min-width="15%">
<template slot-scope="scope">
<!-- 第一层和第二层可以添加菜单,第三层添加功能,第四层修改功能 -->
<el-button
v-if="scope.row.level === 1 || scope.row.level === 2"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'add')"
>添加菜单</el-button
>
<el-button
v-if="scope.row.level === 3"
type="text"
size="mini"
@click="
() => {
dialogPermissionFormVisible = true;
permissionForm = { ...PERMISSION_FORM };
permissionForm.pid = scope.row.id;
permissionTitle = '添加功能';
}
"
>
添加功能</el-button
>
<el-button
v-if="scope.row.level === 4"
type="text"
size="mini"
@click="updatePermissionDialog(scope.row)"
>修改功能</el-button
>
<el-button
v-if="scope.row.level !== 4"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'edit')"
>修改</el-button
>
<el-button @click="deleteSubmit(scope.row)" type="text" size="mini"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 添加菜单的窗口 -->
<el-dialog
:title="title"
:label-position="labelPosition"
:visible.sync="dialogFormVisible"
>
<el-form
ref="menuForm"
:model="menuForm"
:rules="menuFormRules"
label-width="120px"
>
<el-form-item label="菜单名称" prop="name">
<el-input v-model="menuForm.name"></el-input>
</el-form-item>
<el-form-item label="访问路径" prop="path">
<el-input v-model="menuForm.path"></el-input>
</el-form-item>
<el-form-item label="组件路径" prop="component">
<el-input v-model="menuForm.component"></el-input>
</el-form-item>
<!-- <el-form-item label="权限值" prop="permissionValue">
<el-input v-model="menuForm.permissionValue"></el-input>
</el-form-item> -->
<el-form-item label="图标" prop="icon">
<el-input v-model="menuForm.icon"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="resetMenu()">取 消</el-button>
<el-button type="primary" @click="addMenuFormSubmit()">确 定</el-button>
</div>
</el-dialog>
<!-- 添加功能的窗口 -->
<el-dialog
:title="permissionTitle"
:label-position="labelPosition"
:visible.sync="dialogPermissionFormVisible"
>
<el-form
ref="permissionForm"
:model="permissionForm"
:rules="permissionFormRules"
label-width="120px"
>
<el-form-item label="功能名称" prop="name">
<el-input v-model="permissionForm.name"></el-input>
</el-form-item>
<el-form-item label="访问路径" prop="path">
<el-input v-model="permissionForm.path"></el-input>
</el-form-item>
<el-form-item label="组件路径" prop="component">
<el-input v-model="permissionForm.component"></el-input>
</el-form-item>
<el-form-item label="功能权限值" prop="permissionValue">
<el-input v-model="permissionForm.permissionValue"></el-input>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input v-model="permissionForm.icon"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="resetPermission()">取 消</el-button>
<el-button type="primary" @click="addPermissonFormSubmit()"
>确 定</el-button
>
</div>
</el-dialog>
</div>
</template>
<script>
import { getData, updateMenuSubmit, deleteSubmit } from "@/api/permission.js";
import http from "@/utils/request";
const MENU_FORM = {
name: "",
path: "",
component: "",
// status: 0,
icon: "setting",
pid: 0,
type: "1",
id: "",
};
const PERMISSION_FORM = {
name: "",
path: "",
component: "",
permissionValue: "",
// status: 0,
icon: "setting",
pid: 0,
type: "2",
id: "",
};
export default {
data() {
return {
labelPosition: "left",
tableData: [], //获取递归取得的全部菜单
dialogFormVisible: false, //添加修改菜单窗口开关
dialogPermissionFormVisible: false, //添加修改功能窗口开关
title: "添加菜单", //添加修改菜单标题
permissionTitle: "添加功能", //添加修改菜单标题
menuForm: MENU_FORM, //添加修改菜单内容
permissionForm: PERMISSION_FORM, //添加修改功能内容
//表单验证
menuFormRules: {
name: [
{ required: true, message: "请输入菜单名称", trigger: "blur" },
{ min: 3, max: 8, message: "长度在 3 到 8 个字符", trigger: "blur" },
],
path: [
{ required: true, message: "请输入访问路径", trigger: "blur" },
{
min: 3,
max: 18,
message: "长度在 3 到 18 个字符",
trigger: "blur",
},
],
component: [
{ required: true, message: "请输入组件路径", trigger: "blur" },
{
min: 3,
max: 18,
message: "长度在 3 到 18 个字符",
trigger: "blur",
},
],
},
//功能表单验证
permissionFormRules: {
name: [
{ required: true, message: "请输入菜单名称", trigger: "blur" },
{ min: 3, max: 8, message: "长度在 3 到 8 个字符", trigger: "blur" },
],
permissionValue: [
{ required: true, message: "请输入权限值", trigger: "blur" },
{
min: 3,
max: 18,
message: "长度在 3 到 18 个字符",
trigger: "blur",
},
],
},
};
},
created() {
//获取递归取得的全部菜单
this.getAllMenuList();
},
methods: {
//修改功能
updatePermissionDialog(val) {
this.dialogPermissionFormVisible = true;
this.permissionForm = val;
this.permissionTitle = "修改功能";
},
//取消菜单添加或修改
resetMenu() {
this.dialogFormVisible = false;
this.menuForm = {};
},
//取消功能添加或修改
resetPermission() {
this.dialogPermissionFormVisible = false;
this.permissionForm = {};
},
//添加修改下级功能
addPermissonFormSubmit() {
let that = this;
this.$refs.permissionForm.validate((valid) => {
if (valid) {
if (that.permissionForm.id) {
//修改
updateMenuSubmit(that.permissionForm).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "修改功能成功",
});
//刷新页面
that.getAllMenuList();
that.dialogPermissionFormVisible = false;
that.permissionForm = { ...PERMISSION_FORM };
}
});
} else {
//新增
//let permission = that.menuForm;
http({
url: "/permission/save",
method: "post",
data: that.permissionForm,
}).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "添加功能成功",
});
//刷新页面
that.getAllMenuList();
that.dialogPermissionFormVisible = false;
that.permissionForm = { ...PERMISSION_FORM };
}
});
}
} else {
return false;
}
});
},
//删除
deleteSubmit(val) {
let that = this;
this.$confirm("此操作将永久删除该记录,是否继续?", "提升",{
distinguishCancelAndClose: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
})
.then(() => {
deleteSubmit(val.id).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "删除成功",
});
//刷新页面
that.getAllMenuList();
}
});
this.$message({
type: 'info',
message: '保存修改'
});
})
.catch(action => {
this.$message({
type: 'info',
message: action === 'cancel'
? '放弃保存并离开页面'
: '停留在当前页面'
})
});
// let that = this;
// this.$confirm("此操作将永久删除该记录,是否继续?", "提升", {
// cofirmButtonText: "确定",
// cancelButtonText: "取消",
// type: "warning",
// }).then(() => {
// deleteSubmit(val.id).then((res) => {
// if (res.code === 20000) {
// that.$message({
// type: "success",
// message: "删除成功",
// });
// //刷新页面
// that.getAllMenuList();
// }
// });
// });
},
//修改
updateMenuSubmit() {
let that = this;
updateMenuSubmit(that.menuForm).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "修改菜单成功",
});
//刷新页面
that.getAllMenuList();
that.dialogFormVisible = false;
}
});
},
//新增
addMenuSubmit() {
let that = this;
//let permission = that.menuForm;
http({
url: "/permission/add",
method: "post",
data: that.menuForm,
}).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "添加菜单成功",
});
//刷新页面
that.getAllMenuList();
that.dialogFormVisible = false;
}
});
},
//提交添加菜单的数据
addMenuFormSubmit() {
let that = this;
this.$refs.menuForm.validate((valid) => {
if (valid) {
if (that.menuForm.id) {
that.title = "修改菜单";
//修改
that.updateMenuSubmit();
} else {
that.title = "添加菜单";
//新增
that.addMenuSubmit();
}
} else {
return false;
}
});
},
//显示添加菜单
addOrEditMenuDialog(val, flag) {
if ("add" === flag) {
//新增的话,页面的值初始化为定义好的常量
this.menuForm = { ...MENU_FORM };
//新增的话,新增的菜单的pid为当前选中的行的id
this.menuForm.pid = val.id;
} else {
this.title = "修改菜单";
//修改的话,页面的值初始化为列表的具体的内容
Object.assign(this.menuForm, val);
//修改
}
this.dialogFormVisible = true;
},
//获取递归取得的全部菜单
getAllMenuList() {
getData().then((res) => {
if (res.code === 20000 && res.data) {
this.tableData = res.data.data;
}
});
},
},
};
</script>
3、permission.js
import http from "@/utils/request";
const url = '/admin/sys/permission';
//请求首页的数据
export const getData = () => {
return http.get('/admin/sys/permission/getAllMenuList');
}
//更新菜单数据
export const updateMenuSubmit = (permission) => {
return http({
url: "/admin/sys/permission/update",
method: "put",
data: permission
})
}
//更新菜单数据
export const deleteSubmit = (id) => {
return http({
url: `${url}/delete/${id}`,
method: 'delete'
})
}
//获取全部权限菜单
export const ListAllPermissions = () => {
return http({
url: `${url}` + '/ListAllPermissions',
method: 'get'
})
}
export const saveMenu = (menu) => {
return http({
url: `${url}` + '/save',
method: 'post',
data: menu
})
}
export const update = (menu) => {
return http({
url: `${url}'/update`,
method: 'put',
data: menu
})
}
export const removeChildById = (id) => {
return http({
url: `${url}/delete/${id}`,
method: 'delete'
})
}
//根据角色获取菜单
export const toAssign = (roleId) => {
return http({
url: `${url}/toAssign/${roleId}`,
method: 'get'
})
}
//给角色分配菜单权限
export const doAssign = (roleId, permissionId) => {
return http({
url: `${url}/doAssign`,
method: 'post',
params: { roleId, permissionId }
})
}
4、列表
4.1、Menu.vue
4.1.1、列表使用树形数据
4.1.2、注意属性值children要和后端的children一样
<el-table
:data="tableData"
style="width: 100%; margin-bottom: 20px"
row-key="id"
border
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="name" label="名称" sortable min-width="20%">
</el-table-column>
<el-table-column prop="path" label="访问路径" sortable min-width="15%">
</el-table-column>
<el-table-column
prop="component"
label="组件路径"
sortable
min-width="15%"
>
</el-table-column>
<el-table-column
prop="permissionValue"
label="权限值"
sortable
min-width="15%"
>
</el-table-column>
<el-table-column prop="status" label="状态" sortable min-width="10%">
</el-table-column>
<el-table-column prop="icon" label="图标" sortable min-width="10%">
</el-table-column>
<el-table-column label="操作" min-width="15%">
<template slot-scope="scope">
<!-- 第一层和第二层可以添加菜单,第三层添加功能,第四层修改功能 -->
<el-button
v-if="scope.row.level === 1 || scope.row.level === 2"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'add')"
>添加菜单</el-button
>
<el-button
v-if="scope.row.level === 3"
type="text"
size="mini"
@click="
() => {
dialogPermissionFormVisible = true;
permissionForm = { ...PERMISSION_FORM };
permissionForm.pid = scope.row.id;
permissionTitle = '添加功能';
}
"
>
添加功能</el-button
>
<el-button
v-if="scope.row.level === 4"
type="text"
size="mini"
@click="updatePermissionDialog(scope.row)"
>修改功能</el-button
>
<el-button
v-if="scope.row.level !== 1 && scope.row.level !== 4"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'edit')"
>修改</el-button
>
<el-button
v-if="scope.row.level !== 1"
@click="deleteSubmit(scope.row)"
type="text"
size="mini"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
4.1.3、操作
操作分为添加菜单、添加功能、修改功能、修改菜单、删除。
1、分析,菜单这里分为4个部分,顶级模块、模块、菜单、功能,这里分别根据后台返回leve进行区分,1顶级模块,2模块,3菜单,4功能。
模块可以新增菜单,菜单可以新增功能。
顶级模块不能删除修改,可以新增菜单。
模块可以新增,修改,删除菜单。
功能可以进行修改和删除。
2、通过看这个图片可以看出,level等于1和2的时候是模块,可以添加菜单,但是全部数据也就是顶级模块,不可以删除修改,可以新增菜单,2级模块可以新增,修改,删除菜单
3、leve等于3的时候就是带路由的菜单了(页面的跳转就是根据这个访问路径和组件路径实现的),是可以跳转页面的,这个可以添加,修改,删除菜单,动态菜单和路由就是根据这个实现的。
4、leve等于4的时候就是功能了,这个就是增删改查的一些权限,可以修改和删除功能,后边的权限就是根据这个实现的。

<el-table-column label="操作" min-width="15%">
<template slot-scope="scope">
<!-- 第一层和第二层可以添加菜单,第三层添加功能,第四层修改功能 -->
<el-button
v-if="scope.row.level === 1 || scope.row.level === 2"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'add')"
>添加菜单</el-button
>
<el-button
v-if="scope.row.level === 3"
type="text"
size="mini"
@click="
() => {
dialogPermissionFormVisible = true;
permissionForm = { ...PERMISSION_FORM };
permissionForm.pid = scope.row.id;
permissionTitle = '添加功能';
}
"
>
添加功能</el-button
>
<el-button
v-if="scope.row.level === 4"
type="text"
size="mini"
@click="updatePermissionDialog(scope.row)"
>修改功能</el-button
>
<el-button
v-if="scope.row.level !== 1 && scope.row.level !== 4"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'edit')"
>修改</el-button
>
<el-button
v-if="scope.row.level !== 1"
@click="deleteSubmit(scope.row)"
type="text"
size="mini"
>删除</el-button
>
</template>
</el-table-column>
4.2、PermissionController
/***********************************
* 用途说明:获取全部菜单
* 返回值说明:
* @return com.stu.myserver.utils.R
***********************************/
@GetMapping("/getAllMenuList")
public R getAllMenuList() {
return R.ok().data("data", permissionService.listPermissions());
}
4.3、PermissionServiceImpl
首先在数据库里查出pid等于0的父id的全部数据,然后设置level等于1,顶级的pid的子id和子子id的level是层级加1,然后通过递归找到所有的树形数据,前端是根据这个leve进行操作判断的。
/***********************************
* 用途说明:查询所有权限菜单
* 返回值说明:
* @return java.util.List<com.stu.service.acl.entity.Permission>
***********************************/
@Override
public List<Permission> ListAllPermissions() {
QueryWrapper<Permission> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("id");
return bulidPermission(baseMapper.selectList(queryWrapper));
}
/***********************************
* 用途说明:把返回所有菜单list集合进行封装的方法
* @param list
* 返回值说明:
* @return java.util.List<com.stu.service.acl.entity.Permission>
***********************************/
private List<Permission> bulidPermission(List<Permission> list) {
//创建list集合,用于数据最终封装
List<Permission> finalNode = new ArrayList<>();
//把所有菜单list集合遍历,得到顶层菜单 pid=0菜单,设置level是1
for (Permission permission : list) {
//得到顶层菜单 pid=0菜单
if ("0".equals(permission.getPid())) {
permission.setLevel(1);
//根据顶层菜单,向里面进行查询子菜单,封装到finalNode里面
finalNode.add(selectChildren(permission, list));
}
}
return finalNode;
}
/***********************************
* 用途说明:递归查询下级菜单
* @param permission
* @param list
* 返回值说明:
* @return com.stu.service.acl.entity.Permission
***********************************/
private Permission selectChildren(Permission permission, List<Permission> list) {
//1 因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
permission.setChildren(new ArrayList<Permission>());
//2 遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
for (Permission child : list) {
if (permission.getId().equals(child.getPid())) {
child.setLevel(permission.getLevel() + 1);
if (child.getChildren() == null) {
child.setChildren(new ArrayList<>());
}
// permission.getChildren().add(child);
// selectChildren(child,list);
permission.getChildren().add(selectChildren(child, list));
}
}
return permission;
}
4.4、Permission
package com.stu.myserver.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
* <p>
* 权限
* </p>
*
* @author 程序员小明1024
* @since 2023-08-07
*/
@Data
@TableName("acl_permission")
@ApiModel(value = "Permission对象", description = "权限")
public class Permission implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编号")
private String id;
@ApiModelProperty("所属上级")
private String pid;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("类型(1:菜单,2:按钮)")
private Integer type;
@ApiModelProperty("权限值")
private String permissionValue;
@ApiModelProperty("访问路径")
private String path;
@ApiModelProperty("组件路径")
private String component;
@ApiModelProperty("图标")
private String icon;
@ApiModelProperty("状态(0:禁止,1:正常)")
private Integer status;
@ApiModelProperty("逻辑删除 1(true)已删除, 0(false)未删除")
private Integer isDeleted;
@ApiModelProperty("创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime gmtCreate;
@ApiModelProperty("更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime gmtModified;
//递归遍历菜单用到的3个属性
@ApiModelProperty(value = "层级")
@TableField(exist = false)
private Integer level;
@ApiModelProperty(value = "下级,子集")
@TableField(exist = false)
private List<Permission> children;
@ApiModelProperty(value = "是否选中")
@TableField(exist = false)
private boolean hasSelect;
}
4.5、树形数据
[
{
"id": "1",
"pid": "0",
"name": "全部数据",
"type": 0,
"permissionValue": null,
"path": null,
"component": null,
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:13:06",
"gmtModified": "2019-11-15 17:13:06",
"level": 1,
"children": [
{
"id": "1195268474480156673",
"pid": "1",
"name": "权限管理",
"type": 1,
"permissionValue": null,
"path": "",
"component": "",
"icon": "monitor",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:14:23",
"gmtModified": "2023-09-01 22:14:23",
"level": 2,
"children": [
{
"id": "1195268893830864898",
"pid": "1195268474480156673",
"name": "菜单管理",
"type": 1,
"permissionValue": null,
"path": "/menu",
"component": "sys/menu/Menu",
"icon": "menu",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:06:13",
"gmtModified": "2023-09-01 22:06:13",
"level": 3,
"children": [
{
"id": "1195270887933009922",
"pid": "1195268893830864898",
"name": "删除",
"type": 2,
"permissionValue": "permission.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:41",
"gmtModified": "2019-11-15 17:22:41",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270862100291586",
"pid": "1195268893830864898",
"name": "修改",
"type": 2,
"permissionValue": "permission.update",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:35",
"gmtModified": "2019-11-15 17:22:35",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270810560684034",
"pid": "1195268893830864898",
"name": "添加",
"type": 2,
"permissionValue": "permission.add",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:23",
"gmtModified": "2019-11-15 17:22:23",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270744097742849",
"pid": "1195268893830864898",
"name": "查看",
"type": 2,
"permissionValue": "permission.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:07",
"gmtModified": "2019-11-15 17:22:07",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
},
{
"id": "1195268788138598401",
"pid": "1195268474480156673",
"name": "角色管理",
"type": 1,
"permissionValue": null,
"path": "/role",
"component": "sys/role/Role",
"icon": "s-check",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:10:24",
"gmtModified": "2023-09-01 22:10:24",
"level": 3,
"children": [
{
"id": "1195270621548568578",
"pid": "1195268788138598401",
"name": "角色权限",
"type": 2,
"permissionValue": "role.distribution",
"path": "/sys/role/distribution/:id",
"component": "sys/role/roleForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:45:51",
"gmtModified": "2023-09-01 18:45:51",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270442602782721",
"pid": "1195268788138598401",
"name": "删除",
"type": 2,
"permissionValue": "role.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:20:55",
"gmtModified": "2019-11-15 17:20:55",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270037005197313",
"pid": "1195268788138598401",
"name": "添加",
"type": 2,
"permissionValue": "role.add",
"path": "role/add",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:19:19",
"gmtModified": "2019-11-18 11:05:42",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269903542444034",
"pid": "1195268788138598401",
"name": "查看",
"type": 2,
"permissionValue": "role.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:47",
"gmtModified": "2019-11-15 17:18:47",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269821262782465",
"pid": "1195268788138598401",
"name": "修改",
"type": 2,
"permissionValue": "role.update",
"path": "role/update/:id",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:27",
"gmtModified": "2019-11-15 17:19:53",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
},
{
"id": "1195268616021139457",
"pid": "1195268474480156673",
"name": "用户管理",
"type": 1,
"permissionValue": null,
"path": "/user",
"component": "sys/user/User",
"icon": "user",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:04:29",
"gmtModified": "2023-09-01 22:04:29",
"level": 3,
"children": [
{
"id": "1196301740985311234",
"pid": "1195268616021139457",
"name": "分配角色",
"type": 2,
"permissionValue": "user.assgin",
"path": "/userForm/:id",
"component": "sys/user/userForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:44:21",
"gmtModified": "2023-09-01 18:44:21",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269547269873666",
"pid": "1195268616021139457",
"name": "删除",
"type": 2,
"permissionValue": "user.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:22",
"gmtModified": "2019-11-15 17:17:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269473479483394",
"pid": "1195268616021139457",
"name": "修改",
"type": 2,
"permissionValue": "user.update",
"path": "user/update/:id",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:04",
"gmtModified": "2019-11-15 17:17:04",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269295926206466",
"pid": "1195268616021139457",
"name": "添加",
"type": 2,
"permissionValue": "user.add",
"path": "user/add",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:16:22",
"gmtModified": "2019-11-15 17:16:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269143060602882",
"pid": "1195268616021139457",
"name": "查看",
"type": 2,
"permissionValue": "user.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:15:45",
"gmtModified": "2019-11-17 21:57:16",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
}
],
"hasSelect": false
}
],
"hasSelect": false
}
]
4.6、数据库数据

5、添加、修改菜单
5.1、添加修改菜单vue
注意菜单实际准确来说分为模块和菜单,都是同一个页面,只不过模块页面只有菜单名称和图标,菜单才有菜单名称,访问路径,组件路径,图标。访问路径,组件路径的显示隐藏是通过v-if="showFlag"控制的

5.2、按钮
<!-- 第一层和第二层可以添加菜单,第三层添加功能,第四层修改功能 -->
<el-button
v-if="scope.row.level === 1 || scope.row.level === 2"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'add')"
>添加菜单</el-button
>
<el-button
v-if="scope.row.level !== 1 && scope.row.level !== 4"
type="text"
size="mini"
@click="addOrEditMenuDialog(scope.row, 'edit')"
>修改</el-button
>
5.3、vue
访问路径,组件路径的显示隐藏是通过v-if="showFlag"控制的
<!-- 添加菜单的窗口 -->
<el-dialog
:title="title"
:label-position="labelPosition"
:visible.sync="dialogFormVisible"
>
<el-form
ref="menuForm"
:model="menuForm"
:rules="menuFormRules"
label-width="120px"
>
<el-form-item label="菜单名称" prop="name">
<el-input v-model="menuForm.name"></el-input>
</el-form-item>
<el-form-item label="访问路径" prop="path" v-if="showFlag">
<el-input v-model="menuForm.path"></el-input>
</el-form-item>
<el-form-item label="组件路径" prop="component" v-if="showFlag">
<el-input v-model="menuForm.component"></el-input>
</el-form-item>
<!-- <el-form-item label="权限值" prop="permissionValue">
<el-input v-model="menuForm.permissionValue"></el-input>
</el-form-item> -->
<el-form-item label="图标" prop="icon">
<el-input v-model="menuForm.icon"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="resetMenu()">取 消</el-button>
<el-button type="primary" @click="addMenuFormSubmit()">确 定</el-button>
</div>
</el-dialog>
5.4、点击新增修改按钮弹出新增修改菜单窗口
这里的逻辑
全部数据也就是顶级菜单(一级模块)不显示访问路径,组件路径。
二级模块修改的时候不显示访问路径,组件路径
新增的时候初始化表单数据,防止是点击修改在点击新增回显的是修改的数据。
新增的话,新增的菜单的pid为当前选中的行的id
修改的话,页面的值初始化为列表的具体的内容
//显示添加菜单
addOrEditMenuDialog(val, flag) {
if (val.level === 1) {
this.showFlag = false;
} else {
this.showFlag = true;
}
if ("add" === flag) {
//新增的话,页面的值初始化为定义好的常量
this.menuForm = { ...MENU_FORM };
//新增的话,新增的菜单的pid为当前选中的行的id
this.menuForm.pid = val.id;
this.title = "添加菜单";
} else {
this.title = "修改菜单";
//修改的话,页面的值初始化为列表的具体的内容
Object.assign(this.menuForm, val);
//修改
if (val.level === 2) {
this.showFlag = false;
}
}
this.dialogFormVisible = true;
},
5.5、点击新增修改菜单的确定按钮
//提交添加菜单的数据
addMenuFormSubmit() {
let that = this;
this.$refs.menuForm.validate((valid) => {
if (valid) {
if (that.menuForm.id) {
that.title = "修改菜单";
//修改
that.updateMenuSubmit();
} else {
that.title = "添加菜单";
//新增
that.addMenuSubmit();
}
} else {
return false;
}
});
},
//修改
updateMenuSubmit() {
let that = this;
updateMenuSubmit(that.menuForm).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "修改菜单成功",
});
//刷新页面
that.getAllMenuList();
that.dialogFormVisible = false;
}
});
},
//新增
addMenuSubmit() {
let that = this;
//let permission = that.menuForm;
http({
url: "/admin/sys/permission/save",
method: "post",
data: that.menuForm,
}).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "添加菜单成功",
});
//刷新页面
that.getAllMenuList();
that.dialogFormVisible = false;
}
});
},
5.6、PermissionController
/***********************************
* 用途说明:新增
* @param permission
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@PostMapping("save")
public R saveMenu(@RequestBody Permission permission) {
boolean result = permissionService.save(permission);
if (result) {
return R.ok();
}
return R.error();
}
/***********************************
* 用途说明:修改
* @param permission
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@PutMapping("update")
public R update(@RequestBody Permission permission) {
boolean result = permissionService.updateById(permission);
if (result) {
return R.ok();
}
return R.error();
}
6、添加修改功能
6.1、按钮
新增和修改有一点区别的,新增没有方法体,修改有具体的方法体。(我描述可能有误,或者每个人理解不一样,具体区别看下边的代码)
新增按钮一共4个要点
- 设置弹框falg标识为true,让其弹框显示
- 初始化新增功能的页面
- 设置功能的pid
- 设置添加修改功能的title标题
修改就是把当前行的数据传递到修改方法里
<el-button
v-if="scope.row.level === 3"
type="text"
size="mini"
@click="
() => {
dialogPermissionFormVisible = true;
permissionForm = { ...PERMISSION_FORM };
permissionForm.pid = scope.row.id;
permissionTitle = '添加功能';
}
"
>
添加功能</el-button
>
<el-button
v-if="scope.row.level === 4"
type="text"
size="mini"
@click="updatePermissionDialog(scope.row)"
>修改功能</el-button
>
6.2、vue
<!-- 添加功能的窗口 -->
<el-dialog
:title="permissionTitle"
:label-position="labelPosition"
:visible.sync="dialogPermissionFormVisible"
>
<el-form
ref="permissionForm"
:model="permissionForm"
:rules="permissionFormRules"
label-width="120px"
>
<el-form-item label="功能名称" prop="name">
<el-input v-model="permissionForm.name"></el-input>
</el-form-item>
<el-form-item label="访问路径" prop="path">
<el-input v-model="permissionForm.path"></el-input>
</el-form-item>
<el-form-item label="组件路径" prop="component">
<el-input v-model="permissionForm.component"></el-input>
</el-form-item>
<el-form-item label="功能权限值" prop="permissionValue">
<el-input v-model="permissionForm.permissionValue"></el-input>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input v-model="permissionForm.icon"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="resetPermission()">取 消</el-button>
<el-button type="primary" @click="addPermissonFormSubmit()"
>确 定</el-button
>
</div>
</el-dialog>
6.3、添加修改功能确定按钮提交事件
新增和修改我没有提取方法,直接写到一个方法里了
新增和修改用了两种方法调用后台,一个是先请求js,一个是直接请求后台。
新增和修改成功后需要刷新页面和初始化页面数据
//添加修改下级功能
addPermissonFormSubmit() {
let that = this;
this.$refs.permissionForm.validate((valid) => {
if (valid) {
if (that.permissionForm.id) {
//修改
updateMenuSubmit(that.permissionForm).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "修改功能成功",
});
//刷新页面
that.getAllMenuList();
that.dialogPermissionFormVisible = false;
that.permissionForm = { ...PERMISSION_FORM };
}
});
} else {
//新增
//let permission = that.menuForm;
http({
url: "/permission/save",
method: "post",
data: that.permissionForm,
}).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "添加功能成功",
});
//刷新页面
that.getAllMenuList();
that.dialogPermissionFormVisible = false;
that.permissionForm = { ...PERMISSION_FORM };
}
});
}
} else {
return false;
}
});
},
6.4、后台接口和添加菜单的是同一个接口这里不在描述
7、删除模块、菜单、功能
7.1、删除按钮
<el-button
v-if="scope.row.level !== 1"
@click="deleteSubmit(scope.row)"
type="text"
size="mini"
>删除</el-button
>
7.2、删除方法
//删除
deleteSubmit(val) {
let that = this;
this.$confirm("此操作将永久删除该记录,是否继续?", "提升", {
distinguishCancelAndClose: true,
confirmButtonText: "确定",
cancelButtonText: "取消",
})
.then(() => {
deleteSubmit(val.id).then((res) => {
if (res.code === 20000) {
that.$message({
type: "success",
message: "删除成功",
});
//刷新页面
that.getAllMenuList();
}
});
this.$message({
type: "info",
message: "保存修改",
});
})
.catch((action) => {
this.$message({
type: "info",
message:
action === "cancel" ? "放弃保存并离开页面" : "停留在当前页面",
});
});
// let that = this;
// this.$confirm("此操作将永久删除该记录,是否继续?", "提升", {
// cofirmButtonText: "确定",
// cancelButtonText: "取消",
// type: "warning",
// }).then(() => {
// deleteSubmit(val.id).then((res) => {
// if (res.code === 20000) {
// that.$message({
// type: "success",
// message: "删除成功",
// });
// //刷新页面
// that.getAllMenuList();
// }
// });
// });
},
7.3、PermissionController
/***********************************
* 用途说明:递归删除菜单
* @param id
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@DeleteMapping("delete/{id}")
public R delete(@PathVariable String id) {
boolean result = permissionService.removeChildById(id);
if (result) {
return R.ok();
}
return R.error();
}
7.4、PermissionServiceImpl
这里的删除需要递归删除自己和他的所有下级
/***********************************
* 用途说明:递归删除菜单
* @param id
* 返回值说明:
* @return boolean
***********************************/
@Override
public boolean removeChildById(String id) {
List<String> idList = new ArrayList<>();
selectChildListById(id, idList);
idList.add(id);
return baseMapper.deleteBatchIds(idList) > 0;
}
/***********************************
* 用途说明:根据当前菜单id查询他的子子孙孙id,封装到list集合
* @param id
* @param idList
* 返回值说明:
***********************************/
private void selectChildListById(String id, List<String> idList) {
//查询当前菜单的下级
QueryWrapper<Permission> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("pid", id);
queryWrapper.select("id");
List<Permission> childList = baseMapper.selectList(queryWrapper);
//把childIdList里面菜单id值获取出来,封装idList里面,做递归查询
childList.forEach(item -> {
idList.add(item.getId());
selectChildListById(item.getId(), idList);
});
}
十三、springSecurity
1、流程图
登录流程图,此图出大佬自江南一点雨
登录认证流程图,此图出自大佬吕一明
2、添加pom依赖(完整版的)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.9.2</swagger.version>
<mybatis-spring-boot-starter.version>2.3.1</mybatis-spring-boot-starter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<!-- <dependency>-->
<!-- <groupId>org.apache.velocity</groupId>-->
<!-- <artifactId>velocity-engine-core</artifactId>-->
<!-- <version>${velocity.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- spring boot redis缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lecttuce 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
3、使用的组件
- LogoutFilter - 登出过滤器
- logoutSuccessHandler - 登出成功之后的操作类
- UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器
- AuthenticationFailureHandler - 登录失败操作类
- AuthenticationSuccessHandler - 登录成功操作类
- BasicAuthenticationFilter - Basic身份认证过滤器
- SecurityContextHolder - 安全上下文静态工具类
- AuthenticationEntryPoint - 认证失败入口
- ExceptionTranslationFilter - 异常处理过滤器
- AccessDeniedHandler - 权限不足操作类
- FilterSecurityInterceptor - 权限判断拦截器、出口
4、引入Security与jwt
引入Security不必多说,前后端交互用户凭证用的是JWT,验证码的存储需要用到redis,以及用到一些工具类。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.9.2</swagger.version>
<mybatis-spring-boot-starter.version>2.3.1</mybatis-spring-boot-starter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<!-- <dependency>-->
<!-- <groupId>org.apache.velocity</groupId>-->
<!-- <artifactId>velocity-engine-core</artifactId>-->
<!-- <version>${velocity.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- spring boot redis缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lecttuce 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
启动redis,然后我们再启动项目,这时候我们再去访问http://localhost:9090/admin/sys/user/findAll,会发现系统会先判断到你未登录跳转到http://localhost:9090/login,因为security内置了登录页,用户名为user,密码在启动项目的时候打印在了控制台。登录完成之后我们才可以正常访问接口。 因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:
server:
port: 9090
spring:
profiles:
## 环境设置
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/2023Java?serverTimezone=Asia/Shanghai
username: root
password: study
security:
user:
name: admin
password: 111111
## 响应 json 的全局时间格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
##mybatis 日志设置
mybatis-plus:
configuration:
## 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
stu:
jwt:
header: Authorization
expire: 604800 #7天,秒单位
secret: ji8n3439n439n43ld9ne9343fdfer49h
5、验证码认证
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
- 首次登录认证:用户名、密码和验证码完成登录
- 二次token认证:请求头携带Jwt进行身份认证
使用用户名密码来登录的,然后我们还想添加图片验证码,security的所有过滤器都是没有图片验证码的,如果你想用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。 我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。
5.1、这在之前先写Redis的工具类和配置
package com.stu.myserver.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//================有序集合 sort set===================
/**
* 有序set添加元素
*
* @param key
* @param value
* @param score
* @return
*/
public boolean zSet(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
return redisTemplate.opsForZSet().add(key, typles);
}
public void zIncrementScore(String key, Object value, long delta) {
redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 获取zset数量
* @param key
* @param value
* @return
*/
public long getZsetScore(String key, Object value) {
Double score = redisTemplate.opsForZSet().score(key, value);
if(score==null){
return 0;
}else{
return score.longValue();
}
}
/**
* 获取有序集 key 中成员 member 的排名 。
* 其中有序集成员按 score 值递减 (从大到小) 排序。
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
}
配置类
package com.stu.myserver.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/******************************
* 用途说明: redis配置
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
public class RedisConfig {
@Bean
RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
5.2、生成验证码codeConfig 首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则:
package com.stu.myserver.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class codeConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "4");
properties.put("kaptcha.image.height", "40");
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
5.3、通过控制器提供生成验证码的方法captcha
package com.stu.myserver.controller;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson.JSONObject;
import com.google.code.kaptcha.Producer;
import com.stu.myserver.service.IndexService;
import com.stu.myserver.utils.Const;
import com.stu.myserver.utils.R;
import com.stu.myserver.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import cn.hutool.core.map.MapUtil;
/******************************
* 用途说明:
* 作者姓名: 小明的学习圈子
* 创建时间: 2022-09-01 21:42
******************************/
@RestController
@CrossOrigin
public class IndexController {
@Autowired
private IndexService indexService;
@Autowired
Producer producer;
@Autowired
RedisUtil redisUtil;
@GetMapping("/captcha")
public R captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
// 为了测试
//key = "aaaaa";
//code = "123456";
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);
return R.ok().data("data",MapUtil.builder()
.put("token", key)
.put("captchaImg", base64Img)
.build());
}
/***********************************
* 用途说明:退出
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@PostMapping("logout")
public R logout() {
return R.ok();
}
/***********************************
* 用途说明:
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("getUserInfo")
public R getUserInfo() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
Map<String, Object> userInfo = indexService.getUserInfo(userName);
List<JSONObject> permissionList = indexService.getMenu(userName);
return R.ok().data(userInfo).data("permissionList", permissionList);
}
/***********************************
* 用途说明:根据用户明获取动态菜单
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("menu")
public R menu() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
List<JSONObject> permissionList = indexService.getMenu(userName);
return R.ok().data("permissionList", permissionList);
}
}
因为前后端分离,我们禁用了session,所以我们把验证码放在了redis中,使用一个随机字符串作为key,并传送到前端,前端再把随机字符串和用户输入的验证码提交上来,这样我们就可以通过随机字符串获取到保存的验证码和用户的验证码进行比较了是否正确了。 然后因为图片验证码的方式,所以我们进行了encode,把图片进行了base64编码,这样前端就可以显示图片了。 5.4、验证码认证过滤器OncePerRequestFilter 图片验证码进行认证验证码是否正确。
- CaptchaFilter
package com.stu.myserver.security;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.stu.myserver.exception.CaptchaException;
import com.stu.myserver.utils.Const;
import com.stu.myserver.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 验证码认证过滤器
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {
try {
// 校验验证码
validate(httpServletRequest);
} catch (CaptchaException e) {
// 交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
// 校验验证码逻辑
private void validate(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String key = httpServletRequest.getParameter("token");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("验证码错误");
}
// 一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY, key);
}
}
5.5、SecurityConfig
package com.stu.myserver.config;
import com.stu.myserver.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/******************************
* 用途说明: Security配置
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] URL_WHITELIST = {
"/user/register",
"/js/**",
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/login**",
"/login#/login",
"/home/getData",
"captcha/getUserInfo"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() // 登录配置
.formLogin()
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated();
}
}
6、JWT身份认证
登录成功后,前端通过vuex和cookie保存jwt的信息,然后每次请求之前,都会携带这个jwt身份凭证。 6.1、Login.vue登录页面
<template>
<el-form
ref="form"
label-width="70px"
:inline="true"
class="login-container"
:model="form"
:rules="rules"
>
<h3 class="login_title">系统登录</h3>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入账号"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
type="password"
v-model="form.password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-input v-model="form.code" maxlength="6"></el-input>
</el-form-item>
<el-form-item>
<el-image
:src="captchaImg"
class="captchaImg"
style="margin-left: 15px"
@click="getCaptcha"
></el-image>
</el-form-item>
<el-form-item>
<el-button
@click="submit"
style="margin-left: 105px; margin-top: 10px"
type="primary"
:loading="loading"
>登录</el-button
>
<el-button
@click="submit1"
style="margin-left: 105px; margin-top: 10px"
type="primary"
:loading="loading"
>登录1</el-button
>
</el-form-item>
</el-form>
</template>
<script>
// import Mock from 'mockjs'
// import Cookie from 'js-cookie'
// import { getMenu } from '../api'
import qs from "qs";
import http from "@/utils/request";
export default {
data() {
return {
captchaImg: "",
loading: false,
form: {
username: "admin",
password: "111111",
token: "aaaaa",
code: "123456",
},
rules: {
username: [
{ required: true, trigger: "blur", message: "请输入用户名" },
],
password: [{ required: true, trigger: "blur", message: "请输入密码" }],
},
};
},
created() {
this.getCaptcha();
},
methods: {
submit1() {},
getCaptcha() {
http.get("/captcha").then((res) => {
if (res.code === 20000) {
console.log("/captcha");
console.log(res);
this.form.token = res.data.data.token;
this.captchaImg = res.data.data.captchaImg;
//this.form.code = "";
}
});
},
// 登录
submit() {
let that = this;
that.$refs.form.validate((valid) => {
if (valid) {
this.loading = true;
this.$store
.dispatch("Login", this.form)
.then((res) => {
if (res.code != 20000) {
that.$message({
type: "warning",
message: res.message,
});
this.loading = false;
return false;
}
this.loading = false;
this.$router.push({ path: "/home" });
})
.catch(() => {
this.loading = false;
});
// http
// .post("/login?" + qs.stringify(this.form))
// .then((res) => {
// if (res.code != 20000) {
// that.$message({
// type: "warning",
// message: res.message,
// });
// }
// console.log(res);
// const jwt = res.data.authorization;
// this.$store.commit("SET_TOKEN", jwt);
// //
// this.$router.push("/home");
// })
// .catch((e) => {});
//this.loading = true;
// that.$store
// .dispatch("Login", that.form)
// .then(() => {
// //this.loading = false;
// that.$router.push({ path: "/" });
// })
// .catch(() => {
// //this.loading = false;
// });
} else {
console.log("error submit!!");
return false;
}
});
// // token信息
// const token = Mock.Random.guid()
// 校验通过
// 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)
// // 获取菜单的数据,存入store中
// this.$store.commit('setMenu', data.data.menu)
// this.$store.commit('addMenu', this.$router)
// // 跳转到首页
// this.$router.push('/home')
// } else {
// this.$message.error(data.data.message);
// }
// })
// }
// })
},
},
};
</script>
<style lang="less" scoped>
.login-container {
width: 650px;
border: 1px solid #eaeaea;
margin: 180px auto;
padding: 35px 35px 15px 35px;
background-color: #fff;
border-radius: 15px;
box-shadow: 0 0 25px #cac6c6;
box-sizing: border-box;
.login_title {
text-align: center;
margin-bottom: 40px;
color: #505458;
}
.el-input {
width: 198px;
}
}
</style>
6.2、vuex的user.js的Login的actions
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import http from "@/utils/request";
import qs from "qs";
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
buttons: [],
roles: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_BUTTONS: (state, buttons) => {
state.buttons = buttons
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
//const username = userInfo.username.trim()
let that = this
return new Promise((resolve, reject) => {
// http.post(username, userInfo.password).then(response => {
// //
// const jwt = response.data.authorization;
// setToken(jwt)
// commit('SET_TOKEN', jwt)
// resolve()
// }).catch(error => {
// reject(error)
// })
http
.post("/login?" + qs.stringify(userInfo))
.then((res) => {
if (res.code != 20000) {
resolve(res)
} else {
const jwt = res.data.authorization;
//localStorage.setItem("token", jwt)
setToken(jwt)
getToken();
commit('SET_TOKEN', jwt)
resolve(res)
//this.$router.push("/home");
}
})
.catch((e) => {
});
})
},
// Login({ commit }) {
//
// const data = {
// 'token': 'helen'
// }
// setToken(data.token)// 将token存储在cookie中
// commit('SET_TOKEN', data.token)
// },
// 获取用户信息
async GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const data = response.data
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data.roles)
} else {
reject('getInfo: roles must be a non-null array !')
}
const buttonAuthList = []
console.log(data)
data.permissionValueList.forEach(button => {
if (button) {
buttonAuthList.push(button)
}
})
commit('SET_NAME', data.name)
//commit('SET_AVATAR', data.avatar)
commit('SET_BUTTONS', buttonAuthList)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// GetInfo({ commit }) {
//
// const data = {
// 'roles': [
// 'admin'
// ],
// 'name': 'helen',
// 'avatar': 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-5670helen3b4acafe.gif'
// }
// if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
// commit('SET_ROLES', data.roles)
// }
// commit('SET_NAME', data.name)
// commit('SET_AVATAR', data.avatar)
// },
// 登出
// LogOut({ commit, state }) {
// return new Promise((resolve, reject) => {
// logout(state.token).then(() => {
// commit('SET_TOKEN', '')
// commit('SET_ROLES', [])
// removeToken()
// resolve()
// }).catch(error => {
// reject(error)
// })
// })
// },
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
// logout(state.token).then(() => {
http.post("/logout").then(res => {
commit('SET_TOKEN', '')// 清空前端vuex中存储的数据
commit('SET_ROLES', [])// 清空前端vuex中存储的数据
commit('SET_BUTTONS', [])
removeToken()// 清空cookie
resolve(res)
})
// }).catch(error => {
// reject(error)
// })
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
6.3、用js-cookie存放token的auth.js文件
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
6.4、封装的request请求
import axios from "axios";
import store from '../store'
import { getToken } from '@/utils/auth'
const http = axios.create({
baseURL: 'http://localhost:9090/',//通用请求的地址 '/api',//
timeout: 100000,//超时时间,10000毫秒,10秒
});
// 添加请求拦截器
http.interceptors.request.use(function (config) {
console.log("store.getters.token " + store.getters.token)
console.log("getToken() " + getToken())
// 在发送请求之前做些什么
if (store.getters.token) {
config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
export default http
后端进行用户身份认证的时候,通过请求头中获取jwt,然后解析用户名,然后判断用户是否有权限等操作。 自定义一个JwtAuthenticationFilter过滤器用来进行识别jwt。 6.5、JwtAuthenticationFilter后端验证
package com.stu.myserver.security;
import cn.hutool.core.util.StrUtil;
import com.stu.myserver.entity.User;
import com.stu.myserver.service.IUserService;
import com.stu.myserver.service.IndexService;
import com.stu.myserver.utils.JwtUtils;
import com.stu.myserver.utils.R;
import com.stu.myserver.utils.ResponseUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.lang.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/******************************
* 用途说明: Basic身份认证过滤器
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
IUserService userService;
@Autowired
JwtUtils tokenManager;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
UserDetailsService userDetailService;
@Autowired
private IndexService indexService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager,
JwtUtils tokenManager,
RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// if (request.getRequestURI().indexOf("/home/getData") >= 0 ) {
// chain.doFilter(request, response);
// return;
// }
String jwt = request.getHeader(jwtUtils.getHeader());
System.out.println("jwt============================" + jwt);
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
User sysUser = userService.selectByUserName(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = null;
try {
//usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));
usernamePasswordAuthenticationToken = getAuthentication(request);
} catch (Exception e) {
ResponseUtil.out(response, R.error());
}
if (usernamePasswordAuthenticationToken != null) {
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
ResponseUtil.out(response, R.error());
}
chain.doFilter(request, response);
}
/***********************************
* 用途说明:从request获取token,根据token获取权限列表
* 返回值说明:
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
***********************************/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(jwtUtils.getHeader());
if (token != null && !"".equals(token.trim())) {
Claims claims = jwtUtils.getClaimByToken(token);
String userName = claims.getSubject();
if (null == redisTemplate.opsForValue().get(userName)) {
indexService.getUserInfo(userName);
}
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (Collections.isEmpty(permissionValueList)) {
return null;
}
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permissionValue);
authorities.add(simpleGrantedAuthority);
}
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
}
上面的代码,获取到用户名之后封装UsernamePasswordAuthenticationToken,之后交给SecurityContextHolder参数传递authentication对象,这样后续security就能获取到当前登录的用户信息了,也就完成了用户认证。 然后配置到SecurityConfig里,这是完整的配置。
package com.stu.myserver.config;
import com.stu.myserver.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/******************************
* 用途说明: Security配置
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// @Bean
// BCryptPasswordEncoder bCryptPasswordEncoder() {
// return new BCryptPasswordEncoder();
// }
private static final String[] URL_WHITELIST = {
"/user/register",
"/js/**",
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/login**",
"/login#/login",
"/home/getData",
"captcha/getUserInfo"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() // 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
当认证失败的时候会进入AuthenticationEntryPoint,所以自定义认证失败返回的数据: 6.6、自定义认证失败返回的数据的JwtAuthenticationEntryPoint类
package com.stu.myserver.security;
import cn.hutool.json.JSONUtil;
import com.stu.myserver.utils.R;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 认证失败入口,全局异常捕获前捕获异常
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
R result = R.error().message("请先登录");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
如果认证失败,就提示【请先登录】。 然后我们把认证过滤器和认证失败入口配置到SecurityConfig中 6.7、自定义权限不足的JwtAccessDeniedHandler类 这个也需要配置到SecurityConfig中
package com.stu.myserver.security;
import cn.hutool.json.JSONUtil;
import com.stu.myserver.utils.R;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 权限不足操作类,全局异常之后捕获
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
R result = R.error().message(accessDeniedException.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
7、Security用户密码身份认证
上边的用户名密码是写在在配置文件中的,而且密码用的是明文,这明显不符合我们的要求,我们的用户必须是存储在数据库中,密码也是得经过加密的。所以我们先来解决这个问题,然后再去弄授权。 自定义密码解析器PasswordEncoder,这里用到MD5对密码进行加密。
package com.stu.myserver.security;
import com.stu.myserver.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/******************************
* 用途说明: 默认密码编码器
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
/**
* 加密密码
*
* @param rawPassword
*/
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
/**
* 判断密码是否正确
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
8、权限授权
权限授权security的重要功能,当用户认证成功之后,我们就知道谁在访问系统接口,但是要知道用户有没有权限来访问我们这个接口,我们需要知道用户有哪些权限,哪些角色,这样security才能我们做权限判断。 权限授权需要定义五张表,分别是用户,角色,用户角色关联,角色权限关联,权限(菜单)这5个表。 1、左侧菜单的显示与否是根据用户查询菜单并封装返回给前端。通过IndexServiceImpl的getMenu方法实现。 2、一般当权限粒度比较细的时候,我们都通过判断用户有没有此菜单或操作的权限,而不是通过角色判断,而用户和菜单是不直接做关联的,是通过用户拥有哪些角色,然后角色拥有哪些菜单权限这样来获得的。这个通过UserDetailsServiceImpl里调用selectPermissionValueListByUserId方法实现,这里会用Redis存储权限。
8.1、sql查询权限 获取权限的sql,这里分两个sql,一个是超级管理员具有所有权限,另一个是根据用户的id跟角色和权限管理查询出该用户的所具有的权限。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stu.myserver.mapper.PermissionMapper">
<resultMap id="permissionMap" type="com.stu.myserver.entity.Permission">
<result property="id" column="id"/>
<result property="pid" column="pid"/>
<result property="name" column="name"/>
<result property="type" column="type"/>
<result property="permissionValue" column="permission_value"/>
<result property="path" column="path"/>
<result property="component" column="component"/>
<result property="icon" column="icon"/>
<result property="status" column="status"/>
<result property="isDeleted" column="is_deleted"/>
<result property="gmtCreate" column="gmt_create"/>
<result property="gmtModified" column="gmt_modified"/>
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
p.id,p.pid,p.name,p.type,p.permission_value,path,p.component,p.icon,p.status,p.is_deleted,p.gmt_create,p.gmt_modified
</sql>
<select id="selectPermissionByUserId" resultMap="permissionMap">
select
<include refid="columns" />
from acl_user_role ur
inner join acl_role_permission rp on rp.role_id = ur.role_id
inner join acl_permission p on p.id = rp.permission_id
where ur.user_id = #{userId}
and ur.is_deleted = 0
and rp.is_deleted = 0
and p.is_deleted = 0
</select>
<select id="selectPermissionValueByUserId" resultType="String">
select
p.permission_value
from acl_user_role ur
inner join acl_role_permission rp on rp.role_id = ur.role_id
inner join acl_permission p on p.id = rp.permission_id
where ur.user_id = #{userId}
and ur.is_deleted = 0
and rp.is_deleted = 0
and p.is_deleted = 0
</select>
<select id="selectAllPermissionValue" resultType="String">
select
permission_value
from acl_permission
where is_deleted = 0
</select>
</mapper>
8.2、赋予用户权限的地方 1、用户登录,调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。 SecurityUser类需要实现UserDetails。(UserDetails是springsecurity自带的)
package com.stu.myserver.security;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/******************************
* 用途说明: SecurityUser
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前登录用户权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
/***********************************
* 用途说明:获取当前用户的所有权限
* 返回值说明:
* @return java.util.Collection<? extends org.springframework.security.core.GrantedAuthority>
***********************************/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue:permissionValueList){
if(StringUtils.isEmpty(permissionValue)){
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsServiceImpl类需要实现UserDetailsService。(UserDetailsService是springsecurity自带的)
package com.stu.myserver.security;
import com.stu.myserver.exception.CustomException;
//import com.stu.myserver.security.entity.SecurityUser;
import com.stu.myserver.service.IPermissionService;
import com.stu.myserver.service.IUserService;
import com.stu.myserver.utils.ResultCodeEnum;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/******************************
* 用途说明: 自定义UserDetailsService实现类,认证用户详情
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Autowired
private IPermissionService permissionService;
/**
* 根据用户名查询用户信息
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.stu.myserver.entity.User user = userService.selectByUserName(username);
if (user == null) {
throw new CustomException(ResultCodeEnum.LOGIN_MOBILE_ERROR);
}
// 返回UserDetails实现类
com.stu.myserver.security.User curUser = new User();
BeanUtils.copyProperties(user, curUser);
//根据用户id查询有权限的菜单
List<String> authorities = permissionService.selectPermissionValueListByUserId(user.getId());
SecurityUser securityUser = new SecurityUser(curUser);
securityUser.setPermissionValueList(authorities);
return securityUser;
}
}
2、接口调用进行身份认证过滤器JWTAuthenticationFilter的时候,需要返回用户权限信息
package com.stu.myserver.security;
import cn.hutool.core.util.StrUtil;
import com.stu.myserver.entity.User;
import com.stu.myserver.service.IUserService;
import com.stu.myserver.service.IndexService;
import com.stu.myserver.utils.JwtUtils;
import com.stu.myserver.utils.R;
import com.stu.myserver.utils.ResponseUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.lang.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/******************************
* 用途说明: Basic身份认证过滤器
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
IUserService userService;
@Autowired
JwtUtils tokenManager;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
UserDetailsService userDetailService;
@Autowired
private IndexService indexService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager,
JwtUtils tokenManager,
RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// if (request.getRequestURI().indexOf("/home/getData") >= 0 ) {
// chain.doFilter(request, response);
// return;
// }
String jwt = request.getHeader(jwtUtils.getHeader());
System.out.println("jwt============================" + jwt);
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
User sysUser = userService.selectByUserName(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = null;
try {
// usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));
usernamePasswordAuthenticationToken = getAuthentication(request);
} catch (Exception e) {
ResponseUtil.out(response, R.error());
}
if (usernamePasswordAuthenticationToken != null) {
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
ResponseUtil.out(response, R.error());
}
chain.doFilter(request, response);
}
/***********************************
* 用途说明:从request获取token,根据token获取权限列表
* 返回值说明:
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
***********************************/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(jwtUtils.getHeader());
if (token != null && !"".equals(token.trim())) {
Claims claims = jwtUtils.getClaimByToken(token);
String userName = claims.getSubject();
if (null == redisTemplate.opsForValue().get(userName)) {
indexService.getUserInfo(userName);
}
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (Collections.isEmpty(permissionValueList)) {
return null;
}
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permissionValue);
authorities.add(simpleGrantedAuthority);
}
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
}
3、indexService的获取权限的接口
package com.stu.myserver.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.stu.myserver.entity.Role;
import com.stu.myserver.entity.User;
import com.stu.myserver.exception.CustomException;
import com.stu.myserver.service.IPermissionService;
import com.stu.myserver.service.IRoleService;
import com.stu.myserver.service.IUserService;
import com.stu.myserver.service.IndexService;
import com.stu.myserver.utils.ResultCodeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/******************************
* 用途说明:
* 作者姓名: 小明的学习圈子
* 创建时间: 2022-09-01 22:34
******************************/
@Service
public class IndexServiceImpl implements IndexService {
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
@Autowired
private IPermissionService permissionService;
@Autowired
private RedisTemplate redisTemplate;
/***********************************
* 用途说明:根据用户明获取用户登录信息
* @param userName
* 返回值说明:
* @return java.util.Map<java.lang.String, java.lang.Object>
***********************************/
@Override
public Map<String, Object> getUserInfo(String userName) {
Map<String, Object> result = new HashMap<>();
User user = userService.selectByUserName(userName);
if (user == null) {
throw new CustomException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//根据用户id获取角色
List<Role> roleList = roleService.selectRoleByUserId(user.getId());
//转换成角色名称列表
List<String> roleNameList = roleList.stream()
.map(item -> item.getRoleName()).collect(Collectors.toList());
//前端框架必须返回一个角色,否则报错,如果没有角色,返回一个空角色
if (roleNameList.size() == 0) {
roleNameList.add("");
}
List<String> permissionValueList = permissionService.selectPermissionValueListByUserId(user.getId());
redisTemplate.opsForValue().set(userName, permissionValueList);
List<String> permissionValueLisst = (List<String>) redisTemplate.opsForValue().get(userName);
result.put("name", user.getUsername());
result.put("roles", roleNameList);
result.put("permissionValueList", permissionValueList);
return result;
}
/***********************************
* 用途说明:根据用户动态获取菜单
* @param userName
* 返回值说明:
* @return java.util.List<org.json.JSONObject>
***********************************/
@Override
public List<JSONObject> getMenu(String userName) {
User user = userService.selectByUserName(userName);
if (user == null) {
throw new CustomException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//根据用户动态获取菜单
return permissionService.selectPermissionByUserId(user.getId());
}
}
8.3、Security内置的权限注解: 授权、验证权限的流程:
- 用户登录或者调用接口时候识别到用户,并获取到用户的权限信息
- 注解标识Controller中的方法需要的权限或角色
- Security通过FilterSecurityInterceptor匹配URI和权限是否匹配
- 有权限则可以访问接口,当无权限的时候返回异常交给AccessDeniedHandler操作类处理
@PreAuthorize:方法执行前进行权限检查 @PostAuthorize:方法执行后进行权限检查 @Secured:类似于 @PreAuthorize 可以在Controller的方法前添加这些注解表示接口需要什么权限。 比如需要Admin角色权限:
@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限
@PreAuthorize("hasAuthority('user.add')")
9、退出LogoutSuccessHandler
1、调用退出的handler 2、清空Redis缓存的用户权限信息 3、清空token
package com.stu.myserver.security;
import cn.hutool.json.JSONUtil;
import com.stu.myserver.utils.JwtUtils;
import com.stu.myserver.utils.R;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 登出成功处理
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtils jwtUtils;
private JwtUtils tokenManager;
private RedisTemplate redisTemplate;
public JwtLogoutSuccessHandler(JwtUtils tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
String token = request.getHeader(jwtUtils.getHeader());
Claims claim = jwtUtils.getClaimByToken(token);
if (claim != null && !jwtUtils.isTokenExpired(claim)) {
String userName = claim.getSubject();
redisTemplate.delete(userName);
}
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader(jwtUtils.getHeader(), "");
R result = R.ok();
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
4、前端退出的操作 vuex的user.js
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
// logout(state.token).then(() => {
http.post("/logout").then(res => {
commit('SET_TOKEN', '')// 清空前端vuex中存储的数据
commit('SET_ROLES', [])// 清空前端vuex中存储的数据
commit('SET_BUTTONS', [])
removeToken()// 清空cookie
resolve(res)
})
// }).catch(error => {
// reject(error)
// })
})
},
10、上边的组件需要配置到SecurityConfig
package com.stu.myserver.config;
import com.stu.myserver.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/******************************
* 用途说明: Security配置
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// @Bean
// BCryptPasswordEncoder bCryptPasswordEncoder() {
// return new BCryptPasswordEncoder();
// }
private static final String[] URL_WHITELIST = {
"/user/register",
"/js/**",
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/login**",
"/login#/login",
"/home/getData",
"captcha/getUserInfo"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() // 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
十四、动态菜单和路由
1、路由index.js
路由和菜单,路由就是点击左侧菜单要跳转的路由地址,{ path: 'home', name: "home", component: Home }这种格式,之前是固定的现在需要动态拼接的成这样,菜单就是左侧能看到的菜单
定义拼接动态路由的常量manageRoute
setRoutes主要处理动态的路由和菜单
通过localStorage.getItem("menus")获取后台封装的菜单路由数据,localStorage.setItem("menus", JSON.stringify(menuData))是在permission.js里设置的
通过manageRoute.children.push(itemMenu)把路由加到动态路由的常量的children里
动态添加到现在的路由对象中去 router.addRoute(manageRoute)
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/sys/user/User.vue'
import Main from '@/views/Main.vue'
import store from '@/store'
import { getToken } from '@/utils/auth'
import { login, logout, getInfo } from '@/api/login'
Vue.use(VueRouter)
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
//主路由
// {
// path: '/',
// component: Main,
// redirect: '/home',
// children: [
// //子路由
// { path: 'home', name: "home", component: Home },//首页
// // { path: 'user', name: "user", component: User },//用户管理
// // { path: 'role', name: "role", component: () => import("@/views/sys/Role.vue") },//角色管理
// // { path: 'menu', name: "menu", component: () => import("@/views/sys/Menu.vue") },//菜单管理管理
// ]
// },
// {
// path: '/user',
// name: 'User',
// component: User
// },
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
mode: 'history', // 去掉url中的#
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。挂载到main.js的根节点
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
// const app = new Vue({
// router
// }).$mount('#app')
// 防止连续点击多次路由报错
// 获取原型对象push函数
const originalPush = VueRouter.prototype.push
// 获取原型对象replace函数
const originalReplace = VueRouter.prototype.replace
// 修改原型对象中的push函数
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 修改原型对象中的replace函数
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
//模拟菜单数据
//const authoritys = ['sys:user:list', "sys:user:save", "sys:user:delete"]
const menuData1 = [
// {
// path: "/",
// name: "home",
// label: "首页",
// icon: "s-home",
// url: "Home/Home",
// },
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
component: "sys/user/User",
},
{
path: '/sys/role/distribution/:id',
name: '角色权限',
component: 'sys/role/roleForm',
meta: { title: '角色权限', icon: 'table' },
hidden: true
},
{
path: '/userForm/:id',
name: '角色权限',
component: 'sys/user/userForm',
meta: { title: '角色权限', icon: 'table' },
hidden: true
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
component: "sys/role/Role",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
component: "sys/menu/Menu",
},
],
},
]
// 拼装动态路由
const manageRoute = {
path: '/',
component: Main,
redirect: '/login',
children: [
//子路由
{ path: 'home', name: "home", component: Home },//首页
]
}
//获取数据
//菜单
// store.commit('setMenuList', menuData1)
//权限用户
//localStorage.setItem("menus1", JSON.stringify(menuData1))
// 注意:刷新页面会导致页面路由重置
const setRoutes = () => {
//const storeMenus = localStorage.getItem("menus");
const storeMenus = localStorage.getItem("menus");
if (storeMenus) {
// 获取当前的路由对象名称数组
const currentRouteNames = router.getRoutes().map(v => v.name)
if (!currentRouteNames.includes('home')) {
const menus = JSON.parse(storeMenus)
menus.forEach(item => {
if (item.path) { // 当且仅当path不为空的时候才去设置路由
let flag = false;
if (item.hidden) {
flag = true;
}
let itemMenu = {
path: item.path,
name: item.name,
hidden: flag,
component: () => import('../views/' + item.component + '.vue')
}
manageRoute.children.push(itemMenu)
} else if (item.children.length) {
item.children.forEach(item => {
let flag = false;
if (item.hidden) {
flag = true;
}
if (item.path) {
let itemMenu = {
path: item.path,
name: item.name,
hidden: flag,
component: () => import('../views/' + item.component + '.vue')
}
manageRoute.children.push(itemMenu)
}
})
}
})
// 动态添加到现在的路由对象中去
router.addRoute(manageRoute)
}
}
}
// 重置我就再set一次路由
setRoutes()
//转成路由
// const menuToRoute = (item) => {
// if (!item.component) {
// return null
// }
// // let route = {
// // name: item.name,
// // path: item.path.replace("/", ""),
// // meta: {
// // icon: item.icon,
// // title: item.title,
// // }
// // }
// // route.component= () => import('../views/' + item.component + '.vue')
// // route.component=()=>import('@/views/'+item.component+'.vue');
// // route.component = () => import('@/views/' + item.component + '.vue')
// let route = { path: item.path.replace("/", ""), name: item.name, component: () => import('../views/' + item.component + '.vue') }
// return route
// }
// router.beforeEach((to, from, next) => {
// let that = this
// getInfo(store.state.token).then(response => {
// const data = response.data
// if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
// store.commit('SET_ROLES', data.roles)
// }
// const buttonAuthList = []
// menuData = data.permissionList
//
// data.permissionValueList.forEach(button => {
// if (button) {
// buttonAuthList.push(button)
// }
// })
// store.commit('SET_NAME', data.name)
// //commit('SET_AVATAR', data.avatar)
// store.commit('SET_BUTTONS', buttonAuthList)
// }).catch(error => {
// })
// next()
// })
export default router
2、左侧菜单
菜单数据来着vuex的menuList
通过v-if="!item.hidden"和v-if="!menu.hidden"控制功能,因为功能是按钮所以不显示到页面的左侧菜单
左侧菜单的首页是页面固定的,其余菜单是后台获取
<template>
<div>
<!-- <el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>{{ isCollapse ? "后台" : "通用后台管理系统" }}</h3>
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.path"
@click="clickMenu(item)"
>
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<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-group
v-for="subItem in item.children"
:key="subItem.path"
>
<el-menu-item :index="subItem.path" @click="clickMenu(subItem)">{{
subItem.label
}}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu> -->
<el-menu
default-active="1-4-1"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<router-link to="/home">
<el-menu-item
index="Inde123x"
@click="clickMenu({ name: 'home', title: '首页', path: '/home' })"
>
<template slot="title">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</template>
</el-menu-item>
</router-link>
<el-submenu :index="menu.name" v-for="menu in menuData" :key="menu.name" v-if="!menu.hidden">
<template slot="title">
<i :class="`el-icon-${menu.icon}`"></i>
<span>{{ menu.label }}</span>
</template>
<router-link
:to="item.path"
v-for="item in menu.children"
:key="item.name"
v-if="!item.hidden"
>
<el-menu-item :index="item.name" @click="clickMenu(item)">
<template slot="title">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</template>
</el-menu-item>
</router-link>
</el-submenu>
</el-menu>
</div>
</template>
<style lang="less" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-menu {
//height: calc(100vh - 0px);
border-right: 0px;
height: 100vh;
h3 {
color: #fff;
text-align: center;
line-height: 48px;
font-size: 16px;
font-weight: 400px;
}
}
</style>
<script>
export default {
data() {
return {
//isCollapse: false,
menuData11: [
// {
// path: "/",
// name: "home",
// label: "首页",
// icon: "s-home",
// url: "Home/Home",
// },
{
label: "权限管理",
name: "system",
icon: "location",
children: [
{
path: "/user",
name: "user",
label: "用户管理",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/role",
name: "role",
label: "角色管理",
icon: "setting",
url: "Other/PageTwo",
},
{
path: "/menu",
name: "menu",
label: "菜单管理",
icon: "setting",
url: "Other/PageTwo",
},
],
},
],
};
},
created() {
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
clickMenu(item) {
//当页面的路由和跳转的路由不一致才允许跳转
//this.$route.path 上一个路径,item.path下一个路径
if (
this.$route.path !== item.path &&
!(this.$route.path === "/home" && item.path === "/")
) {
this.$router.push(item.path);
}
this.$store.commit("breadcrumbChange", item);
},
},
computed: {
// //没有子菜单
// noChildren() {
// return this.menuData.filter((item) => !item.children);
// },
// //有子菜单
// hasChildren() {
// return this.menuData.filter((item) => item.children);
// },
//vuex取得菜单展开收起的值,但是data里不能重复定义
isCollapse() {
return this.$store.state.menu.isCollapse;
},
// menuData11: {
// get() {
// return this.$store.state.menu.menuList;
// },
// },
menuData() {
return this.$store.state.menu.menuList;
},
},
};
</script>
3、menu.js
export default {
state: {
menuList: [],//动态路由
//authoritysList: [],//权限
isCollapse: false, //用于控制菜单的展开收起
breadcrumbList: [
//默认的数据
{
path: "/home",
name: "home",
label: "首页",
icon: "s-home",
url: "Home/Home",
}
]//面包屑的数据
},
mutations: {
setMenuList(state, val) {
state.menuList = val
},
// setAuthoritysList(state, val) {
// state.authoritysList = val
// },
//删除指定的tag
closeTag(state, item) {
const index = state.breadcrumbList.findIndex(val => { return val.name === item.name })
state.breadcrumbList.splice(index, 1)
},
//用于控制菜单的展开收起
handleCollapse(state) {
state.isCollapse = !state.isCollapse
},
//更新面包屑数据
breadcrumbChange(state, val) {
//判断添加的数据是否是首页
if (val.name !== 'home') {
const index = state.breadcrumbList.findIndex(item => item.name === val.name)
//如果不存在就添加道面包屑数组里
if (index === -1) {
state.breadcrumbList.push(val)
}
}
}
}
}
4、store下的user.js
通过GetInfo方法从后台获取动态菜单和路由的树形结构数据
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import http from "@/utils/request";
import qs from "qs";
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
buttons: [],
roles: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_BUTTONS: (state, buttons) => {
state.buttons = buttons
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
//const username = userInfo.username.trim()
let that = this
return new Promise((resolve, reject) => {
// http.post(username, userInfo.password).then(response => {
// //
// const jwt = response.data.authorization;
// setToken(jwt)
// commit('SET_TOKEN', jwt)
// resolve()
// }).catch(error => {
// reject(error)
// })
http
.post("/login?" + qs.stringify(userInfo))
.then((res) => {
if (res.code != 20000) {
resolve(res)
} else {
const jwt = res.data.authorization;
//localStorage.setItem("token", jwt)
setToken(jwt)
getToken();
commit('SET_TOKEN', jwt)
resolve(res)
//this.$router.push("/home");
}
})
.catch((e) => {
});
})
},
// Login({ commit }) {
//
// const data = {
// 'token': 'helen'
// }
// setToken(data.token)// 将token存储在cookie中
// commit('SET_TOKEN', data.token)
// },
// 获取用户信息
async GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const data = response.data
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data.roles)
} else {
reject('getInfo: roles must be a non-null array !')
}
const buttonAuthList = []
console.log(data)
data.permissionValueList.forEach(button => {
if (button) {
buttonAuthList.push(button)
}
})
commit('SET_NAME', data.name)
//commit('SET_AVATAR', data.avatar)
commit('SET_BUTTONS', buttonAuthList)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// GetInfo({ commit }) {
//
// const data = {
// 'roles': [
// 'admin'
// ],
// 'name': 'helen',
// 'avatar': 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-5670helen3b4acafe.gif'
// }
// if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
// commit('SET_ROLES', data.roles)
// }
// commit('SET_NAME', data.name)
// commit('SET_AVATAR', data.avatar)
// },
// 登出
// LogOut({ commit, state }) {
// return new Promise((resolve, reject) => {
// logout(state.token).then(() => {
// commit('SET_TOKEN', '')
// commit('SET_ROLES', [])
// removeToken()
// resolve()
// }).catch(error => {
// reject(error)
// })
// })
// },
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
// logout(state.token).then(() => {
http.post("/logout").then(res => {
commit('SET_TOKEN', '')// 清空前端vuex中存储的数据
commit('SET_ROLES', [])// 清空前端vuex中存储的数据
commit('SET_BUTTONS', [])
removeToken()// 清空cookie
resolve(res)
})
// }).catch(error => {
// reject(error)
// })
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
5、permission.js
- 通过store.commit('setMenuList', menuData)调用上边的GetInfo返回需要的数据
import router from './router'
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import http from "@/utils/request";
import qs from "qs";
import store from './store'
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
router.beforeEach(async (to, from, next) => {
const hasToken = getToken()
debugger
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next()
} else {
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
// get user info
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const response = await store.dispatch('GetInfo')
if (response.code === 20000) {
const data = response.data
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
store.commit('SET_ROLES', data.roles)
}
const buttonAuthList = []
const menuData = data.permissionList
store.commit('setMenuList', menuData)
//权限用户
localStorage.setItem("menus", JSON.stringify(menuData))
data.permissionValueList.forEach(button => {
if (button) {
buttonAuthList.push(button)
}
})
store.commit('SET_NAME', data.name)
//commit('SET_AVATAR', data.avatar)
store.commit('SET_BUTTONS', buttonAuthList)
}
next()
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next({ path: '/' })
}
}
})
6、IndexController
调用getUserInfo方法返回前端需要是数据
indexService.getMenu(userName)这个方法是封装菜单路由的主要数据
package com.stu.myserver.controller;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson.JSONObject;
import com.google.code.kaptcha.Producer;
import com.stu.myserver.service.IndexService;
import com.stu.myserver.utils.Const;
import com.stu.myserver.utils.R;
import com.stu.myserver.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import cn.hutool.core.map.MapUtil;
/******************************
* 用途说明:
* 作者姓名: 公众号:小明的学习圈子 https://stu.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@RestController
@CrossOrigin
public class IndexController {
@Autowired
private IndexService indexService;
@Autowired
Producer producer;
@Autowired
RedisUtil redisUtil;
@GetMapping("/captcha")
public R captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
// 为了测试
key = "aaaaa";
code = "123456";
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);
return R.ok().data("data",MapUtil.builder()
.put("token", key)
.put("captchaImg", base64Img)
.build());
}
/***********************************
* 用途说明:退出
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@PostMapping("logout")
public R logout() {
return R.ok();
}
/***********************************
* 用途说明:
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("getUserInfo")
public R getUserInfo() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
Map<String, Object> userInfo = indexService.getUserInfo(userName);
List<JSONObject> permissionList = indexService.getMenu(userName);
return R.ok().data(userInfo).data("permissionList", permissionList);
}
/***********************************
* 用途说明:根据用户明获取动态菜单
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("menu")
public R menu() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
List<JSONObject> permissionList = indexService.getMenu(userName);
return R.ok().data("permissionList", permissionList);
}
}
7、IndexServiceImpl
- 先根据登录用户获取到用户信息(这里的用户是唯一的,注意登录用户和名称是有区别的,登录用户应该是唯一的)
- 然后根据获取的用户信息的id封装动态菜单路由
@Override
public List<JSONObject> getMenu(String userName) {
User user = userService.selectByUserName(userName);
if (user == null) {
throw new CustomException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//根据用户动态获取菜单
return permissionService.selectPermissionByUserId(user.getId());
}
8、PermissionServiceImpl
- 根据用户id获取用户的所有权限
- 转成树形结构
- 转换成前端需要的格式
- 这里一共封装3级模块,菜单,功能。数据结构看 9树形结构的菜单数据
/***********************************
* 用途说明:根据用户id查询所有权限的菜单详细列表
* @param userId
* 返回值说明:
* @return java.util.List<org.json.JSONObject>
***********************************/
@Override
public List<JSONObject> selectPermissionByUserId(String userId) {
List<Permission> selectPermissionList = null;
if (checkAdmin(userId)) {
//如果是超级管理员获取所有权限
selectPermissionList = baseMapper.selectList(null);
} else {
//根据用户id查询所有权限
selectPermissionList = baseMapper.selectPermissionByUserId(userId);
}
//先转换成树状
List<Permission> permissionList = bulidPermission(selectPermissionList);
//然后转化成前端需要的格式
List<JSONObject> result = bulidJson(permissionList);
return result;
}
/***********************************
* 用途说明:把返回所有菜单list集合进行封装的方法
* @param list
* 返回值说明:
* @return java.util.List<com.stu.service.acl.entity.Permission>
***********************************/
private List<Permission> bulidPermission(List<Permission> list) {
//创建list集合,用于数据最终封装
List<Permission> finalNode = new ArrayList<>();
//把所有菜单list集合遍历,得到顶层菜单 pid=0菜单,设置level是1
for (Permission permission : list) {
//得到顶层菜单 pid=0菜单
if ("0".equals(permission.getPid())) {
permission.setLevel(1);
//根据顶层菜单,向里面进行查询子菜单,封装到finalNode里面
finalNode.add(selectChildren(permission, list));
}
}
return finalNode;
}
/***********************************
* 用途说明:递归查询下级菜单
* @param permission
* @param list
* 返回值说明:
* @return com.stu.service.acl.entity.Permission
***********************************/
private Permission selectChildren(Permission permission, List<Permission> list) {
//1 因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
permission.setChildren(new ArrayList<Permission>());
//2 遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
for (Permission child : list) {
if (permission.getId().equals(child.getPid())) {
child.setLevel(permission.getLevel() + 1);
if (child.getChildren() == null) {
child.setChildren(new ArrayList<>());
}
// permission.getChildren().add(child);
// selectChildren(child,list);
permission.getChildren().add(selectChildren(child, list));
}
}
return permission;
}
/***********************************
* 用途说明:转化成前端需要的格式
* @param permissionList
* 返回值说明:
* @return java.util.List<org.json.JSONObject>
***********************************/
private List<JSONObject> bulidJson(List<Permission> permissionList) {
List<JSONObject> menus = new ArrayList<>();
if (permissionList.size() == 1) {
Permission topNode = permissionList.get(0);
//组建左侧一级菜单
List<Permission> oneMenuList = topNode.getChildren();
for (Permission one : oneMenuList) {
JSONObject oneMenu = new JSONObject();
oneMenu.put("path", one.getPath());
oneMenu.put("component", one.getComponent());
oneMenu.put("redirect", "noredirect");//第一级不需要重定向
oneMenu.put("name", "name_" + one.getId());
oneMenu.put("title", one.getName());
oneMenu.put("label", one.getName());
oneMenu.put("icon", one.getIcon());
oneMenu.put("hidden", false);//一级不需要因此,3级需要
JSONObject oneMeta = new JSONObject();
oneMeta.put("title", one.getName());
oneMeta.put("icon", one.getIcon());
oneMenu.put("meta", oneMeta);
List<JSONObject> children = new ArrayList<>();
List<Permission> twoMenuList = one.getChildren();//二级菜单
for (Permission two : twoMenuList) {
JSONObject twoMenu = new JSONObject();
twoMenu.put("path", two.getPath());
twoMenu.put("component", two.getComponent());
twoMenu.put("title", two.getName());
twoMenu.put("label", two.getName());
twoMenu.put("icon", two.getIcon());
// twoMenu.put("redirect", "noredirect");//第一级不需要重定向
twoMenu.put("name", "name_" + two.getId());
twoMenu.put("hidden", false);//一级不需要因此,3级需要
JSONObject twoMeta = new JSONObject();
twoMeta.put("title", two.getName());
twoMeta.put("icon", two.getIcon());
twoMenu.put("meta", twoMeta);
children.add(twoMenu);
//功能按钮
List<Permission> threeMenuList = two.getChildren();
for (Permission three : threeMenuList) {
if (StringUtils.isEmpty(three.getPath())) {
continue;
}
JSONObject threeMenu = new JSONObject();
threeMenu.put("path", three.getPath());
threeMenu.put("component", three.getComponent());
// threeMenu.put("redirect", "noredirect");//第一级不需要重定向
threeMenu.put("name", "name_" + three.getId());
threeMenu.put("title", three.getName());
threeMenu.put("label", three.getName());
threeMenu.put("icon", three.getIcon());
threeMenu.put("hidden", true);//一级不需要因此,3级需要
JSONObject threeMeta = new JSONObject();
threeMeta.put("title", three.getName());
threeMeta.put("icon", three.getIcon());
threeMenu.put("meta", threeMeta);
children.add(threeMenu);
}
}
oneMenu.put("children", children);
menus.add(oneMenu);
}
}
return menus;
}
9、树形结构的菜单数据
[
{
"id": "1",
"pid": "0",
"name": "全部数据",
"type": 0,
"permissionValue": null,
"path": null,
"component": null,
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:13:06",
"gmtModified": "2019-11-15 17:13:06",
"level": 1,
"children": [
{
"id": "1195268474480156673",
"pid": "1",
"name": "权限管理",
"type": 1,
"permissionValue": null,
"path": "",
"component": "",
"icon": "monitor",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:14:23",
"gmtModified": "2023-09-01 22:14:23",
"level": 2,
"children": [
{
"id": "1195268893830864898",
"pid": "1195268474480156673",
"name": "菜单管理",
"type": 1,
"permissionValue": null,
"path": "/menu",
"component": "sys/menu/Menu",
"icon": "menu",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:06:13",
"gmtModified": "2023-09-01 22:06:13",
"level": 3,
"children": [
{
"id": "1195270887933009922",
"pid": "1195268893830864898",
"name": "删除",
"type": 2,
"permissionValue": "permission.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:41",
"gmtModified": "2019-11-15 17:22:41",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270862100291586",
"pid": "1195268893830864898",
"name": "修改",
"type": 2,
"permissionValue": "permission.update",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:35",
"gmtModified": "2019-11-15 17:22:35",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270810560684034",
"pid": "1195268893830864898",
"name": "添加",
"type": 2,
"permissionValue": "permission.add",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:23",
"gmtModified": "2019-11-15 17:22:23",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270744097742849",
"pid": "1195268893830864898",
"name": "查看",
"type": 2,
"permissionValue": "permission.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:22:07",
"gmtModified": "2019-11-15 17:22:07",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
},
{
"id": "1195268788138598401",
"pid": "1195268474480156673",
"name": "角色管理",
"type": 1,
"permissionValue": null,
"path": "/role",
"component": "sys/role/Role",
"icon": "s-check",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:10:24",
"gmtModified": "2023-09-01 22:10:24",
"level": 3,
"children": [
{
"id": "1195270621548568578",
"pid": "1195268788138598401",
"name": "角色权限",
"type": 2,
"permissionValue": "role.distribution",
"path": "/sys/role/distribution/:id",
"component": "sys/role/roleForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:45:51",
"gmtModified": "2023-09-01 18:45:51",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270442602782721",
"pid": "1195268788138598401",
"name": "删除",
"type": 2,
"permissionValue": "role.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:20:55",
"gmtModified": "2019-11-15 17:20:55",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195270037005197313",
"pid": "1195268788138598401",
"name": "添加",
"type": 2,
"permissionValue": "role.add",
"path": "role/add",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:19:19",
"gmtModified": "2019-11-18 11:05:42",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269903542444034",
"pid": "1195268788138598401",
"name": "查看",
"type": 2,
"permissionValue": "role.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:47",
"gmtModified": "2019-11-15 17:18:47",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269821262782465",
"pid": "1195268788138598401",
"name": "修改",
"type": 2,
"permissionValue": "role.update",
"path": "role/update/:id",
"component": "/acl/role/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:18:27",
"gmtModified": "2019-11-15 17:19:53",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
},
{
"id": "1195268616021139457",
"pid": "1195268474480156673",
"name": "用户管理",
"type": 1,
"permissionValue": null,
"path": "/user",
"component": "sys/user/User",
"icon": "user",
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 22:04:29",
"gmtModified": "2023-09-01 22:04:29",
"level": 3,
"children": [
{
"id": "1196301740985311234",
"pid": "1195268616021139457",
"name": "分配角色",
"type": 2,
"permissionValue": "user.assgin",
"path": "/userForm/:id",
"component": "sys/user/userForm",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2023-09-01 18:44:21",
"gmtModified": "2023-09-01 18:44:21",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269547269873666",
"pid": "1195268616021139457",
"name": "删除",
"type": 2,
"permissionValue": "user.remove",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:22",
"gmtModified": "2019-11-15 17:17:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269473479483394",
"pid": "1195268616021139457",
"name": "修改",
"type": 2,
"permissionValue": "user.update",
"path": "user/update/:id",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:17:04",
"gmtModified": "2019-11-15 17:17:04",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269295926206466",
"pid": "1195268616021139457",
"name": "添加",
"type": 2,
"permissionValue": "user.add",
"path": "user/add",
"component": "/acl/user/form",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:16:22",
"gmtModified": "2019-11-15 17:16:22",
"level": 4,
"children": [ ],
"hasSelect": false
},
{
"id": "1195269143060602882",
"pid": "1195268616021139457",
"name": "查看",
"type": 2,
"permissionValue": "user.list",
"path": "",
"component": "",
"icon": null,
"status": null,
"isDeleted": 0,
"gmtCreate": "2019-11-15 17:15:45",
"gmtModified": "2019-11-17 21:57:16",
"level": 4,
"children": [ ],
"hasSelect": false
}
],
"hasSelect": false
}
],
"hasSelect": false
}
],
"hasSelect": false
}
]



