1.项目介绍和后台项目搭建
本项目是基于springboot + vue的前后端分离权限项目,适合做毕设项目和个人学习,手把手搭建。
后端技术栈:springboot,MySQL,redis,Maven
前端技术栈:vue,Vuex,Vue-Router,Echarts,elementUI
通用的权限管理项目,可以在本系统的基础上开发大部分的管理系统。
导图:
创建springboot项目,如视频操作
pom.xml文件
<?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.example</groupId>
<artifactId>base-authority</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>base-authority</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</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JSON的依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.69</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建数据库,在创建sys_user表
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id主键',
`username` varchar(255) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`nickname` varchar(255) DEFAULT NULL COMMENT '昵称',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`email` varchar(255) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(18) DEFAULT NULL COMMENT '联系方式',
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`header_url` varchar(255) DEFAULT NULL COMMENT '用户头像',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4;
创建user的后端代码,entity,mapper,service,controller层代码,如视频操作,代码太多就不复制粘贴了。
2.前端项目首页搭建
效果图
Aside.vue
<template>
<el-menu default-active="1-4-1"
class="el-menu-vertical-demo"
style="min-height:100%;overflow-x: hidden;"
text-color="#fff"
active-text-color="#ffd046"
background-color="rgb(48,65,86)"
:collapse-transition="false"
router
:collapse="isCollapse">
<div style="height: 60px;line-height: 60px;text-align: center">
<img src="../assets/logo.png" style="width:30px;position: relative;margin-right: 5px;top:6px">
<b style="color:white" v-show="logoTextShow">后台管理系统</b>
</div>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span slot="title">导航一</span>
</template>
<el-menu-item-group>
<span slot="title">分组一</span>
<el-menu-item index="/user">用户管理</el-menu-item>
<el-menu-item index="/role">角色管理</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<span slot="title">选项4</span>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
</el-menu>
</template>
<script>
export default {
name: "Aside",
props:{
isCollapse:Boolean,
logoTextShow:Boolean
}
}
</script>
<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
</style>
Header.vue
<template>
<div style="line-height: 60px;display: flex">
<div style="flex: 1;font-size: 30px">
<span :class="collapseBtnClass" style="cursor: pointer" @click="collapse"></span>
<el-breadcrumb separator="/" style="display: inline-block;margin-left:10px;position: absolute;top:22px" >
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item><b>{{currentPathName}}</b></el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-dropdown style="width: 150px;cursor: pointer;text-align: center">
<div style="display: inline-block">
<img src="../assets/logo.png" style="width:65px;height:45px;border-radius: 50%;position: relative;top:10px;right:8px"/>
<span>张三</span><i class="el-icon-arrow-down" style="margin-left:5px"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 100px;text-align: center;text-decoration: none">
<el-dropdown-item style="font-size: 15px;padding:5px 0">
<router-link to="/person" style="text-decoration: none;color: #606266">个人信息</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 15px;padding:5px 0">
<router-link to="/person" style="text-decoration: none;color: #606266">修改密码</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 15px;padding:5px 0">
<div style="text-decoration: none">退出</div>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
name: "Header",
props:{
collapseBtnClass:String
},
computed:{
currentPathName(){
return this.$store.state.currentPathName;
}
},
methods:{
collapse(){
this.$emit('collapse')
}
}
}
</script>
<style scoped>
</style>
Manage.vue
<template>
<el-container style="min-height: 100vh">
<el-aside :width="sideWidth + 'px'">
<Aside :isCollapse="isCollapse" :logoTextShow="logoTextShow"></Aside>
</el-aside>
<el-container>
<el-header style="border-bottom: 1px solid #ccc">
<Header @collapse="collapse" :collapseBtnClass="collapseBtnClass"></Header>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
import Aside from "@/components/Aside";
import Header from "@/components/Header";
export default {
name: "Manage",
components:{
Aside,Header
},
data(){
return{
isCollapse:false,
logoTextShow:true,
collapseBtnClass:'el-icon-s-fold',
sideWidth:200
}
},
methods:{
collapse(){
this.isCollapse = !this.isCollapse;
if(this.isCollapse){
//收缩
this.sideWidth = 64;
this.logoTextShow = false;
this.collapseBtnClass = 'el-icon-s-unfold';
}else{
this.sideWidth = 200;
this.logoTextShow = true;
this.collapseBtnClass = 'el-icon-s-fold';
}
}
}
}
</script>
<style scoped>
/*去掉aside侧边栏的底部滚动条*/
.el-aside::-webkit-scrollbar {
display: none;
}
</style>
VUEX的index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
currentPathName:''
},
getters: {
},
mutations: {
setPath(state){
state.currentPathName = localStorage.getItem('currentPathName');
}
},
actions: {
},
modules: {
}
})
Router的index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '../store'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'manage',
component: () => import(/* webpackChunkName: "about" */ '../views/Manage.vue'),
children:[
{
path: 'user',
name: '用户管理',
component: () => import(/* webpackChunkName: "about" */ '../views/User.vue'),
},
{
path: 'role',
name: '角色管理',
component: () => import(/* webpackChunkName: "about" */ '../views/Role.vue'),
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to,from,next) => {
localStorage.setItem('currentPathName',to.name);
store.commit('setPath')
next();
})
export default router
3.用户管理
补充
sys_user表添加 hearer_url 头像字段
去掉Aside侧边栏的底部滚动条
安装axios
npm install axios -S
封装axios的请求文件
import axios from 'axios'
import { Notification, MessageBox, Message, Loading } from 'element-ui'
import ElementUI from 'element-ui'
import router from "@/router";
const request = axios.create({
baseURL:'http://localhost:8899/',
timeout:5000
})
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=UTF-8'
let user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null
if(user){
config.headers["token"] = user.token;
}
return config;
},error => {
return Promise.reject(error)
})
request.interceptors.response.use(
response => {
let res = response.data;
if(response.config.responseType === 'blob'){
return res;
}
if(typeof res === 'string'){
res = res ? JSON.parse(res) : res
}
// 当权限验证不通过的时候给出提示
if (res.code === '401') {
ElementUI.Message({
message: res.msg,
type: 'error'
});
console.log('router.currentRoute.fullPath ')
console.log(router.currentRoute.fullPath )
// if (router.currentRoute.fullPath !== '/login') {
// router.push('/login')
// }
}
return res;
},
error => {
if(error.code === '401'){
router.push("/login")
}
Message.error(error)
return Promise.reject(error);
}
)
export default request
编写User.vue的界面,五部分组成:
1.搜索栏
2.加新增和批量删除按钮
3.table表格
4.新增和编辑的弹出框
5.分页
生命周期介绍 //created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成识图 //mounted:在模板渲染成html后调用,通常初始化页面完成后,再对html的dom节点进行一些需要的操作。
跨域
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
报错显示:
解决方法:
package com.example.authority.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;
@Configuration
public class CorsConfig {
// 当前跨域请求最大有效时长。这里默认1天
private static final long MAX_AGE = 24 * 60 * 60;
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.setMaxAge(MAX_AGE);
source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
return new CorsFilter(source);
}
}
完成的User.vue页面
<template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入用户名" v-model="username" prefix-icon="el-icon-user"></el-input>
<el-input style="width: 200px" placeholder="请输入邮箱" v-model="email" prefix-icon="el-icon-message"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
<!-- 新增。批量删除-->
<el-button style="margin-right: 10px;" type="success" class="el-icon-plus" @click="save">新增</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="deleteBatch"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">批量删除</el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-style="getRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="nickname" label="昵称"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="phone" label="联系方式"></el-table-column>
<el-table-column prop="roleId" label="角色"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="headerUrl" label="头像"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="primary" class="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="handleDelete(scope.row.id)"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="block" style="padding:10px 0;align-content: center;margin-left: 30%;margin-top:30px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 15, 20]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="收货地址" :visible.sync="dialogFormVisible" width="30%">
<el-form :model="form">
<el-form-item label="用户名" :label-width="formLabelWidth">
<el-input v-model="form.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" :label-width="formLabelWidth">
<el-input v-model="form.password" autocomplete="off" type="password" show-password></el-input>
</el-form-item>
<el-form-item label="昵称" :label-width="formLabelWidth">
<el-input v-model="form.nickname" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="地址" :label-width="formLabelWidth">
<el-input v-model="form.address" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="邮箱" :label-width="formLabelWidth">
<el-input v-model="form.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="联系方式" :label-width="formLabelWidth">
<el-input v-model="form.phone" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="handleAdd">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "User",
data(){
return{
pageSize:10,
pageNum:1,
username:'',
email:'',
tableData:[],
dialogFormVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0
}
},
created() {
this.load();
},
methods:{
load(){
this.request.get("/user/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
username:this.username,
email:this.email
}
}).then(res => {
this.tableData = res.data.records;
this.total = res.data.total;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.email = '';
this.username = '';
this.load();
},
save(){
this.dialogFormVisible = true;
this.form = {};
},
handleAdd(){
this.request.post("/user/save",this.form).then(res => {
if(res.code === '200'){
if(this.form.id){
this.$message.success('编辑成功');
}else{
this.$message.success('新增成功');
}
this.dialogFormVisible = false;
this.load();
}else{
this.$message.error('操作失败,请联系管理员')
}
})
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row));
this.dialogFormVisible = true;
},
handleDelete(id){
if(id){
this.request.delete('/user/deleteById/' + id).then(res => {
if(res.code === '200'){
this.$message.success('删除数据成功');
this.load();
}else{
this.$message.error('删除数据失败,请联系管理员')
}
})
}else{
this.$message.error('没有id信息,无法删除');
}
},
cancel(){
this.$message.success('取消操作成功');
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
deleteBatch(){
//批量删除数据
if(this.multipleSelection.length === 0){
this.$message.warning("请先选择要删除的数据");
return
}
const ids = this.multipleSelection.map(v => v.id);
this.request.post('/user/deleteBatch',ids).then(res => {
if(res.code === '200'){
this.$message.success('批量删除成功');
this.load();
}else{
this.$message.error('批量删除失败');
}
})
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
}
}
}
</script>
<style scoped>
</style>
4.角色管理
补充:删除数据的分页显示
创建角色表
创建角色管理后台代码
如视频操作,代码较多,不发出来了
创建角色页面
<template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入名称" v-model="name" prefix-icon="el-icon-user"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
<!-- 新增。批量删除-->
<el-button style="margin-right: 10px;" type="success" class="el-icon-plus" @click="save">新增</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="deleteBatch"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">批量删除</el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-style="getRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="name" label="角色名称"></el-table-column>
<el-table-column prop="code" label="角色编码"></el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="primary" class="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="handleDelete(scope.row.id)"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="block" style="padding:10px 0;align-content: center;margin-left: 30%;margin-top:30px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 15, 20]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="角色信息" :visible.sync="dialogFormVisible" width="30%">
<el-form :model="form">
<el-form-item label="角色名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="角色编码" :label-width="formLabelWidth">
<el-input v-model="form.code" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="描述" :label-width="formLabelWidth">
<el-input v-model="form.description" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="handleAdd">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "Role",
data(){
return{
pageSize:2,
pageNum:1,
name:'',
tableData:[],
dialogFormVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0
}
},
created() {
this.load();
},
methods:{
load(){
this.request.get("/role/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
name:this.name,
}
}).then(res => {
this.tableData = res.data.records;
this.total = res.data.total;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.email = '';
this.username = '';
this.load();
},
save(){
this.dialogFormVisible = true;
this.form = {};
},
handleAdd(){
this.request.post("/role/save",this.form).then(res => {
if(res.code === '200'){
if(this.form.id){
this.$message.success('编辑成功');
}else{
this.$message.success('新增成功');
}
this.dialogFormVisible = false;
this.load();
}else{
this.$message.error('操作失败,请联系管理员')
}
})
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row));
this.dialogFormVisible = true;
},
handleDelete(id){
if(id){
this.request.delete('/role/deleteById/' + id).then(res => {
if(res.code === '200'){
this.$message.success('删除数据成功');
this.handleCalPageNum();
}else{
this.$message.error('删除数据失败,请联系管理员')
}
})
}else{
this.$message.error('没有id信息,无法删除');
}
},
cancel(){
this.$message.success('取消操作成功');
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
deleteBatch(){
//批量删除数据
if(this.multipleSelection.length === 0){
this.$message.warning("请先选择要删除的数据");
return
}
const ids = this.multipleSelection.map(v => v.id);
this.request.post('/role/deleteBatch',ids).then(res => {
if(res.code === '200'){
this.$message.success('批量删除成功');
this.handleCalPageNum();
}else{
this.$message.error('批量删除失败');
}
})
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
},
handleCalPageNum(){
this.request.get("/role/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
username:this.username,
email:this.email
}
}).then(res => {
this.total = res.data.total;
this.pageNum = (this.total % this.pageSize === 0) ? (this.total / this.pageSize) : Math.floor((this.total / this.pageSize) + 1);
if(this.pageNum < 1){
this.pageNum = 1;
}
this.load();
})
}
}
}
</script>
<style scoped>
</style><template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入名称" v-model="name" prefix-icon="el-icon-user"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
<!-- 新增。批量删除-->
<el-button style="margin-right: 10px;" type="success" class="el-icon-plus" @click="save">新增</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="deleteBatch"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">批量删除</el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-style="getRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="name" label="角色名称"></el-table-column>
<el-table-column prop="code" label="角色编码"></el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="primary" class="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="handleDelete(scope.row.id)"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="block" style="padding:10px 0;align-content: center;margin-left: 30%;margin-top:30px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 15, 20]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="角色信息" :visible.sync="dialogFormVisible" width="30%">
<el-form :model="form">
<el-form-item label="角色名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="角色编码" :label-width="formLabelWidth">
<el-input v-model="form.code" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="描述" :label-width="formLabelWidth">
<el-input v-model="form.description" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="handleAdd">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "Role",
data(){
return{
pageSize:2,
pageNum:1,
name:'',
tableData:[],
dialogFormVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0
}
},
created() {
this.load();
},
methods:{
load(){
this.request.get("/role/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
name:this.name,
}
}).then(res => {
this.tableData = res.data.records;
this.total = res.data.total;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.email = '';
this.username = '';
this.load();
},
save(){
this.dialogFormVisible = true;
this.form = {};
},
handleAdd(){
this.request.post("/role/save",this.form).then(res => {
if(res.code === '200'){
if(this.form.id){
this.$message.success('编辑成功');
}else{
this.$message.success('新增成功');
}
this.dialogFormVisible = false;
this.load();
}else{
this.$message.error('操作失败,请联系管理员')
}
})
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row));
this.dialogFormVisible = true;
},
handleDelete(id){
if(id){
this.request.delete('/role/deleteById/' + id).then(res => {
if(res.code === '200'){
this.$message.success('删除数据成功');
this.handleCalPageNum();
}else{
this.$message.error('删除数据失败,请联系管理员')
}
})
}else{
this.$message.error('没有id信息,无法删除');
}
},
cancel(){
this.$message.success('取消操作成功');
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
deleteBatch(){
//批量删除数据
if(this.multipleSelection.length === 0){
this.$message.warning("请先选择要删除的数据");
return
}
const ids = this.multipleSelection.map(v => v.id);
this.request.post('/role/deleteBatch',ids).then(res => {
if(res.code === '200'){
this.$message.success('批量删除成功');
this.handleCalPageNum();
}else{
this.$message.error('批量删除失败');
}
})
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
},
handleCalPageNum(){
this.request.get("/role/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
username:this.username,
email:this.email
}
}).then(res => {
this.total = res.data.total;
this.pageNum = (this.total % this.pageSize === 0) ? (this.total / this.pageSize) : Math.floor((this.total / this.pageSize) + 1);
if(this.pageNum < 1){
this.pageNum = 1;
}
this.load();
})
}
}
}
</script>
<style scoped>
</style>
完善用户页面的角色部分
集成git,把代码提交git仓库
5.登录注册页面
5.1 验证码组件
<template>
<span class="s-canvas">
<canvas id="s-canvas" :width="contentWidth" :height="contentHeight"></canvas>
</span>
</template>
<script>
export default {
name: 'SIdentify',
props: {
identifyCode: {
type: String,
default: '1234'
},
fontSizeMin: {
type: Number,
default: 16
},
fontSizeMax: {
type: Number,
default: 40
},
backgroundColorMin: {
type: Number,
default: 180
},
backgroundColorMax: {
type: Number,
default: 240
},
colorMin: {
type: Number,
default: 50
},
colorMax: {
type: Number,
default: 160
},
lineColorMin: {
type: Number,
default: 40
},
lineColorMax: {
type: Number,
default: 180
},
dotColorMin: {
type: Number,
default: 0
},
dotColorMax: {
type: Number,
default: 255
},
contentWidth: {
type: Number,
default: 112
},
contentHeight: {
type: Number,
default: 38
}
},
methods: {
// 生成一个随机数
randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
},
// 生成一个随机的颜色
randomColor(min, max) {
let r = this.randomNum(min, max)
let g = this.randomNum(min, max)
let b = this.randomNum(min, max)
return 'rgb(' + r + ',' + g + ',' + b + ')'
},
drawPic() {
let canvas = document.getElementById('s-canvas')
let ctx = canvas.getContext('2d')
ctx.textBaseline = 'bottom'
// 绘制背景
ctx.fillStyle = this.randomColor(this.backgroundColorMin, this.backgroundColorMax)
ctx.fillRect(0, 0, this.contentWidth, this.contentHeight)
// 绘制文字
for (let i = 0; i < this.identifyCode.length; i++) {
this.drawText(ctx, this.identifyCode[i], i)
}
this.drawLine(ctx)
this.drawDot(ctx)
},
drawText(ctx, txt, i) {
ctx.fillStyle = this.randomColor(this.colorMin, this.colorMax)
ctx.font = this.randomNum(this.fontSizeMin, this.fontSizeMax) + 'px SimHei'
let x = (i + 1) * (this.contentWidth / (this.identifyCode.length + 1))
let y = this.randomNum(this.fontSizeMax, this.contentHeight - 5)
var deg = this.randomNum(-45, 45)
// 修改坐标原点和旋转角度
ctx.translate(x, y)
ctx.rotate(deg * Math.PI / 180)
ctx.fillText(txt, 0, 0)
// 恢复坐标原点和旋转角度
ctx.rotate(-deg * Math.PI / 180)
ctx.translate(-x, -y)
},
drawLine(ctx) {
// 绘制干扰线
for (let i = 0; i < 3; i++) {
ctx.strokeStyle = this.randomColor(this.lineColorMin, this.lineColorMax)
ctx.beginPath()
ctx.moveTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.lineTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.stroke()
}
},
drawDot(ctx) {
// 绘制干扰点
for (let i = 0; i < 10; i++) {
ctx.fillStyle = this.randomColor(0, 255)
ctx.beginPath()
ctx.arc(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight), 1, 0, 2 * Math.PI)
ctx.fill()
}
}
},
watch: {
identifyCode() {
this.drawPic()
}
},
mounted() { //mounted:在模板渲染成html后调用,通常初始化页面完成后,再对html的dom节点进行一些需要的操作。
this.drawPic()
}
}
</script>
<style>
.s-canvas {
height: 38px;
}
canvas {
margin-top: 1px;
margin-left: 8px;
}
</style>
5.2 编写Login.vue页面
<template>
<div class="wrapper">
<!-- 项目名称-->
<div style="height: 60px;line-height: 60px;font-size: 20px;padding-left: 50px;color: white;text-align: center;align-content: center;background-color: #cccccc">
权限脚手架项目
</div>
<div style="display: flex;width:55%;height:40%;margin: 150px auto;background-color: white;border-radius: 10px;overflow: hidden;background-color: bisque">
<!-- 左侧图片显示-->
<div style="width: 50%;margin-top: 30px;margin-left: 30px">
<img src="../assets/register.png" alt="" style="width:100%;height:90%" />
</div>
<!-- 提交表单-->
<div style="width:350px;margin-top: 30px">
<div style="margin: 20px 0;text-align: center;font-size: 20px">登录页面</div>
<el-form :model="userForm" :rules="rules" ref="userForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" prefix-icon="el-icon-user"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="userForm.password" prefix-icon="el-icon-lock" type="password" show-password></el-input>
</el-form-item>
<el-form-item label="验证码" prop="identifyCode">
<el-input v-model="userForm.identifyCode" style="width:50%;position:relative;bottom:8px;" prefix-icon="el-icon-mobile-phone"></el-input>
<span @click="refreshCode" style="position: relative;top:5px;left:6px;">
<SIdentity :identify-code="identifyCode"></SIdentity>
</span>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login('userForm')">登录</el-button>
<el-button type="success" @click="$router.push('/register')">前往注册</el-button>
<!-- <el-button type="warning" @click="resetForm('userForm')">重置</el-button>-->
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
import SIdentity from "@/components/SIdentity";
export default {
name: "Login",
components:{
SIdentity
},
mounted() {
this.refreshCode()
},
data(){
return{
userForm:{},
identifyCode:'',
identifyCodes:'1234567890zxcvbnmasdfghjklqwertyuiopQWERTYUIPOLSADFGHJ',
rules: {
username: [
{ required: true, message: '请输入用户名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 50, message: '长度不能小于6位', trigger: 'blur' }
],
identifyCode: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ min: 4, max: 4, message: '验证码长度为4位', trigger: 'blur' }
]
}
}
},
methods:{
//提交数据
login(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
if(this.identifyCode.toUpperCase() !== this.userForm.identifyCode.toUpperCase()){
this.$message.warning("验证码输入错误,请重新输入");
return;
}
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
},
// 重置数据
resetForm(formName) {
this.$refs[formName].resetFields();
},
// 验证码方法
refreshCode(){
this.identifyCode = ''
this.makeCode(this.identifyCodes,4);
},
makeCode(o,l){
for(let i = 0;i < l;i++){
this.identifyCode += this.identifyCodes[
this.randomNum(0,this.identifyCodes.length)
]
}
},
randomNum(min,max){
return Math.floor(Math.random() * (max - min) + min);
}
}
}
</script>
<style scoped>
.wrapper{
background: url("../assets/auth.jpg");
width:100%;
height: 100%;
position: fixed;
background-size: 100% 100%;
}
</style>
5.4 编写Register.vue页面
<template>
<div class="wrapper">
<!-- 项目名称-->
<div style="height: 60px;line-height: 60px;font-size: 20px;padding-left: 50px;color: white;text-align: center;align-content: center;background-color: #cccccc">
权限脚手架项目
</div>
<div style="display: flex;width:55%;height:40%;margin: 150px auto;background-color: white;border-radius: 10px;overflow: hidden;background-color: bisque">
<!-- 左侧图片显示-->
<div style="width: 50%;margin-top: 30px;margin-left: 30px">
<img src="../assets/register.png" alt="" style="width:100%;height:90%" />
</div>
<!-- 提交表单-->
<div style="width:350px;margin-top: 30px">
<div style="margin: 20px 0;text-align: center;font-size: 20px">注册页面</div>
<el-form :model="userForm" :rules="rules" ref="userForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" prefix-icon="el-icon-user"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="userForm.password" prefix-icon="el-icon-lock" type="password" show-password></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="userForm.confirmPassword" prefix-icon="el-icon-lock" type="password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="register('userForm')">注册</el-button>
<el-button type="success" @click="$router.push('/login')">返回登录</el-button>
<!-- <el-button type="warning" @click="resetForm('userForm')">重置</el-button>-->
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data(){
return{
userForm:{},
rules: {
username: [
{ required: true, message: '请输入用户名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 50, message: '长度不能小于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请输入确认密码', trigger: 'blur' },
{ min: 6, max: 50, message: '长度不能小于6位', trigger: 'blur' }
]
}
}
},
methods:{
//提交数据
register(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
if(this.userForm.password !== this.userForm.confirmPassword){
this.$message.warning("两次密码不一致,请重新输入");
return;
}
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
},
// 重置数据
resetForm(formName) {
this.$refs[formName].resetFields();
},
// 验证码方法
refreshCode(){
this.identifyCode = ''
this.makeCode(this.identifyCodes,4);
},
makeCode(o,l){
for(let i = 0;i < l;i++){
this.identifyCode += this.identifyCodes[
this.randomNum(0,this.identifyCodes.length)
]
}
},
randomNum(min,max){
return Math.floor(Math.random() * (max - min) + min);
}
}
}
</script>
<style scoped>
.wrapper{
background: url("../assets/auth.jpg");
width:100%;
height: 100%;
position: fixed;
background-size: 100% 100%;
}
</style>
5.4 添加两个路由到index.js文件
6.登录注册接口
创建UserDto类
package com.example.authority.dto;
import lombok.Data;
import java.util.Date;
@Data
public class UserDto {
private Integer id;
private String username;
private String password;
private String nickname;
private String headerUrl;
private String token;
}
编写两个接口代码,看视频
JwtUtils的代码
package com.example.authority.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtils {
public static String generateToken(String userId,String sign){
return JWT.create().withAudience(userId)
.withExpiresAt(new Date(System.currentTimeMillis() + 3600 * 24 * 1000))
.sign(Algorithm.HMAC256(sign));
}
}
7.菜单管理
用户管理的bug:
新增和修改用户名的时候加一下判断条件:用户名不能重复
角色管理的bug:
删除角色的时候先判断角色是否被使用了
RBAC:RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案,相对于传统方案,RBAC提供了中间层Role(角色),其权限模式是给用户分配角色,给角色分配权限
菜单表:
字典管理,如视频操作
菜单后台代码,如视频操作
菜单页面:
<template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入菜单名称" v-model="name" prefix-icon="el-icon-user"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
<!-- 新增。批量删除-->
<el-button style="margin-right: 10px;" type="success" class="el-icon-plus" @click="save(null)">新增</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="deleteBatch"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">批量删除</el-button>
</el-popconfirm>
</div>
<el-table
:data="tableData" border stripe
:header-cell-style="getRowClass"
@selection-change="handleSelectionChange"
row-key="id"
border
default-expand-all>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="name" label="菜单名称"></el-table-column>
<el-table-column prop="path" label="菜单路径"></el-table-column>
<el-table-column label="菜单图标">
<template slot-scope="scope">
<i :class="scope.row.icon"/>
</template>
</el-table-column>
<el-table-column prop="description" label="菜单描述"></el-table-column>
<el-table-column prop="pagePath" label="页面路径"></el-table-column>
<el-table-column prop="sortNum" label="排序"></el-table-column>
<el-table-column label="操作" width="350">
<template slot-scope="scope">
<el-button type="success" class="el-icon-circle-plus" @click="save(scope.row.id)" v-if="!scope.row.pid && !scope.row.path ">新增子菜单</el-button>
<el-button type="primary" class="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="handleDelete(scope.row.id)"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog title="菜单信息" :visible.sync="dialogFormVisible" width="30%">
<el-form :model="form">
<el-form-item label="菜单名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="菜单路径" :label-width="formLabelWidth">
<el-input v-model="form.path" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="菜单图标" :label-width="formLabelWidth">
<el-select v-model="form.icon" filterable placeholder="请选择图标">
<el-option
v-for="dict in dictList"
:key="dict.name"
:label="dict.name"
:value="dict.value">
<i :class="dict.value"></i> {{dict.name}}
</el-option>
</el-select>
</el-form-item>
<el-form-item label="页面路径" :label-width="formLabelWidth">
<el-input v-model="form.pagePath" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="排序" :label-width="formLabelWidth">
<el-input v-model="form.sortNum" autocomplete="off" type="number"></el-input>
</el-form-item>
<el-form-item label="描述" :label-width="formLabelWidth">
<el-input v-model="form.description" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="handleAdd">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "Role",
data(){
return{
pageSize:10,
pageNum:1,
name:'',
tableData:[],
dialogFormVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0,
dictList:[]
}
},
created() {
this.load();
},
methods:{
load(){
this.request.get("/menu/findAll",{
params:{
name:this.name,
}
}).then(res => {
this.tableData = res.data;
});
this.request.get("/dict/findAll",{
params:{
type:'icon',
}
}).then(res => {
this.dictList = res.data;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.name = '';
this.load();
},
save(pid){
this.dialogFormVisible = true;
this.form = {};
if(pid){
this.form.pid = pid;
}
},
handleAdd(){
this.request.post("/menu/save",this.form).then(res => {
if(res.code === '200'){
if(this.form.id){
this.$message.success('编辑成功');
}else{
this.$message.success('新增成功');
}
this.dialogFormVisible = false;
this.load();
}else{
this.$message.error(res.msg)
}
})
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row));
this.dialogFormVisible = true;
},
handleDelete(id){
if(id){
this.request.delete('/menu/deleteById/' + id).then(res => {
if(res.code === '200'){
this.$message.success('删除数据成功');
this.load();
}else{
this.$message.error(res.msg)
}
})
}else{
this.$message.error('没有id信息,无法删除');
}
},
cancel(){
this.$message.success('取消操作成功');
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
deleteBatch(){
//批量删除数据
if(this.multipleSelection.length === 0){
this.$message.warning("请先选择要删除的数据");
return
}
const ids = this.multipleSelection.map(v => v.id);
this.request.post('/menu/deleteBatch',ids).then(res => {
if(res.code === '200'){
this.$message.success('批量删除成功');
this.load();
}else{
this.$message.error(res.msg);
}
})
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
}
}
}
</script>
<style scoped>
</style>
8.角色分配权限
添加swagger配置类
package com.example.authority.config;
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.builders.RequestHandlerSelectors;
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;
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.pathMapping("/")
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.authority.controller")) //controller类所在的路径
.paths(PathSelectors.any())
.build().apiInfo(new ApiInfoBuilder()
.title("SpringBoot整合Swagger")
.description("SpringBoot整合Swagger,详细信息......")
.version("9.0")
.contact(new Contact("111","blog.csdn.net","www@gmail.com"))
.license("hello")
.licenseUrl("http://www.baidu.com")
.build());
}
}
启动类添加 :@EnableSwagger2
package com.example.authority;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@MapperScan("com.example.authority.mapper")
@EnableSwagger2
public class BaseAuthorityApplication {
public static void main(String[] args) {
SpringApplication.run(BaseAuthorityApplication.class, args);
}
}
常用注解
@Api(tags = "用户管理"):加在controller类上做说明@ApiOperation(value = "新增/修改用户信息"):加在接口方法上
全局异常处理
处理类
package com.example.authority.exception;
import com.example.authority.common.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author wangjy
* @version 1.0
* @date 2023/6/30 15:29
* 全局异常处理器
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthException.class)
@ResponseBody
public Result handle(AuthException ex){
return Result.error(ex.getCode(),ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handle(Exception ex){
return Result.error("500",ex.getMessage());
}
}
自定义异常
package com.example.authority.exception;
import lombok.Data;
@Data
public class AuthException extends RuntimeException{
private String code;
public AuthException( String code,String msg) {
super(msg);
this.code = code;
}
}
在角色页面增加分配菜单功能,如视频操作
9.动态菜单和动态路由
动态菜单:根据角色分配的菜单在Aside页面动态显示
动态路由:动态路由,动态即不是写死的,是可变的。我们可以根据自己不同的需求加载不同的路由,做到不同的实现及页面的渲染。动态的路由存储可分为两种,一种是将路由存储到前端。另一种则是将路由存储到数据库。动态路由的使用一般结合角色权限控制一起使用。
具体代码看视频
10.后端拦截器
解决路由显示问题
import Vue, {set} from 'vue'
import VueRouter from 'vue-router'
import store from '../store'
Vue.use(VueRouter)
import { Notification, MessageBox, Message, Loading } from 'element-ui'
import ElementUI from 'element-ui'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: "about" */ '../views/Login.vue'),
},
{
path: '/register',
name: 'Register',
component: () => import(/* webpackChunkName: "about" */ '../views/Register.vue'),
},
{
path: '/404',
name: '404',
component: () => import(/* webpackChunkName: "about" */ '../views/404.vue'),
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export const setRoutes = () => {
//获取浏览器缓存的菜单数据
const localMenus = localStorage.getItem("menus") ;
if(localMenus){
const currentRoutes = router.getRoutes().map(v => v.name);
if(!currentRoutes.includes('manage')){
//当前Router不包含manage,在拼装
const manageRoute = {
path: '/',
name: 'manage',
component: () => import(/* webpackChunkName: "about" */ '../views/Manage.vue'),
children:[]
};
const menus = JSON.parse(localMenus);
menus.forEach(item => {
if(item.path){
const itemMenu = {
path:item.path.replace("/",""),
name:item.name,
component: () => import(/* webpackChunkName: "about" */ '../views/' + item.pagePath + '.vue'),
};
manageRoute.children.push(itemMenu);
}else if(item.children.length){
item.children.forEach(item => {
const itemMenu = {
path:item.path.replace("/",""),
name:item.name,
component: () => import(/* webpackChunkName: "about" */ '../views/' + item.pagePath + '.vue'),
};
manageRoute.children.push(itemMenu);
})
}
})
router.addRoute(manageRoute);
console.log(router.getRoutes())
}
}
}
setRoutes()
router.beforeEach((to,from,next) => {
localStorage.setItem('currentPathName',to.name);
store.commit('setPath')
const localMenus = localStorage.getItem("menus");
if(!to.matched.length){
//没有匹配到路由(也就是未找到路由)
if(localMenus){
//用户登录了
next('/404')
}else{
ElementUI.Message({
message: '请先登录',
type: 'warning'
});
next('/login')
}
}
next();
})
export default router
后端拦截器代码:看视频,详细教导。
11.AOP记录日志
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
获取request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
剩下的代码看视频
12.日志前端页面,退出登录
12.1.补充配置拦截器里面放行swagger
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").excludePathPatterns(
"/swagger-resources/**"
,"/webjars/**"
,"/v2/**"
,"/swagger-ui.html/**"
);
}
12.2.退出登录
看视频操作
12.3.重置路由器的路由集合
看视频操作
12.4.日志页面
<template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入操作用户名称" v-model="username" prefix-icon="el-icon-user"></el-input>
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入操作用户名称" v-model="type" prefix-icon="el-icon-info"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
</div>
<el-table :data="tableData" border stripe :header-cell-style="getRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="username" label="操作用户"></el-table-column>
<el-table-column prop="record" label="操作记录"></el-table-column>
<el-table-column prop="type" label="操作类型"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
</el-table>
<div class="block" style="padding:10px 0;align-content: center;margin-left: 30%;margin-top:30px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 15, 20]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name: "Role",
data(){
return{
pageSize:10,
pageNum:1,
name:'',
tableData:[],
dialogFormVisible:false,
menuVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0,
menuData:[],
checks:[],
props: {
children: 'children',
label: 'name'
}
}
},
created() {
this.load();
},
methods:{
load(){
this.request.get("/sysLog/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
username:this.username,
type:this.type
}
}).then(res => {
this.tableData = res.data.records;
this.total = res.data.total;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.username = '';
this.type = '';
this.load();
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
},
}
}
</script>
<style scoped>
</style>
13.文件管理
13.1.文件表:
13.2上传文件,下载文件接口
package com.example.authority.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.authority.annotation.Log;
import com.example.authority.annotation.NoAuth;
import com.example.authority.common.Result;
import com.example.authority.entity.SysFile;
import com.example.authority.service.SysFileService;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/sysFile")
public class SysFileController {
@Autowired
private SysFileService sysFileService;
@Value("${files.upload.path}")
private String fileUploadPath;
/**
* 批量删除文件
* @param idList
* @return
*/
@PostMapping("/deleteBatch")
@Log(record = "批量删除文件",type = "删除")
public Result deleteBatch(@RequestBody List<Integer> idList){
for (Integer id : idList) {
SysFile sysFile = sysFileService.getById(id);
sysFile.setIsDelete(1);
sysFileService.updateById(sysFile);
}
return Result.success();
}
/**
* 改变启用状态
* @param sysFile
* @return
*/
@PostMapping("/updateEnable")
@Log(record = "updateEnable",type = "修改")
public Result updateEnable(@RequestBody SysFile sysFile){
boolean b = sysFileService.updateById(sysFile);
if(b){
return Result.success();
}else{
return Result.error();
}
}
/**
* 根据id删除
* @param id
* @return
*/
@DeleteMapping("/deleteById/{id}")
@Log(record = "根据id删除文件",type = "删除")
public Result deleteById(@PathVariable Integer id){
SysFile sysFile = sysFileService.getById(id);
sysFile.setIsDelete(1);
boolean b = sysFileService.updateById(sysFile);
if(b){
return Result.success();
}else{
return Result.error();
}
}
/**
* 上传文件
* @param file
* @return
*/
@PostMapping("/upload")
@Log(record = "上传文件",type = "新增")
@NoAuth
public String upload(@RequestParam MultipartFile file) throws IOException {
String md5 = DigestUtils.md5Hex(file.getBytes());
String originalFilename = file.getOriginalFilename(); //文件的名称
String type = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);//文件类型
long size = file.getSize();
File uploadParentFile = new File(fileUploadPath);
if(!uploadParentFile.exists()){
uploadParentFile.mkdirs();
}
List<SysFile> existFileList = sysFileService.getByMD5(md5);
String url = null;
if(CollectionUtils.isNotEmpty(existFileList)){
//文件已经存在上传目录
url = existFileList.get(0).getUrl();
}else{
//文件不存在上传目录
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String fileUUID = uuid + "." + type;
File uploadFile = new File(fileUploadPath + fileUUID);
url = "http://localhost:8888/sysFile/" + fileUUID;
file.transferTo(uploadFile);
}
//存储数据库
SysFile sysFile = new SysFile();
sysFile.setName(originalFilename);
sysFile.setSize(size / 1024);
sysFile.setType(type);
sysFile.setUrl(url);
sysFile.setMd5(md5);
sysFileService.save(sysFile);
return url;
}
/**
* 下载文件
* @param fileUUID
*/
@GetMapping("/{fileUUID}")
@NoAuth
public void download(@PathVariable String fileUUID, HttpServletResponse response){
File downloadFile = new File(fileUploadPath + fileUUID);
try {
FileInputStream fileInputStream = new FileInputStream(downloadFile);
// 设置输出流的格式
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileUUID, "UTF-8"));
//作用是使客户端浏览器区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。
response.setContentType("application/octet-stream"); //.*( 二进制流,不知道下载文件类型)
ServletOutputStream outputStream = response.getOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
while((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
outputStream.flush();
outputStream.close();
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 查询全部数据
* @return
*/
@GetMapping("/findAll")
@Log(record = "查询全部文件",type = "查询")
public Result findAll(@RequestParam(name = "type",defaultValue = "") String type){
QueryWrapper<SysFile> queryWrapper = new QueryWrapper<>();
if(StringUtils.isNotBlank(type)){
queryWrapper.eq("type",type);
}
return Result.success(sysFileService.list(queryWrapper));
}
/**
* 分页查询
* @param pageNum:页码
* @param pageSize:每页条数
* @param name:角色名称
* @return
*/
@GetMapping("/findPage")
@Log(record = "查询文件分页",type = "查询")
public Result findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(name = "name",defaultValue = "") String name){
Page<SysFile> page = new Page<>(pageNum,pageSize);
QueryWrapper<SysFile> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("is_delete",0);
if(StringUtils.isNotBlank(name)){
queryWrapper.like("name",name);
}
Page<SysFile> sysFilePage = sysFileService.page(page, queryWrapper);
return Result.success(sysFilePage);
}
}
13.3前端页面
<template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入名称" v-model="name" prefix-icon="el-icon-user"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
<!-- 新增。批量删除-->
<el-upload
style="display: inline-block"
action="http://localhost:8888/sysFile/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:on-error="handleAvatarError">
<el-button style="margin-right: 10px;" type="primary" class="el-icon-plus">上传文件</el-button>
</el-upload>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="deleteBatch"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">批量删除</el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-style="getRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="name" label="文件名称"></el-table-column>
<el-table-column prop="type" label="文件类型"></el-table-column>
<el-table-column prop="size" label="文件大小(kb)"></el-table-column>
<el-table-column prop="enable" label="是否启用">
<template slot-scope="scope">
<el-switch
v-model="scope.row.enable"
active-color="#13ce66"
inactive-color="#ff4949"
@change="changeEnable(scope.row)"
:active-value="1"
:inactive-value="0">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="300">
<template slot-scope="scope">
<el-button type="success" class="el-icon-view" @click="viewImage(scope.row.url)">预览</el-button>
<el-button type="primary" class="el-icon-download" @click="download(scope.row.url)" v-if="scope.row.enable === 1">下载</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="handleDelete(scope.row.id)"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="block" style="padding:10px 0;align-content: center;margin-left: 30%;margin-top:30px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 15, 20]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="字典信息" :visible.sync="viewVisible" width="30%">
<img width="100%" :src="dialogImageUrl" alt="图片"/>
</el-dialog>
</div>
</template>
<script>
export default {
name: "Role",
data(){
return{
pageSize:10,
pageNum:1,
name:'',
tableData:[],
dialogImageUrl:'',
dialogFormVisible:false,
viewVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0
}
},
created() {
this.load();
},
methods:{
viewImage(url){
this.dialogImageUrl = url;
this.viewVisible = true;
},
changeEnable(row){
this.request.post('/sysFile/updateEnable',row).then(res => {
if(res.code === '200'){
this.$message.success('更新启用状态成功')
}else{
this.$message.error('更新启用状态失败')
}
})
},
load(){
this.request.get("/sysFile/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
name:this.name,
}
}).then(res => {
this.tableData = res.data.records;
this.total = res.data.total;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.name = '';
this.load();
},
save(){
this.dialogFormVisible = true;
this.form = {};
},
handleAdd(){
this.request.post("/sysFile/save",this.form).then(res => {
if(res.code === '200'){
if(this.form.id){
this.$message.success('编辑成功');
}else{
this.$message.success('新增成功');
}
this.dialogFormVisible = false;
this.load();
}else{
this.$message.error(res.msg)
}
})
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row));
this.dialogFormVisible = true;
},
handleDelete(id){
if(id){
this.request.delete('/sysFile/deleteById/' + id).then(res => {
if(res.code === '200'){
this.$message.success('删除数据成功');
this.handleCalPageNum();
}else{
this.$message.error(res.msg)
}
})
}else{
this.$message.error('没有id信息,无法删除');
}
},
cancel(){
this.$message.success('取消操作成功');
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
deleteBatch(){
//批量删除数据
if(this.multipleSelection.length === 0){
this.$message.warning("请先选择要删除的数据");
return
}
const ids = this.multipleSelection.map(v => v.id);
this.request.post('/sysFile/deleteBatch',ids).then(res => {
if(res.code === '200'){
this.$message.success('批量删除成功');
this.handleCalPageNum();
}else{
this.$message.error(res.msg);
}
})
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
},
handleCalPageNum(){
this.request.get("/sysFile/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
name:this.name,
}
}).then(res => {
this.total = res.data.total;
this.pageNum = (this.total % this.pageSize === 0) ? (this.total / this.pageSize) : Math.floor((this.total / this.pageSize) + 1);
if(this.pageNum < 1){
this.pageNum = 1;
}
this.load();
})
},
handleAvatarSuccess(res, file) {
this.$message.success('上传文件成功!');
this.load();
},
handleAvatarError() {
this.$message.error('上传文件失败!');
},
download(url){
window.open(url);
}
}
}
</script>
<style scoped>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
网页显示图片的配置,
InterceptorConfig类
/** * 映射路径修改:这段代码意思就配置一个拦截器, 如果访问路径是addResourceHandler中的filepath 这个路径 * 那么就 映射到访问本地的addResourceLocations 的参数的这个路径上, * 这样就可以让别人访问服务器的本地文件了,比如本地图片或者本地音乐视频什么的。 * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/sysFile/show/**").addResourceLocations("file:D:\\temp\\files\\"); }
14.文章管理
文章表:
后端代码,参考视频
安装npm依赖包
npm install mavon-editor --s
main.js
// main.js全局注册 import mavonEditor from 'mavon-editor' import 'mavon-editor/dist/css/index.css' // use Vue.use(mavonEditor)
// 绑定@imgAdd event imgAdd(pos, $file) { let $vm = this.$refs.md // 第一步.将图片上传到服务器. const formData = new FormData(); formData.append('file', $file); axios({ url: 'http://localhost:8899/file/upload', method: 'post', data: formData, headers: {'Content-Type': 'multipart/form-data'}, }).then((res) => { // 第二步.将返回的url替换到文本原位置![...](./0) -> ![...](url) $vm.$img2Url(pos, res.data); }) }展示富文本
<mavon-editor class="md" :value="content" :subfield="false" :defaultOpen="'preview'" :toolbarsFlag="false" :editable="false" :scrollStyle="true" :ishljs="true" />富文本编辑
<mavon-editor ref="md" v-model="form.content" :ishljs="true" @imgAdd="imgAdd"/>
前端页面
<template>
<div>
<div>
<!-- 搜索栏-->
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入文章名称" v-model="name" prefix-icon="el-icon-user"></el-input>
<el-input style="width: 200px;margin-right: 20px" placeholder="请输入创建者名称" v-model="user" prefix-icon="el-icon-user"></el-input>
<el-button style="margin-left: 10px;" type="primary" @click="load" class="el-icon-search">搜索</el-button>
<el-button style="margin-left: 10px;" type="warning" @click="reset" class="el-icon-refresh">重置</el-button>
</div>
<div style="margin-top:20px;margin-bottom: 20px;">
<!-- 新增。批量删除-->
<el-button style="margin-right: 10px;" type="success" class="el-icon-plus" @click="save">新增</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="deleteBatch"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">批量删除</el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-style="getRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="name" label="文章名称"></el-table-column>
<el-table-column prop="content" label="文章内容">
<template slot-scope="scope">
<el-button type="primary" class="el-icon-view" @click="view(scope.row.content)">查看内容</el-button>
</template>
</el-table-column>
<el-table-column prop="value" label="创建用户"></el-table-column>
<el-table-column prop="type" label="类型"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="primary" class="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm
confirm-button-text='确定'
cancel-button-text='取消'
icon="el-icon-info"
icon-color="red"
title="确定删除这些数据吗?"
@confirm="handleDelete(scope.row.id)"
@cancel="cancel">
<el-button slot="reference" type="danger" style="margin-left:5px;" class="el-icon-delete">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="block" style="padding:10px 0;align-content: center;margin-left: 30%;margin-top:30px;">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 15, 20]"
:page-size="10"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="文章信息" :visible.sync="dialogFormVisible" width="60%">
<el-form :model="form">
<el-form-item label="文章名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="文章内容" :label-width="formLabelWidth">
<mavon-editor ref="md" v-model="form.content" :ishljs="true" @imgAdd="imgAdd"/>
</el-form-item>
<el-form-item label="类型" :label-width="formLabelWidth">
<el-input v-model="form.type" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="handleAdd">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="文章内容展示" :visible.sync="contentVisible" width="60%">
<el-card>
<mavon-editor
class="md"
:value="content"
:subfield="false"
:defaultOpen="'preview'"
:toolbarsFlag="false"
:editable="false"
:scrollStyle="true"
:ishljs="true"
/>
</el-card>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Article",
data(){
return{
pageSize:10,
pageNum:1,
name:'',
user:'',
content:'',
tableData:[],
dialogFormVisible:false,
contentVisible:false,
form:{},
formLabelWidth: '80px',
multipleSelection:[],
total:0
}
},
created() {
this.load();
},
methods:{
view(content){
this.content = content;
this.contentVisible = true;
},
// 绑定@imgAdd event
imgAdd(pos, $file) {
let $vm = this.$refs.md
// 第一步.将图片上传到服务器.
const formData = new FormData();
formData.append('file', $file);
axios({
url: 'http://localhost:8888/sysFile/upload',
method: 'post',
data: formData,
headers: {'Content-Type': 'multipart/form-data'},
}).then((res) => {
console.log(res)
// 第二步.将返回的url替换到文本原位置![...](./0) -> ![...](url)
$vm.$img2Url(pos, res.data);
})
},
load(){
this.request.get("/article/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
name:this.name,
}
}).then(res => {
this.tableData = res.data.records;
this.total = res.data.total;
})
},
getRowClass({rowIndex,columnIndex}){
if(rowIndex === 0){
return 'background:#ccc'
}
},
reset(){
this.name = '';
this.user = '';
this.load();
},
save(){
this.dialogFormVisible = true;
this.form = {};
},
handleAdd(){
this.request.post("/article/save",this.form).then(res => {
if(res.code === '200'){
if(this.form.id){
this.$message.success('编辑成功');
}else{
this.$message.success('新增成功');
}
this.dialogFormVisible = false;
this.load();
}else{
this.$message.error(res.msg)
}
})
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row));
this.dialogFormVisible = true;
},
handleDelete(id){
if(id){
this.request.delete('/article/deleteById/' + id).then(res => {
if(res.code === '200'){
this.$message.success('删除数据成功');
this.handleCalPageNum();
}else{
this.$message.error(res.msg)
}
})
}else{
this.$message.error('没有id信息,无法删除');
}
},
cancel(){
this.$message.success('取消操作成功');
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
deleteBatch(){
//批量删除数据
if(this.multipleSelection.length === 0){
this.$message.warning("请先选择要删除的数据");
return
}
const ids = this.multipleSelection.map(v => v.id);
this.request.post('/article/deleteBatch',ids).then(res => {
if(res.code === '200'){
this.$message.success('批量删除成功');
this.handleCalPageNum();
}else{
this.$message.error(res.msg);
}
})
},
handleSizeChange(val) {
this.pageSize = val;
this.load();
},
handleCurrentChange(val) {
this.pageNum = val;
this.load();
},
handleCalPageNum(){
this.request.get("/article/findPage",{
params:{
pageNum:this.pageNum,
pageSize:this.pageSize,
name:this.name,
user:this.user,
}
}).then(res => {
this.total = res.data.total;
this.pageNum = (this.total % this.pageSize === 0) ? (this.total / this.pageSize) : Math.floor((this.total / this.pageSize) + 1);
if(this.pageNum < 1){
this.pageNum = 1;
}
this.load();
})
}
}
}
</script>
<style scoped>
</style>
15.公告和轮播图管理
公告表,轮播图表
后端代码
前端页面
看视频操作,代码较多
16.个人信息页面和修改密码页面
参考视频
17.整合Echarts搭建后台首页
Home页面
<template>
<div>
<el-row :gutter="20">
<el-col :span="6">
<el-card>权限项目</el-card>
</el-col>
<el-col :span="6">
<el-card>毕设项目</el-card>
</el-col>
<el-col :span="6">
<el-card>前后端分离</el-card>
</el-col>
<el-col :span="6">
<el-card>脚手架系统</el-card>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<div style="width:400px;height:400px" id="main">
</div>
</el-col>
<el-col :span="8">
<div style="width:400px;height:400px" id="main1">
</div>
</el-col>
<el-col :span="8">
<div style="width:400px;height:400px" id="main2">
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: "Home",
mounted() {
var chartDom = document.getElementById('main');
var myChart = echarts.init(chartDom);
var option;
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}
]
};
var chartDom1 = document.getElementById('main1');
var myChart1 = echarts.init(chartDom1);
var option1;
option1 = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)'
}
}
]
};
var chartDom2 = document.getElementById('main2');
var myChart2 = echarts.init(chartDom2);
var option2;
option2 = {
tooltip: {
trigger: 'item'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
]
}
]
};
this.request.get('/user/echart').then(res => {
if(res.code === '200'){
option.xAxis.data = res.data.list1; //名称的集合
option.series[0].data = res.data.list2; //数值的集合
option && myChart.setOption(option);
option1.xAxis.data = res.data.list1; //名称的集合
option1.series[0].data = res.data.list2; //数值的集合
option1 && myChart1.setOption(option1);
option2.series[0].data = res.data.list3;
option2 && myChart2.setOption(option2);
}
})
}
}
</script>
<style scoped>
</style>
18.前台首页搭建
app.vue中不但可以当做是网站首页,也可以写所有页面中公共需要的动画或者样式。
app.vue是vue页面资源的首加载项,是主组件,页面入口文件,所有页面都是在App.vue下进行切换的;也是整个项目的关键,app.vue负责构建定义及页面组件归集。
index.html---主页,项目入口
App.vue---根组件
main.js---入口文件
Front.vue和Home.vue页面,实现轮播图和项目介绍,头部展示基本信息
19.前台文章列表显示和文章详情
看视频操作,代码较多不复制了
20.前台公告列表和项目总结
看视频操作,代码较多不复制了