前言:创建一个根目录管理前端和后端的项目 比如 E:\javaStudy\SOFAB2
gitee仓库地址[项目完整代码都在里面了]:SOFAB: 权限管理系统
搭建后端系统架构
1.创建jd-admin项目
2.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> <groupId>com.jd</groupId> <artifactId>jd-admin</artifactId> <version>0.0.1-SNAPSHOT</version> <name>jd-admin</name> <description>jd-admin</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.2.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--mp启动器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.40</version> </dependency> <!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--redis缓存--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--lettuce poll 缓存连接池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--hutool工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.3</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
3.在resources目录下创建application.yml文件
server: port: 81 servlet: context-path: / spring: datasource: #mysql数据连接池配置 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/db_admin?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC username: root password: yr redis: #redis配置 host: 192.168.200.130 #ip port: 6379 #端口 password: yr #密码 lettuce: #lettuce redis客户端配置 pool: #连接池配置 max-active: 8 #连接池最大连接数[使用负值表示没有限制] 默认 8 max-wait: 200s #连接池最大阻塞等待事件[使用负值表示没有限制] 默认 -1 max-idle: 8 #连接池中的最大空闲连接 默认 8 min-idle: 0 #连接池中最小空闲连接 默认 0 mybatis-plus: global-config: db-config: id-type: auto #主键自增 configuration: map-underscore-to-camel-case: true #开启下划线到驼峰命名法的自动映射 auto-mapping-behavior: full #置自动映射行为为 full:会自动映射所有的结果集,无论是否有嵌套结果集映射 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志输出 mapper-locations: classpath:mapper/*.xml #XXXMapper.xml文件存放路径
4.注意redis要启动 且ip是192.168.200.130[我是把redis安装在了linux虚拟机上 可以使用ip address查看ip]
5.创建数据库和表
# 新建数据库 db_admin
CREATE DATABASE jd_admin;
USE jd_admin;# 新建用户表
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户Id',
username VARCHAR(100) DEFAULT NULL COMMENT '用户名',
`password` VARCHAR(100) DEFAULT NULL COMMENT '密码',
`avatar` VARCHAR(255) DEFAULT 'default.jpg' COMMENT '用户头像',
`email` VARCHAR(100) DEFAULT '' COMMENT '用户邮箱',
`phonenumber` VARCHAR(11) DEFAULT '' COMMENT '手机号码',
`login_date` DATETIME DEFAULT NULL COMMENT '最后登录时间',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1禁用)',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注'
)ENGINE=INNODB CHARSET = utf8;#测试数据
INSERT INTO
`sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`)
VALUES
(1,'tom','$2a$10$Kib4zuVhTzg3I1CoqJfd0unuY9G9ysI7cfbhyT3fi7k7Z/4pr3bGW','20220727112556000000325.jpg','caofeng4017@126.com','18862857417','2022-08-29 22:10:52','0','2022-06-09 08:47:52','2022-06-22 08:47:54','备注'),
(2,'common','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','222.jpg','','','2022-08-22 21:34:39','0',NULL,NULL,NULL),
(3,'test','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','333.jpg','','','2022-07-24 17:36:07','0',NULL,NULL,NULL),
(4,'1','$2a$10$lD0Fx7oMsFFmX9hVkmYy7eJteH8pBaXXro1X9DEMP5sbM.Z6Co55m','default.jpg','','',NULL,'1',NULL,NULL,NULL),
(5,'2',NULL,'default.jpg','','',NULL,'1',NULL,NULL,NULL),
(15,'fdsfs','$2a$10$AQVcp4hQ7REc5o7ztVnI7eX.sJdcYy3d1x2jm5CfrcCoMZMPacfpi','default.jpg','fdfa4@qq.com','18862851414','2022-08-02 02:22:45','1','2022-08-02 02:21:24','2022-08-01 18:23:16','fdfds4'),
(28,'sdfss2','$2a$10$7aNJxwVmefI0XAk64vrzYuOqeeImYJUQnoBrtKP9pLTGTWO2CXQ/y','default.jpg','dfds3@qq.com','18862857413',NULL,'1','2022-08-07 00:42:46','2022-08-06 16:43:04','ddd33'),
(29,'ccc','$2a$10$7cbWeVwDWO9Hh3qbJrvTHOn0E/DLYXxnIZpxZei0jY4ChfQbJuhi.','20220829080150000000341.jpg','3242@qq.com','18862584120','2022-08-29 19:52:27','0','2022-08-29 17:04:58',NULL,'xxx'),
(30,'ccc666','$2a$10$Tmw5VCM/K2vb837AZDYHQOqE3gPiRZKevxLsh/ozndpTSjdwABqaK','20220829100454000000771.jpg','fdafds@qq.com','18865259845','2022-08-29 22:05:18','0','2022-08-29 22:00:39',NULL,'ccc');
6.使用MybatisX插件快速生成 entity、mapper、service
此时便可以看到该库下的表和字段了
好啦生成完毕!
7.创建封装返回json的数据的通用类
package com.yr.java123admin.entity; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * @author java大帝 * @ClassName R * @description 封装返回参数的类,用于统一处理应用程序的响应结果。 * 该类包含了消息、状态码和扩展数据等属性,以及一系列静态方法用于创建不同状态的响应对象。 * @date 2025年01月13日 * @version 1.0 */ @Data public class R { // 消息 private String msg; // 状态码 private Integer code; // 数据 private Map<String, Object> data = new HashMap<>(); /** * 创建一个表示成功的R对象,默认消息为 "success",状态码为 200。 * * @return 表示成功的R对象。 */ public static R success() { R r = new R(); r.setMsg("success"); r.setCode(200); return r; } /** * 创建一个表示成功的R对象,自定义成功消息,状态码为 200。 * * @param msg 自定义的成功消息。 * @return 表示成功的R对象。 */ public static R success(String msg) { R r = new R(); r.setMsg(msg); r.setCode(200); return r; } /** * 创建一个表示成功的R对象,带有扩展数据,默认消息为 "success",状态码为 200。 * * @param extend 包含扩展数据的Map。 * @return 表示成功的R对象。 */ public static R success(Map<String, Object> extend) { R r = new R(); r.setMsg("success"); r.setCode(200); r.setData(extend); return r; } /** * 创建一个表示失败的R对象,默认消息为 "未知异常 请联系管理员!",状态码为 500。 * * @return 表示失败的R对象。 */ public static R fail() { R r = new R(); r.setMsg("未知异常 请联系管理员!"); r.setCode(500); return r; } /** * 创建一个表示失败的R对象,自定义失败消息,状态码为 500。 * * @param msg 自定义的失败消息。 * @return 表示失败的R对象。 */ public static R fail(String msg) { R r = new R(); r.setMsg(msg); r.setCode(500); return r; } public static R fail(Integer code,String msg) { R r = new R(); r.setMsg(msg); r.setCode(code); return r; } public R put(String key, Object val) { this.data.put(key, val); return this; } }
8.创建controlelr.TestController
package com.yr.java123admin.controller; import com.yr.java123admin.entity.R; import com.yr.java123admin.entity.SysUser; import com.yr.java123admin.service.SysUserService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; /** * @author java大帝 * @ClassName TestController * @description: 测试包 * @date 2025年 01月 13日 * @version: 1.0 */ @RestController @RequestMapping("/test") public class TestController { @Resource private SysUserService sysUserService; @RequestMapping("list") public R userList(){ HashMap<String, Object> resultMap = new HashMap<>(); List<SysUser> list = sysUserService.list(); resultMap.put("userList",list); return R.success(resultMap); } }
9.修改一下启动类并使用浏览器测试
package com.jd.jdadmin; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.jd.jdadmin.mapper") //扫描XXXMapper类 public class JdAdminApplication { public static void main(String[] args) { SpringApplication.run(JdAdminApplication.class, args); } }
搭建前端系统架构
1.使用命令创建vue3
2.使用idea打开SOFAB2
3.启动前端项目
使用浏览器访问
4.安装axios和elementPlus依赖
5.测试elementPlus是否安装成功
在main.ts中引入
import ElementPlus from 'element-plus' import 'element-plus/dist/index.css'app.use(ElementPlus)
在App.vue中使用elementPlus的按钮组件
<template> <div> <el-button type="danger">猜猜我是谁</el-button> </div> </template> <script setup lang="ts"> </script> <style scoped> </style>
浏览器访问
JWT前后端交互
Json Web Token(jwt),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)
JWT就是一段字符串,用来进行用户身份认证的凭证,该字符串分成三段[头部、载荷、凭证]
1.创建common.constant包 并创建JwtConstant类
package com.jd.jdadmin.common.constant; /** * @author java大帝 * @ClassName JwtConstant * @description: 常量类 * @date 2025年 01月 13日 * @version: 1.0 */ public class JwtConstant { /*token*/ public static final int JWT_ERRCODE_NULL = 4000; // token不存在 public static final int JWT_ERRCODE_EXPIRE = 4001; // token过期 public static final int JWT_ERRCODE_FAIL = 4002; // 认证不通过 /*jwt*/ public static final String JWT_SECERT = "1ba@842as45*74s?1p41-15a*s/dj;"; // 密钥 public static final long JWT_TTL = 24* 60 * 60 * 1000; // 24小时 }
2.在entity包下创建CheckResult类
package com.jd.jdadmin.entity; import io.jsonwebtoken.Claims; /** * @author java大帝 * @ClassName CheckResult * @description: 认证结果 * @date 2025年 01月 13日 * @version: 1.0 */ public class CheckResult { private int errCode; private boolean success; private Claims claims; public int getErrCode() { return errCode; } public void setErrCode(int errCode) { this.errCode = errCode; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public Claims getClaims() { return claims; } public void setClaims(Claims claims) { this.claims = claims; } }
3.创建util包 并创建JwtUtils类
package com.jd.jdadmin.util; import cn.hutool.core.codec.Base64; import com.jd.jdadmin.common.constant.JwtConstant; import com.jd.jdadmin.entity.CheckResult; import io.jsonwebtoken.*; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Date; /** * @author java大帝 * @ClassName JwtUtils * @description: Jwt工具类 * @date 2025年 01月 13日 * @version: 1.0 */ public class JwtUtils { // 生成JWT public static String createJWT(String id,String subject,long ttlMillis){ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); SecretKey secretKey = generalKey(); JwtBuilder jwtBuilder = Jwts.builder() .setId(id) .setSubject(subject) // 主体 .setIssuer("yr") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey);// 签名算法以及密钥 if (ttlMillis >= 0){ long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); jwtBuilder.setExpiration(expDate); //设置过期时间 } return jwtBuilder.compact(); } // 生成jwt token public static String genJwtToken(String username){return createJWT(username,username,60 * 60 * 1000);} // 验证JWT public static CheckResult validateJWT(String jwtStr){ CheckResult checkResult = new CheckResult(); Claims claims = null; try { claims = parseJWT(jwtStr); checkResult.setSuccess(true); checkResult.setClaims(claims); } catch (ExpiredJwtException e) { checkResult.setErrCode(JwtConstant.JWT_ERRCODE_EXPIRE); checkResult.setSuccess(false); } catch (SignatureException e){ checkResult.setErrCode(JwtConstant.JWT_ERRCODE_FAIL); checkResult.setSuccess(false); } catch (Exception e) { checkResult.setErrCode(JwtConstant.JWT_ERRCODE_FAIL); checkResult.setSuccess(false); } return checkResult; } // 生成加密key public static SecretKey generalKey(){ byte[] encodeKey = Base64.decode(JwtConstant.JWT_SECERT); SecretKeySpec key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES"); return key; } // 解析JWT字符串 public static Claims parseJWT(String jwt){ SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } public static void main(String[] args) { // 小明失效 10s String sc = createJWT("1", "小明", 60 * 60 * 1000); System.out.println(sc); System.out.println(validateJWT(sc).getErrCode()); System.out.println(validateJWT(sc).getClaims().getId()); System.out.println(validateJWT(sc).getClaims().getSubject()); System.out.println(validateJWT(sc).getClaims()); } }
测试一下
3.发请求测试login登录返回token
3.1后端:创建测试类controller.TestController
@RestController @RequestMapping("/test") public class TestController { @Resource private SysUserService sysUserService; @RequestMapping("list") public R userList(@RequestHeader(required = false) String token){ if (StrUtil.isNotEmpty(token)){ HashMap<String, Object> resultMap = new HashMap<>(); List<SysUser> list = sysUserService.list(); resultMap.put("userList",list); return R.success(resultMap); } return R.fail(401,"无权访问"); } @RequestMapping("login") public R login(){ String token = JwtUtils.genJwtToken("java123456"); return R.success().put("token",token); } }
3.2 后端:创建util.StrUtil字符串的工具类
package com.jd.jdadmin.util; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * @author java大帝 * @ClassName StrUtil * @description: 字符串工具类 * @date 2025年 01月 13日 * @version: 1.0 */ public class StrUtil { /** * 判断是否是空 * @param str * @return */ public static boolean isEmpty(String str){ if(str==null||"".equals(str.trim())){ return true; }else{ return false; } } /** * 判断是否不是空 * @param str * @return */ public static boolean isNotEmpty(String str){ if((str!=null)&&!"".equals(str.trim())){ return true; }else{ return false; } } /** * 格式化模糊查询 * @param str * @return */ public static String formatLike(String str){ if(isNotEmpty(str)){ return "%"+str+"%"; }else{ return null; } } /** * 过滤掉集合里的空格 * @param list * @return */ public static List<String> filterWhite(List<String> list){ List<String> resultList=new ArrayList<String>(); for(String l:list){ if(isNotEmpty(l)){ resultList.add(l); } } return resultList; } /** * 去除html标签 */ public static String stripHtml(String content) { // <p>段落替换为换行 content = content.replaceAll("<p .*?>", "\r\n"); // <br><br/>替换为换行 content = content.replaceAll("<br\\s*/?>", "\r\n"); // 去掉其它的<>之间的东西 content = content.replaceAll("\\<.*?>", ""); // 去掉空格 content = content.replaceAll(" ", ""); return content; } /** * 生成六位随机数 * @return */ public static String genSixRandomNum(){ Random random = new Random(); String result=""; for (int i=0;i<6;i++) { result+=random.nextInt(10); } return result; } /** * 生成由[A-Z,0-9]生成的随机字符串 * @param length 欲生成的字符串长度 * @return */ public static String getRandomString(int length){ Random random = new Random(); StringBuffer sb = new StringBuffer(); for(int i = 0; i < length; ++i){ int number = random.nextInt(2); long result = 0; switch(number){ case 0: result = Math.round(Math.random() * 25 + 65); sb.append(String.valueOf((char)result)); break; case 1: sb.append(String.valueOf(new Random().nextInt(10))); break; } } return sb.toString(); } }
3.3前端:二次封装axios
// 引入axios import axios from 'axios' import {ElMessage} from "element-plus" import useUserStore from "@/stores/modules/user" //基础路径 export let baseUrl = 'http://localhost:81/' const request = axios.create({ baseURL:baseUrl, // 基础路径 timeout:5000 // 超时时间 }) // 请求拦截器 request.interceptors.request.use(config => { let userStore = useUserStore() // 在发送请求之前做些什么 比如携带token请求头 config.headers.token = userStore.token return config },err => { return Promise.reject(err) }) // 响应拦截器 request.interceptors.response.use(resp => { console.log(resp) return resp.data },err => { //错误处理 let message = '' let status = err.response.status switch (status){ case 401: message = 'token过期' break case 403: message = '无权访问' break case 404: message='请求地址有误' break case 500: message = '服务器出错' break default: message = '网络错误' break } ElMessage({ type:'error', message }) }) export default request
3.4前端:创建用户相关小仓库
大仓库
import {createPinia} from "pinia" let pinia = createPinia() export default pinia
在main.ts中引入大仓库并使用
import pinia from "@/stores" app.use(pinia)
小仓库
import { defineStore } from "pinia" import request from "@/util/request" import {GET_TOKEN, SET_TOKEN} from "@/util/token"; const useUserStore = defineStore('User',{ state(){ return { token:GET_TOKEN() } }, actions:{ async userLogin(){ let res = await request.get('test/login') if (res.code == 200){ SET_TOKEN(res.data.token) return 'ok' } else { return Promise.reject(res.msg) } } } }) export default useUserStore
3.5 创建token工具类
export const SET_TOKEN = (token:string)=> { localStorage.setItem("TOKEN",token) } export const GET_TOKEN = ()=> { return localStorage.getItem("TOKEN") } export const REMOVE_TOKEN = ()=> { return localStorage.removeItem("TOKEN") }
3.6App.vue中写相关测试代码
<template> <div> <el-button type="danger" @click="handleLogin">测试登录</el-button> <el-button type="primary" @click="handleUserList">测试获取用户请求</el-button> </div> </template> <script setup lang="ts"> import useUserStore from "@/stores/modules/user" import request from "@/util/request" let userStore = useUserStore() const handleLogin = async () => { await userStore.userLogin() } const handleUserList = async () => { let res = await request.get('test/list') if (res.code == 200) { console.log('userlist',res) } } </script> <style scoped> </style>
3.7浏览器访问出现跨域问题
3.8创建config.WebAppConfigurer解决跨域
package com.jd.jdadmin.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author java大帝 * @ClassName WebAppConfigurer * @description: 跨域解决 * @date 2025年 01月 13日 * @version: 1.0 */ @Configuration public class WebAppConfigurer implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry .addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("GET","HEAD","POST","DELETE","OPTIONS") .maxAge(3600); } }
测试成功
登录功能实现
前端静态登录页面实现
1.准备两个样式文件
border.css
@charset "utf-8"; .border, .border-top, .border-right, .border-bottom, .border-left, .border-topbottom, .border-rightleft, .border-topleft, .border-rightbottom, .border-topright, .border-bottomleft { position: relative; } .border::before, .border-top::before, .border-right::before, .border-bottom::before, .border-left::before, .border-topbottom::before, .border-topbottom::after, .border-rightleft::before, .border-rightleft::after, .border-topleft::before, .border-topleft::after, .border-rightbottom::before, .border-rightbottom::after, .border-topright::before, .border-topright::after, .border-bottomleft::before, .border-bottomleft::after { content: "\0020"; overflow: hidden; position: absolute; } /* border * 因,边框是由伪元素区域遮盖在父级 * 故,子级若有交互,需要对子级设置 * 定位 及 z轴 */ .border::before { box-sizing: border-box; top: 0; left: 0; height: 100%; width: 100%; border: 1px solid #eaeaea; transform-origin: 0 0; } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { left: 0; width: 100%; height: 1px; } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { top: 0; width: 1px; height: 100%; } .border-top::before, .border-topbottom::before, .border-topleft::before, .border-topright::before { border-top: 1px solid #eaeaea; transform-origin: 0 0; } .border-right::before, .border-rightbottom::before, .border-rightleft::before, .border-topright::after { border-right: 1px solid #eaeaea; transform-origin: 100% 0; } .border-bottom::before, .border-topbottom::after, .border-rightbottom::after, .border-bottomleft::before { border-bottom: 1px solid #eaeaea; transform-origin: 0 100%; } .border-left::before, .border-topleft::after, .border-rightleft::after, .border-bottomleft::after { border-left: 1px solid #eaeaea; transform-origin: 0 0; } .border-top::before, .border-topbottom::before, .border-topleft::before, .border-topright::before { top: 0; } .border-right::before, .border-rightleft::after, .border-rightbottom::before, .border-topright::after { right: 0; } .border-bottom::before, .border-topbottom::after, .border-rightbottom::after, .border-bottomleft::after { bottom: 0; } .border-left::before, .border-rightleft::before, .border-topleft::after, .border-bottomleft::before { left: 0; } @media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx) { /* 默认值,无需重置 */ } @media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49), (min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49), (min-resolution: 144dpi) and (max-resolution: 239dpi), (min-resolution: 1.5dppx) and (max-resolution: 2.49dppx) { .border::before { width: 200%; height: 200%; transform: scale(.5); } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { transform: scaleY(.5); } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { transform: scaleX(.5); } } @media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5), (min-device-pixel-ratio: 2.5), (min-resolution: 240dpi), (min-resolution: 2.5dppx) { .border::before { width: 300%; height: 300%; transform: scale(.33333); } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { transform: scaleY(.33333); } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { transform: scaleX(.33333); } }
reset.css
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } html{ font-size: 12px; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; }
2.准备好背景图片
网址:The night sky over a mountain range with stars in the sky photo – Free Chamonix Image on Unsplash
3. 在main.ts中引入两个样式文件
// 引入样式文件 import '@/assets/styles/border.css' import '@/assets/styles/reset.css'
4. 修改router/index.ts
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: () => import('@/views/Home.vue') }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') } ] }) export default router
5.创建Login.vue
<script setup> const handleLogin = () => { } </script> <template> <div class="login"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">java大帝 Vue3 后台管理系统</h3> <el-form-item prop="username"> <el-input type="text" size="large" auto-complete="off" placeholder="账号" > </el-input> </el-form-item> <el-form-item prop="password"> <el-input type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" show-password > </el-input> </el-form-item> <el-checkbox style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-form-item style="width:100%;"> <el-button size="large" type="primary" style="width:100%;" @click.prevent="handleLogin" > <span>登 录</span> </el-button> </el-form-item> </el-form> <!-- 底部 --> <div class="el-login-footer"> <span>Copyright © 2013-2022 <a href="http://www.java1234.vip" target="_blank">java大帝.vip</a> 版权所有.</span> </div> </div> </template> <style lang="scss" scoped> a{ color:white } .login { display: flex; justify-content: center; align-items: center; height: 100%; background-image: url("../assets/images/bg.jpg"); background-size: cover; } .title { margin: 0px auto 30px auto; text-align: center; color: #707070; } .login-form { border-radius: 6px; background: #ffffff; width: 400px; padding: 25px 25px 5px 25px; .el-input { height: 40px; input { display: inline-block; height: 40px; } } .input-icon { height: 39px; width: 14px; margin-left: 0px; } } .login-tip { font-size: 13px; text-align: center; color: #bfbfbf; } .login-code { width: 33%; height: 40px; float: right; img { cursor: pointer; vertical-align: middle; } } .el-login-footer { height: 40px; line-height: 40px; position: fixed; bottom: 0; width: 100%; text-align: center; color: #fff; font-family: Arial; font-size: 12px; letter-spacing: 1px; } .login-code-img { height: 40px; padding-left: 12px; } </style>
6.安装scss
npm install sass sass-loader --save-dev
7.修改App.vue
<template> <router-view /> </template> <script setup lang="ts"> </script> <style> html,body,#app { height: 100%; } .app-container { padding: 20px; } </style>
8.浏览器访问
全局注册Element-Plus的图标组件
1.安装element-plus icon
npm install @element-plus/icons-vue
2.将其注册为全局组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue' export default { install(app:any){ //@ts-ignore for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } } }
3.在main.ts中引入钩子并使用
// 引入钩子 import globalComponent from '@/components/index' app.use(globalComponent)
4.在Login.vue的表单项中使用
<el-form-item prop="username"> <el-input prefix-icon="User" type="text" size="large" auto-complete="off" placeholder="账号" > </el-input> </el-form-item> <el-form-item prop="password"> <el-input prefix-icon="Lock" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" show-password > </el-input> </el-form-item>
SpringSecurity执行原理概述
SpringSecurity简单原理
SpringSecurity有很多很多的拦截器 在执行流程里面主要有两个核心的拦截器
1.登录验证拦截器 AuthenticationProcessingFilter
2.资源管理拦截器AbstractSecurityInterceptor
但拦截器里面的实现需要一些组件来实现 所以就有了AuthenticationManager认证管理器、accessDecisionManager决策管理器等组件来支撑
FilterChainProxy是一个代理 真正起作用的是各个Filter,这些Filter作为Bean被Spring管理,是Spring Security的核心,各有各的职责 不直接处理认证和授权,交由认证管理器和决策管理器处理!
大概流程
认证管理
流程图解读:
1.用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类.
2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证.
3.认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息、身份信息、细节信息,但密码通常会被移除)Authentication实例.
4.SecurityContextHolder安全上下文容器将第三步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(...)方法 设置到其中,可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager,而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的,web表单对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取,最终AuthenticationProvider将UserDetails填充至Authentication
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过Spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票否定、一票决定、少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。
详细执行流程
项目整合SpringSecurity
1.在pom.xml中加入SpringSecurity依赖
<!--springsecurity--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2.创建config.SecurityConfig类
package com.jd.jdadmin.config; 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; /** * @author java大帝 * @ClassName SecurityConfig * @description: TODO * @date 2025年 01月 14日 * @version: 1.0 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用了基于方法的安全性控制 public class SecurityConfig extends WebSecurityConfigurerAdapter { private static final String URI_WHITELIST[] = { "/login", "/logout", "captcha", "/password", "/image/**", "/test/**" }; @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置 http .cors() .and() .csrf() .disable() .formLogin() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(URI_WHITELIST) // 白名单 .permitAll() .anyRequest() .authenticated(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); // 使用默认的用户名和密码[内置] } }
3.启动测试 此时浏览器访问http://localhost:81/test/list会报错 因为必须先登录
控制台会打印出密码
重写登录成功和登录失败处理器
1.创建common.security包 和 LoginSuccessHandler类
package com.jd.jdadmin.common.security; import cn.hutool.json.JSONUtil; import com.jd.jdadmin.entity.R; import com.jd.jdadmin.util.JwtUtils; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 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; /** * @author java大帝 * @ClassName LoginSuccessHandler * @description: 登录成功后的处理器 * @date 2025年 01月 14日 * @version: 1.0 */ @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // 设置返回的内容格式 httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); // 目前使用SpringSecurity生成的user用户名 String username = "user"; String token = JwtUtils.genJwtToken(username); outputStream.write(JSONUtil.toJsonStr(R.success().put("authorization",token)).getBytes()); outputStream.flush(); outputStream.close(); } }
2.创建security.LoginFailureHandler类
package com.jd.jdadmin.common.security; import cn.hutool.json.JSONUtil; import com.jd.jdadmin.entity.R; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; 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; /** * @author java大帝 * @ClassName LoginSuccessHandler * @description: 登录失败后的处理器 * @date 2025年 01月 14日 * @version: 1.0 */ @Component public class LoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // 设置返回的内容格式 httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); String message = e.getMessage(); if (e instanceof BadCredentialsException){ message = "用户名或密码错误!"; } outputStream.write(JSONUtil.toJsonStr(R.fail(message)).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); } }
3.在 SecurityConfig类中将登录成功和失败的处理器配置进去
package com.jd.jdadmin.config; import com.jd.jdadmin.common.security.LoginFailureHandler; import com.jd.jdadmin.common.security.LoginSuccessHandler; 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 javax.annotation.Resource; /** * @author java大帝 * @ClassName SecurityConfig * @description: TODO * @date 2025年 01月 14日 * @version: 1.0 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用了基于方法的安全性控制 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private LoginSuccessHandler loginSuccessHandler; @Resource private LoginFailureHandler loginFailureHandler; ... @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置 http .cors() ... .formLogin() .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) ...; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); } }
4.前台:创建api统一管理项目接口
创建user文件夹 存储用户相关API接口
在user文件夹下创建index.ts和type.ts
index.ts
// 用户相关的API接口 import request from "@/util/request" import type {UserFormData, UserLoginResponseData} from "@/api/user/type" enum API { LOGIN_URL = 'login' } export const reqUserLogin = (data:UserFormData) => request.post<any,UserLoginResponseData>(API.LOGIN_URL + `?username=${data.username}&password=${data.password}`)
type.ts
export interface ResponseData { msg:string code:number } export interface UserFormData { username:string, password:string } export interface UserLoginResponseData extends ResponseData { data:{ authorization:string } }
5.在仓库中发登录请求[存储token]
import { defineStore } from "pinia" import {GET_TOKEN, SET_TOKEN} from "@/util/token"; import {reqUserLogin} from "@/api/user"; import type {UserFormData} from "@/api/user/type"; const useUserStore = defineStore('User',{ state(){ return { token:GET_TOKEN() } }, actions:{ async userLogin(data:UserFormData){ let res = await reqUserLogin(data) if (res.code == 200){ this.token = res.data.authorization SET_TOKEN(res.data.authorization) // 存储token return 'ok' } else { return Promise.reject(res.msg) } } } }) export default useUserStore
6. 修改Login.vue页面 测试登录请求
<script setup> import { ref,reactive } from 'vue' import useUserStore from "@/stores/modules/user" import {ElMessage} from "element-plus"; import router from "@/router" import Cookies from "js-cookie" import { encrypt, decrypt } from "@/util/jsencrypt" let userStore = useUserStore() // el-form组件实例 let loginRef = ref() // 收集表单数据 let loginForm = reactive({ username:'', password:'', rememberMe:false }) // 验证对象 let loginRules = reactive({ username:[{required:true,trigger:'blur',message:'请输入您的账号'}], password:[{required:true,trigger:'blur',message:'请输入您的密码'}] }) const handleLogin = async () => { await loginRef.value.validate() //勾选了需要记住密码 设置在cookie中设置记住用户名和密码 if (loginForm.rememberMe){ Cookies.set('username',loginForm.username,{ expires: 30 }) Cookies.set('password',encrypt(loginForm.password),{ expires: 30 }) Cookies.set('rememberMe',loginForm.rememberMe,{ expires: 30 }) }else { // 否则删除 Cookies.remove('username') Cookies.remove('password') Cookies.remove('rememberMe') } try { await userStore.userLogin(loginForm) console.log(1111) router.push('/') } catch (e) { ElMessage.error("登录失败!") } } function getCookie() { const username = Cookies.get("username"); const password = Cookies.get("password"); const rememberMe = Cookies.get("rememberMe"); Object.assign(loginForm, { username: username === undefined ? loginForm.username : username, password: password === undefined ? loginForm.password : decrypt(password), rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) }); } // 获取cookie getCookie(); </script> <template> <div class="login"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">java大帝 Vue3 后台管理系统</h3> <el-form-item prop="username"> <el-input v-model="loginForm.username" prefix-icon="User" type="text" size="large" auto-complete="off" placeholder="账号" > </el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" prefix-icon="Lock" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" show-password > </el-input> </el-form-item> <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-form-item style="width:100%;"> <el-button size="large" type="primary" style="width:100%;" @click.prevent="handleLogin" > <span>登 录</span> </el-button> </el-form-item> </el-form> <!-- 底部 --> <div class="el-login-footer"> <span>Copyright © 2013-2022 <a href="https://blog.csdn.net/2302_80480374/article/details/145109271?spm=1001.2014.3001.5502" target="_blank">java大帝.vip</a> 版权所有.</span> </div> </div> </template> <style lang="scss" scoped> a{ color:white } .login { display: flex; justify-content: center; align-items: center; height: 100%; background-image: url("../assets/images/bg.jpg"); background-size: cover; } .title { margin: 0px auto 30px auto; text-align: center; color: #707070; } .login-form { border-radius: 6px; background: #ffffff; width: 400px; padding: 25px 25px 5px 25px; .el-input { height: 40px; input { display: inline-block; height: 40px; } } .input-icon { height: 39px; width: 14px; margin-left: 0px; } } .login-tip { font-size: 13px; text-align: center; color: #bfbfbf; } .login-code { width: 33%; height: 40px; float: right; img { cursor: pointer; vertical-align: middle; } } .el-login-footer { height: 40px; line-height: 40px; position: fixed; bottom: 0; width: 100%; text-align: center; color: #fff; font-family: Arial; font-size: 12px; letter-spacing: 1px; } .login-code-img { height: 40px; padding-left: 12px; } </style>
7.浏览器测试
用户登录SpringSecurity查库实现
1.创建security.MyUserDetailsServiceImpl类
package com.jd.jdadmin.common.security; import com.jd.jdadmin.common.exception.UserCountLockException; import com.jd.jdadmin.entity.SysUser; import com.jd.jdadmin.service.SysUserService; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; 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 javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * @author java大帝 * @ClassName MyUserDetailsServiceImpl * @description: 自定义UserDetails * @date 2025年 01月 14日 * @version: 1.0 */ @Service public class MyUserDetailsServiceImpl implements UserDetailsService { @Resource private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.getByUsername(username); if (sysUser == null){ throw new UsernameNotFoundException("用户名或密码错误"); }else if ("1".equals(sysUser.getStatus())){ throw new UserCountLockException("该用户账号被封禁,具体请联系管理员"); } // 用户名 密码 权限 return new User(sysUser.getUsername(),sysUser.getPassword(),getUserAuthority()); } private List<GrantedAuthority> getUserAuthority() { return new ArrayList<>(); } }
2.创建common.exception.UserCountLockException异常类
package com.jd.jdadmin.common.exception; import org.springframework.security.core.AuthenticationException; /** * @author java大帝 * @ClassName UserCountLockException * @description: 账号被禁用异常 * @date 2025年 01月 14日 * @version: 1.0 */ public class UserCountLockException extends AuthenticationException { public UserCountLockException(String msg, Throwable t) { super(msg, t); } public UserCountLockException(String msg) { super(msg); } }
3.创建common.exception.GlobalExceptionHandler全局异常处理类
package com.jd.jdadmin.common.exception; import com.jd.jdadmin.entity.R; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * @author java大帝 * @ClassName GlobalException * @description: 全局异常处理 * @date 2025年 01月 14日 * @version: 1.0 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = RuntimeException.class) public R handler(RuntimeException e){ log.error("运行时异常:-----------------{}",e.getMessage()); return R.fail(e.getMessage()); } }
4.在SysUserService中新增根据用户名查询SysUser方法
impl实现
@Override public SysUser getByUsername(String username) { return getOne(new QueryWrapper<SysUser>().eq("username",username)); }
5.在SecurityConfig中装配MyUserDetailsService类
@Resource private MyUserDetailsServiceImpl myUserDetailsService; // 加密因为密码就是使用的BCryptPasswordEncoder 加密存储的 @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService); }
6.重新启动 浏览器测试
实现JWT认证过滤器
1.创建JwtAuthenticationFilter
package com.jd.jdadmin.common.security; import com.jd.jdadmin.common.constant.JwtConstant; import com.jd.jdadmin.entity.CheckResult; import com.jd.jdadmin.entity.SysUser; import com.jd.jdadmin.service.SysUserService; import com.jd.jdadmin.util.JwtUtils; import com.jd.jdadmin.util.StrUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.annotation.Resource; 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.Arrays; /** * @author java大帝 * @ClassName JwtAuthenticationFilter * @description: Jwt认证 * @date 2025年 01月 14日 * @version: 1.0 */ public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Resource private SysUserService sysUserService; @Resource private MyUserDetailsServiceImpl myUserDetailsService; private static final String URI_WHITELIST[] = { "/login", "/logout", "captcha", "/password", "/image/**" }; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 1.获取token请求头 String token = request.getHeader("token"); System.out.println("请求url" + request.getRequestURI()); // 2.判断token为空或uri在白名单里面 则放行 if (StrUtil.isEmpty(token) || new ArrayList<String>(Arrays.asList(URI_WHITELIST)).contains(request.getRequestURI())) { chain.doFilter(request,response); return; } // 3.否则验证token CheckResult checkResult = JwtUtils.validateJWT(token); if (checkResult.isSuccess()){ switch (checkResult.getErrCode()){ case JwtConstant.JWT_ERRCODE_NULL: throw new JwtException("Token 不存在"); case JwtConstant.JWT_ERRCODE_FAIL: throw new JwtException("Token 验证不通过"); case JwtConstant.JWT_ERRCODE_EXPIRE: throw new JwtException("Token 过期"); } } // 获取主体信息 Claims claims = JwtUtils.parseJWT(token); String username = claims.getSubject(); SysUser sysUser = sysUserService.getByUsername(username); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username,null,myUserDetailsService.getUserAuthority()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);// 将新创建的认证对象放入 SecurityContextHolder中,这样在整个请求链中都可以访问到当前认证的信息。 chain.doFilter(request,response); } }
2.在SecurityConfig类中装配
@Bean public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { return new JwtAuthenticationFilter(authenticationManager()); } @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置 http ... .and() .addFilter(jwtAuthenticationFilter()) // 过滤器 ; }
实现JWT认证异常处理器
1.新建security.JwtAuthenticationEntryPoint类
package com.jd.jdadmin.common.security; import cn.hutool.json.JSONUtil; import com.jd.jdadmin.entity.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; /** * @author java大帝 * @ClassName JwtAuthenticationEntryPoint * @description: JWT自定义认证失败后的处理 * @date 2025年 01月 14日 * @version: 1.0 */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // 设置返回的内容格式 httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); outputStream.write(JSONUtil.toJsonStr(R.fail(HttpServletResponse.SC_UNAUTHORIZED,"认证失败请登录!")).getBytes()); outputStream.flush(); outputStream.close(); } }
2.在SecurityConfig中装配并设置
private static final String URI_WHITELIST[] = { "/login", "/logout", "captcha", "/password", "/image/**" }; @Resource private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置、自定义过滤器配置 http ... .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) ... ; }
3.前端:修改一下Login.vue 实现登录成功后去往首页
3.1创建src/layout文件夹 并新建index.vue
<script setup lang="ts"> import request from "@/util/request" const testHandler = async () => { let res = await request.get('test/list') console.log(res) } </script> <template> <div>首页</div> <el-button type="danger" @click="testHandler">测试接口</el-button> </template> <style scoped> </style>
3.2在router/index.ts中配置
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: '首页', component: () => import('@/layout/index.vue') }, { path: '/login', name: 'login', component: () => import('@/views/Login.vue') } ] }) export default router
3.3修改views/Login.vue页面
<script setup> import { ref,reactive } from 'vue' import useUserStore from "@/stores/modules/user" import {ElMessage} from "element-plus"; import router from "@/router" let userStore = useUserStore() // el-form组件实例 let loginRef = ref() // 收集表单数据 let loginForm = reactive({ username:'', password:'' }) // 验证对象 let loginRules = reactive({ username:[{required:true,trigger:'blur',message:'请输入您的账号'}], password:[{required:true,trigger:'blur',message:'请输入您的密码'}] }) const handleLogin = async () => { await loginRef.value.validate() try { await userStore.userLogin(loginForm) console.log(1111) router.push('/') } catch (e) { ElMessage.error("登录失败!") } } </script> <template> <div class="login"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">Jd Vue3 后台管理系统</h3> <el-form-item prop="username"> <el-input v-model="loginForm.username" prefix-icon="User" type="text" size="large" auto-complete="off" placeholder="账号" > </el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" prefix-icon="Lock" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" show-password > </el-input> </el-form-item> <el-checkbox style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-form-item style="width:100%;"> <el-button size="large" type="primary" style="width:100%;" @click.prevent="handleLogin" > <span>登 录</span> </el-button> </el-form-item> </el-form> <!-- 底部 --> <div class="el-login-footer"> <span>Copyright © 2013-2022 <a href="http://www.java1234.vip" target="_blank">jd.vip</a> 版权所有.</span> </div> </div> </template> <style lang="scss" scoped> ... </style>
4.浏览器测试
实现自定义logout处理
默认logout请求实现是有状态的 返回到login请求页面 我们现在是前后端分离处理 所以需要自定义实现logout
1.新建security.JwtLogoutSuccessHandler
@Component public class JwtLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // 设置返回的内容格式 httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); outputStream.write(JSONUtil.toJsonStr(R.success("登出成功")).getBytes()); outputStream.flush(); outputStream.close(); } }
2.在SecurityConfig中配置
@Resource private JwtLogoutSuccessHandler jwtLogoutSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置 http ... .and() .logout() .logoutSuccessHandler(jwtLogoutSuccessHandler) ... ; }
3.浏览器测试
*获取用户角色权限信息实现
springsecurity鉴权需要获取用户的角色权限系统 包括前端也需要这些信息
首先新建角色表 sys_role、菜单权限表sys_menu、用户角色关联表 sys_user_role、角色菜单权限关联表sys_role_menu
1.创建数据库表
# 角色表
CREATE TABLE `sys_role`(
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色主键id',
`name` VARCHAR(30) DEFAULT NULL COMMENT '角色名称',
`code` VARCHAR(100) DEFAULT NULL COMMENT '角色权限字符串',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注'
)ENGINE=INNODB CHARSET = utf8;# 测试数据
INSERT INTO `sys_role`(`id`,`name`,`code`,`create_time`,`update_time`,`remark`)
VALUES (1,'超级管理员','admin','2024-07-04 14:40:44','2024-07-04 14:40:47','拥有系统最高权限'),
(2,'普通角色','common','2024-07-04 14:41:56','2024-07-04 14:41:58','普通角色'),
(3,'测试角色','test','2024-07-04 14:42:24','2024-07-04 14:42:27','测试角色'),
(4,'2',NULL,NULL,NULL,NULL),(5,'3',NULL,NULL,NULL,NULL),
(6,'4',NULL,NULL,NULL,NULL),(7,'5',NULL,NULL,NULL,NULL),
(14,'6',NULL,NULL,NULL,NULL),(16,'8',NULL,NULL,NULL,NULL),
(17,'0',NULL,NULL,NULL,NULL),(19,'测2','cc2','2024-08-13 21:06:21','2024-08-13
13:06:27','eewew2'),(20,'ccc测试','test2','2024-08-29 17:10:33',NULL,'xxx'),
(21,'今天测试角色','todytest','2024-08-29 22:01:11',NULL,'ccc');# 菜单权限表
CREATE TABLE `sys_menu` (
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '菜单主键id',
`name` VARCHAR(50) DEFAULT NULL COMMENT '菜单名称',
`icon` VARCHAR(100) DEFAULT '#' COMMENT '菜单图标',
`parent_id` BIGINT(20) DEFAULT NULL COMMENT '父菜单id',
`order_num` INT(11) DEFAULT '0' COMMENT '显示顺序',
`path` VARCHAR(200) DEFAULT '' COMMENT '路由地址',
`component` VARCHAR(255) DEFAULT NULL COMMENT '路由路径',
`menu_type` CHAR(1) DEFAULT '' COMMENT '菜单类型 M目录 C菜单 F按钮',
`perms` VARCHAR(100) DEFAULT '' COMMENT '权限标识',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注'
)ENGINE=INNODB CHARSET = utf8;# 测试数据
INSERT INTO `sys_menu`(`id`,`name`,`icon`,`parent_id`,`order_num`,`path`,`component`,`menu_type`,`perms`,`create_time`,`update_time`,`remark`)
VALUES
(1,'系统管理','system',0,1,'/sys','','M','','2024-07-04 14:56:29','2024-07-04 14:56:31','系统管理目录'),
(2,'业务管理','monitor',0,2,'/bsns','','M','','2024-07-04 14:59:43','2024-07-04 14:59:45','业务管理目录'),
(3,'用户管理','user',1,1,'/sys/user','sys/user/index','C','system:user:list','2024-07-04 15:20:51','2024-07-04 15:20:53','用户管理菜单'),
(4,'角色管理','peoples',1,2,'/sys/role','sys/role/index','C','system:role:list','2024-07-04 15:23:35','2024-07-04 15:23:39','角色管理菜单'),
(5,'菜单管理','tree- table',1,3,'/sys/menu','sys/menu/index','C','system:menu:list','2024-07-04 15:23:41','2024-07-04 15:23:43','菜单管理菜单'),
(6,'部门管理','tree',2,1,'/bsns/department','bsns/Department','C','','2024-07-04 15:24:40','2024-07-04 15:24:44','部门管理菜单'),
(7,'岗位管理','post',2,2,'/bsns/post','bsns/Post','C','','2024-07-04 15:24:42','2024-07-04 15:24:46','岗位管理菜单'),
(8,'用户新增','#',3,2,'','','F','system:user:add','2024-07-04 15:24:42','2024-07-04 15:24:46','添加用户按钮'),
(9,'用户修改','#',3,3,'','','F','system:user:edit','2024-07-04 15:24:42','2024-07-04 15:24:46','修改用户按钮'),
(10,'用户删除','#',3,4,'','','F','system:user:delete','2024-07-04 15:24:42','2024-07-04 15:24:46','删除用户按钮'),
(11,'分配角色','#',3,5,'','','F','system:user:role','2024-07-04 15:24:42','2024-07-04 15:24:46','分配角色按钮'),
(12,'重置密码','#',3,6,'','','F','system:user:resetPwd','2024-07-04 15:24:42','2024-07-04 15:24:46','重置密码按钮'),
(13,'角色新增','#',4,2,'','','F','system:role:add','2024-07-04 15:24:42','2024-07-04 15:24:46','添加用户按钮'),
(14,'角色修改','#',4,3,'','','F','system:role:edit','2024-07-04 15:24:42','2024-07-04 15:24:46','修改用户按钮'),
(15,'角色删除','#',4,4,'',NULL,'F','system:role:delete','2024-07-04 15:24:42','2024-07-04 15:24:46','删除用户按钮'),
(16,'分配权限','#',4,5,'','','F','system:role:menu','2024-07-04 15:24:42','2024-07-04 15:24:46','分配权限按钮'),
(17,'菜单新增','#',5,2,'',NULL,'F','system:menu:add','2024-07-04 15:24:42','2024-07-04 15:24:46','添加菜单按钮'),
(18,'菜单修改','#',5,3,'',NULL,'F','system:menu:edit','2024-07-04 15:24:42','2024-07-04 15:24:46','修改菜单按钮'),
(19,'菜单删除','#',5,4,'',NULL,'F','system:menu:delete','2024-07-04 15:24:42','2024-07-04 15:24:46','删除菜单按钮'),
(20,'用户查询','#',3,1,'',NULL,'F','system:user:query','2024-07-04 15:24:42','2024-07-04 15:24:46','用户查询按钮'),
(21,'角色查询','#',4,1,'',NULL,'F','system:role:query','2024-07-04 15:24:42','2024-07-04 15:24:46','角色查询按钮'),
(22,'菜单查询','#',5,1,'',NULL,'F','system:menu:query','2024-07-04 15:24:42','2024-07-04 15:24:46','菜单查询按钮'),
(33,'测速22','122',3,3,'','34','M','33','2024-08-19 03:11:20','2024-08-18 19:11:33',NULL);# 用户角色关联表
CREATE TABLE `sys_user_role`(
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户角色主键id',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户id',
`role_id` BIGINT(20) DEFAULT NULL COMMENT '角色id'
)ENGINE=INNODB CHARSET = utf8;# 测试数据
INSERT INTO `sys_user_role`(`id`,`user_id`,`role_id`)
VALUES (1,1,1),(2,2,2),(4,1,2),(6,3,3),(7,3,2),(9,4,3),(10,5,3),(11,15,3),(16,28,2),(17,28,3),(20,29,20),(21,30,17),(22,30,21);# 角色菜单关联表
CREATE TABLE `sys_role_menu`(
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色菜单主键id',
`role_id` BIGINT(20) DEFAULT NULL COMMENT '角色id',
`menu_id` BIGINT(20) DEFAULT NULL COMMENT '菜单id'
)ENGINE=INNODB CHARSET = utf8;# 测试数据
INSERT INTO `sys_role_menu`(`id`,`role_id`,`menu_id`)
VALUES
(8,2,1),(9,2,2),(10,2,3),(11,2,4),(12,2,5),(13,2,6),(14,2,7),(15,3,2),(16,3,6),(17,3,7),(21,7,1),(22,7,2),(23,7,6),(24,7,7),
(25,6,1),(26,6,3),(27,6,9),(28,6,10),(29,19,1),(30,19,3),(31,19,2),(32,19,6),(33,1,1),(34,1,3),(35,1,20),(36,1,8),(37,1,9),
(38,1,10),(39,1,11),(40,1,12),(41,1,4),(42,1,21),(43,1,13),(44,1,14),(45,1,15),(46,1,16),(47,1,23),(48,1,5),(49,1,22),(50,1,17),
(51,1,18),(52,1,19),(53,1,2),(54,1,6),(55,1,7),(208,20,1),(209,20,3),(210,20,20),(211,20,8),(212,20,9),(213,20,33),(214,20,10),
(215,20,11),(216,20,4),(217,20,21),(218,20,13),(219,20,5),(220,20,22),(221,20,17),(222,20,18),(223,20,2),(224,20,6),(225,20,7),
(232,21,1),(233,21,9),(234,21,4),(235,21,21),(236,21,2),(237,21,6),(238,21,7);
2.使用MybatisX快速生成entity、mapper、service
3.完善MyUserDetailsServiceImpl的getUserAuthority方法
// 在调用getUserAuthority方法的地方也要做相应修改 比如JwtAuthenticationFilter的方法 public List<GrantedAuthority> getUserAuthority(Long userId) { String authority=sysUserService.getUserAuthorityInfo(userId); return AuthorityUtils.commaSeparatedStringToAuthorityList(authority); }
4.在SysUserService中定义方法
String getUserAuthorityInfo(Long userId);
5.在SysUserServiceImpl中做相应实现
package com.jd.jdadmin.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.jd.jdadmin.entity.SysMenu; import com.jd.jdadmin.entity.SysRole; import com.jd.jdadmin.entity.SysUser; import com.jd.jdadmin.mapper.SysMenuMapper; import com.jd.jdadmin.mapper.SysRoleMapper; import com.jd.jdadmin.service.SysUserService; import com.jd.jdadmin.mapper.SysUserMapper; import com.jd.jdadmin.util.StrUtil; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @author java大帝 * @description 针对表【sys_user】的数据库操作Service实现 * @createDate 2025-01-13 15:41:51 */ @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService{ @Resource private SysRoleMapper sysRoleMapper; @Resource private SysMenuMapper sysMenuMapper; @Override public SysUser getByUsername(String username) { return getOne(new QueryWrapper<SysUser>().eq("username",username)); } // 返回格式: ROLE_admin,ROLE_common,system:user:resetPwd,system:role:delete,system:user:list,system:menu:query,system:menu:list 的字符串 @Override public String getUserAuthorityInfo(Long userId) { StringBuffer authority = new StringBuffer(); // 1.根据用户id获取所有的权限信息[通过用户角色关联表] List<SysRole> roleList = sysRoleMapper.selectList(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id = " + userId)); if (roleList.size() > 0){ // 长度大于0 String roleCodeStrs = roleList.stream().map(item -> "ROLE_" + item.getCode()).collect(Collectors.joining(",")); authority.append(roleCodeStrs); } // 2.遍历所有角色获取所有菜单权限而且不重复 // 使用set集合 避免重复 Set<String> menuCodeSet = new HashSet<>(); for (SysRole sysRole : roleList) { List<SysMenu> menuList = sysMenuMapper.selectList(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id = " + sysRole.getId())); // 遍历菜单列表 取出perms权限值 for (SysMenu sysMenu : menuList) { String perms = sysMenu.getPerms(); if (StrUtil.isNotEmpty(perms)){ // 添加到set集合中 menuCodeSet.add(perms); } } } if (menuCodeSet.size() > 1 ){ // 默认就是1所以长度必须大于1 // 在角色字符串的后面加个, authority.append(","); String menuCodeStrs = menuCodeSet.stream().collect(Collectors.joining(",")); authority.append(menuCodeStrs); } System.out.println("authority=" + authority); return authority.toString(); } }
6.在TestController方法上使用PreAuthorize注解做相应权限校验
@PreAuthorize("hasRole('admin')") // 在校验时可以不带上ROLE_因为会自动拼接上,当存入进去的role值必须带ROLE_ @RequestMapping("list") public R userList(@RequestHeader(required = false) String token){ if (StrUtil.isNotEmpty(token)){ HashMap<String, Object> resultMap = new HashMap<>(); List<SysUser> list = sysUserService.list(); resultMap.put("userList",list); return R.success(resultMap); } return R.fail(401,"无权访问"); }
7.修改一下LoginSuccessHandler
package com.jd.jdadmin.common.security; import cn.hutool.json.JSONUtil; import com.jd.jdadmin.entity.R; import com.jd.jdadmin.util.JwtUtils; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 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; /** * @author java大帝 * @ClassName LoginSuccessHandler * @description: 登录成功后的处理器 * @date 2025年 01月 14日 * @version: 1.0 */ @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // 设置返回的内容格式 httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); String username = authentication.getName(); String token = JwtUtils.genJwtToken(username); outputStream.write(JSONUtil.toJsonStr(R.success().put("authorization",token)).getBytes()); outputStream.flush(); outputStream.close(); } }
8.浏览器测试 登录之后再测试
记住密码功能实现
记住密码 通过cookie实现 先安装'js-cookie'依赖
存储用户密码为了安全需要加密 获取密码解密 所以我们安装'jsencrypt'依赖
1.安装js-cookie和jsencrypt依赖
npm install js-cookie@3.0.1 jsencrypt@3.2.1
2.util下新建jsencrypt.ts
//@ts-ignore import JSEncrypt from 'jsencrypt/bin/jsencrypt.min' // 密钥对生成 http://web.chacuo.net/netrsakeypair const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + 'UP8iWi1Qw0Y=' // 加密 export function encrypt(txt:string) { const encryptor = new JSEncrypt() encryptor.setPublicKey(publicKey)// 设置公钥 return encryptor.encrypt(txt) // 对数据进行加密 } // 解密 export function decrypt(txt:string) { const encryptor = new JSEncrypt() encryptor.setPrivateKey(privateKey) // 设置私钥 return encryptor.decrypt(txt) // 对数据进行解密 }
3.修改登录页Login.vue
<script setup>
import { ref,reactive } from 'vue'
import useUserStore from "@/stores/modules/user"
import {ElMessage} from "element-plus"
import router from "@/router"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/util/jsencrypt"
...
// 验证对象
let loginRules = reactive({
username:[{required:true,trigger:'blur',message:'请输入您的账号'}],
password:[{required:true,trigger:'blur',message:'请输入您的密码'}]
})
const handleLogin = async () => {
await loginRef.value.validate()
//勾选了需要记住密码 设置在cookie中设置记住用户名和密码
if (loginForm.rememberMe){
Cookies.set('username',loginForm.username,{ expires: 30 })
Cookies.set('password',encrypt(loginForm.password),{ expires: 30 })
Cookies.set('rememberMe',loginForm.rememberMe,{ expires: 30 })
}else {
// 否则删除
Cookies.remove('username')
Cookies.remove('password')
Cookies.remove('rememberMe')
}
try {
await userStore.userLogin(loginForm)
console.log(1111)
router.push('/')
} catch (e) {
ElMessage.error("登录失败!")
}
}
function getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
Object.assign(loginForm, {
username: username === undefined ? loginForm.username : username,
password: password === undefined ? loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
});
}
// 获取cookie
getCookie();
</script>
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
...
<el-checkbox style="margin:0px 0px 25px 0px;" v-model="loginForm.rememberMe">记住密码</el-checkbox>
...
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2013-2022 <a href="http://www.java1234.vip" target="_blank">jd.vip</a> 版权所有.</span>
</div>
</div>
</template>
<style lang="scss" scoped>
...
</style>
主页面功能实现
主页面布局实现
1.在layout目录下新建四个子目录[每个子目录下都要建index.vue] 分模块管理主页面
2.修改layout/index.vue
<script setup lang="ts"> import Menu from '@/layout/menu/index.vue' import Header from '@/layout/header/index.vue' import Tabs from '@/layout/tabs/index.vue' import Footer from '@/layout/footer/index.vue' </script> <template> <div class="app-wrapper"> <el-container> <el-aside class="sidebar-container" width="200px"> <Menu /> </el-aside> <el-container> <el-header> <Header /> </el-header> <el-main> <Tabs /> </el-main> <el-footer> <Footer /> </el-footer> </el-container> </el-container> </div> </template> <style scoped> .app-wrapper { position: relative; width: 100%; height: 100%; } .sidebar-container { background-color: #2d3a4b; height: 100%; } .el-container { height: 100%; } .el-header { padding-left: 0; padding-right: 0; } /*:deep深度选择器*/ :deep(ul,el-menu) { border-right-width: 0; } </style>
3.修改footer/index.vue
<script setup> </script> <template> <div class="footer"> Copyright © 2012-2022 java大帝 版权所有 <a href="https://blog.csdn.net/2302_80480374/article/details/145109271?spm=1001.2014.3001.5501" target="_blank">www.java大帝.vip</a> </div> </template> <style lang="scss" scoped> .footer{ padding: 20px; display: flex; align-items: center; } </style>
4.修改tabs/index.vue
<script lang="ts" setup> import { ref } from 'vue' let tabIndex = 2 const editableTabsValue = ref('2') const editableTabs = ref([ { title: 'Tab 1', name: '1', content: 'Tab 1 content', }, { title: 'Tab 2', name: '2', content: 'Tab 2 content', }, ]) const addTab = (targetName: string) => { const newTabName = `${++tabIndex}` editableTabs.value.push({ title: 'New Tab', name: newTabName, content: 'New Tab content', }) editableTabsValue.value = newTabName } const removeTab = (targetName: string) => { const tabs = editableTabs.value let activeName = editableTabsValue.value if (activeName === targetName) { tabs.forEach((tab, index) => { if (tab.name === targetName) { const nextTab = tabs[index + 1] || tabs[index - 1] if (nextTab) { activeName = nextTab.name } } }) } editableTabsValue.value = activeName editableTabs.value = tabs.filter((tab) => tab.name !== targetName) } </script> <template> <div style="margin-bottom: 20px"> <el-button size="small" @click="addTab(editableTabsValue)"> add tab </el-button> </div> <el-tabs v-model="editableTabsValue" type="card" class="demo-tabs" closable @tab-remove="removeTab" > <el-tab-pane v-for="item in editableTabs" :key="item.name" :label="item.title" :name="item.name" > { { item.content }} </el-tab-pane> </el-tabs> </template> <style> .demo-tabs > .el-tabs__content { padding: 32px; color: #6b778c; font-size: 32px; font-weight: 600; } </style>
左侧动态权限菜单实现
1.后端: LoginSuccessHandler在登录成功处理器中做处理
package com.jd.jdadmin.common.security; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.jd.jdadmin.entity.R; import com.jd.jdadmin.entity.SysMenu; import com.jd.jdadmin.entity.SysRole; import com.jd.jdadmin.entity.SysUser; import com.jd.jdadmin.service.SysMenuService; import com.jd.jdadmin.service.SysRoleService; import com.jd.jdadmin.service.SysUserService; import com.jd.jdadmin.util.JwtUtils; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; /** * @author java大帝 * @ClassName LoginSuccessHandler * @description: 登录成功后的处理器 * @date 2025年 01月 14日 * @version: 1.0 */ @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Resource private SysUserService sysUserService; @Resource private SysRoleService sysRoleService; @Resource private SysMenuService sysMenuService; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { // 设置返回的内容格式 httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); String username = authentication.getName(); String token = JwtUtils.genJwtToken(username); // 当前登录用户 SysUser currentUser = sysUserService.getByUsername(username); StringBuffer authority = new StringBuffer(); // 1.根据用户id获取所有的权限信息[通过用户角色关联表] List<SysRole> roleList = sysRoleService.list(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id = " + currentUser.getId())); // 2.遍历所有角色获取所有菜单权限而且不重复 // 使用set集合 避免重复 Set<SysMenu> menuSet = new HashSet<>(); for (SysRole sysRole : roleList) { List<SysMenu> menuList = sysMenuService.list(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id = " + sysRole.getId())); // 遍历菜单列表 取出perms权限值 for (SysMenu sysMenu : menuList) { menuSet.add(sysMenu); } } List<SysMenu> sysMenuList = new ArrayList<>(menuSet); // 排序 sysMenuList.sort(Comparator.comparing(SysMenu::getOrderNum)); // 生成菜单树 List<SysMenu> menuList = sysMenuService.buildTreeMenu(sysMenuList); outputStream.write(JSONUtil.toJsonStr(R.success().put("authorization",token).put("currentUser",currentUser).put("menuList",menuList)).getBytes()); outputStream.flush(); outputStream.close(); } }
2.后端:在SysMenuService中创建buildTreeMenu方法
List<SysMenu> buildTreeMenu(List<SysMenu> sysMenuList);
3.后端:在 SysMenuServiceImpl实现类中实现 处理成菜单树方法
package com.jd.jdadmin.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.jd.jdadmin.entity.SysMenu; import com.jd.jdadmin.service.SysMenuService; import com.jd.jdadmin.mapper.SysMenuMapper; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * @author java大帝 * @description 针对表【sys_menu】的数据库操作Service实现 * @createDate 2025-01-15 11:16:24 */ @Service public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService{ @Override public List<SysMenu> buildTreeMenu(List<SysMenu> sysMenuList) { List<SysMenu> resultMenuList = new ArrayList<>(); for (SysMenu sysMenu : sysMenuList) { for (SysMenu menu : sysMenuList) { if (menu.getParentId() == sysMenu.getId()){ // 寻找子节点 sysMenu.getChildren().add(menu); } } if (sysMenu.getParentId() == 0L ){ // 若是一级菜单 resultMenuList.add(sysMenu); // 直接加入集合中 } } return resultMenuList; } }
4.后端:在SysMenu中新建字段存储子节点
/** * 子节点 * */ @TableField(exist = false) private List<SysMenu> children = new ArrayList<>();
5.前端:补全一下api/user/type.ts 类型
... export interface UserLoginResponseData extends ResponseData { data:{ authorization:string, menuList: MenuInfo[], currentUser:UserInfo } } export interface UserInfo { phonenumber: string, loginDate: number, updateTime: number, remark: string, avatar: string, password: string, createTime: number, id: number, email: string, username: string, status: string } export interface MenuInfo { icon: string, orderNum: number, updateTime: number, remark: string, parentId: number, path: string, component: string, children: MenuInfo[] }
6.前端:在util下新建menu.ts 菜单相关的方法
import type { MenuInfo } from "@/api/user/type"; export const SET_MENU = (menuList:MenuInfo[])=> { localStorage.setItem("MENU",JSON.stringify(menuList)) } export const GET_MENU = ()=> { return JSON.parse(localStorage.getItem("MENU") || "[]") } export const REMOVE_MENU = ()=> { return localStorage.removeItem("MENU") }
7.在用户仓库 登录请求发送完毕后存储一下后端返回的menuList数据
import { defineStore } from "pinia" import {GET_TOKEN, SET_TOKEN} from "@/util/token"; import {reqUserLogin} from "@/api/user"; import type {UserFormData, UserLoginResponseData} from "@/api/user/type"; import {GET_MENU, SET_MENU} from "@/util/menu"; const useUserStore = defineStore('User',{ state(){ return { token: GET_TOKEN(), menuList: GET_MENU() } }, actions:{ async userLogin(data:UserFormData){ let res:UserLoginResponseData = await reqUserLogin(data) if (res.code == 200){ SET_TOKEN(res.data.authorization) SET_MENU(res.data.menuList) return 'ok' } else { return Promise.reject(res.msg) } } } }) export default useUserStore
8.修改layout/menu/inedx.vue 动态渲染菜单
因为 只遍历两次所以不用害怕第三级按钮权限会显示出来、首页是每个用户都可以访问的所以直接写静态的了
<script setup lang="ts"> import { ref } from 'vue' import useUserStore from "@/stores/modules/user" let userStore = useUserStore() </script> <template> <el-menu active-text-color="#ffd04b" background-color="#2d3a4b" text-color="#fff" router > <el-menu-item index="/index"> <el-icon> <component is="House"/> </el-icon> <span>首页</span> </el-menu-item> <el-sub-menu :index="menu.path" v-for="menu in userStore.menuList"> <template #title> <el-icon> <component :is="menu.icon" /> </el-icon> <span>{ { menu.name }}</span> </template> <el-menu-item :index="item.path" v-for="item in menu.children"> <el-icon> <component :is="item.icon" /> </el-icon> <span>{ { item.name }}</span> </el-menu-item> </el-sub-menu> </el-menu> </template> <style scoped lang="scss"> </style>
9.浏览器访问 显示效果
右上角用户头像显示及退出登录实现
1.后端:在config.WebAppConfigurer类中添加addResourceHandlers方法
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 虚拟路径映射 registry.addResourceHandler("/image/userAvatar/**").addResourceLocations("file:D:\\java1234\\img\\userAvatar\\"); }
2.前端:在util下新建user.ts用户相关方法
import type { UserInfo} from "@/api/user/type"; export const SET_USER = ((user:UserInfo)=> { localStorage.setItem("USER",JSON.stringify(user)) }) export const GET_USER = ()=> { return JSON.parse(localStorage.getItem("USER") || "{}") } export const REMOVE_USER = ()=> { return localStorage.removeItem("USER") }
3.前端:新建两个子组件 并在layout/header/index.vue中引入使用
<script setup lang="ts"> import Breadcrumb from './breadCrumb/index.vue' import Avatar from './avator/index.vue' </script> <template> <div class="navbar"> <Breadcrumb /> <div class="navbar-right"> <Avatar /> </div> </div> </template> <style scoped lang="scss"> .navbar { width: 100%; height: 60px; overflow: hidden; background-color: #F5F5F5; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); padding: 0 16px; display: flex; align-items: center; box-sizing: border-box; position: relative; .navbar-right { flex: 1; display: flex; align-items: center; justify-content: flex-end; :deep(.navbar-item) { display: inline-block; margin-left: 18px; font-size: 22px; color: #5a5e66; box-sizing: border-box; cursor: pointer; } } } </style>
4.前端:在layout/header/avator/index.vue实现显示头像、退出登录
<script setup lang="ts"> import useUserStore from "@/stores/modules/user" import { baseUrl } from '@/util/request' import {ref} from "vue" import user from "@/stores/modules/user" import router from "@/router" let userStore = useUserStore() // 头像地址 let squareUrl = ref(baseUrl + 'image/userAvatar/' + userStore.userInfo.avatar) const logout = async () => { await userStore.userLogout() router.replace('/login') } </script> <template> <el-avatar shape="square" :size="40" :src="squareUrl" style="margin-right: 10px"/> <el-dropdown> <span class="el-dropdown-link"> { { userStore.userInfo.username }} <el-icon class="el-icon--right"> <arrow-down /> </el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>个人中心</el-dropdown-item> <el-dropdown-item @click="logout">安全退出</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </template> <style scoped> .el-dropdown-link { cursor: pointer; color: var(--el-color-primary); display: flex; align-items: center; } </style>
5.前端:在api/user/index.ts中新建相关api接口
enum API { ... LOGOUT_URL = 'logout' } export const reqUserLogout = () => request.get<any,any>(API.LOGOUT_URL)
6.前端:修改stores/modules/user.ts用户小仓库
import { defineStore } from "pinia" import {GET_TOKEN, REMOVE_TOKEN, SET_TOKEN} from "@/util/token"; import {reqUserLogin, reqUserLogout} from "@/api/user"; import type {UserFormData, UserLoginResponseData} from "@/api/user/type"; import {GET_MENU, REMOVE_MENU, SET_MENU} from "@/util/menu"; import {GET_USER, REMOVE_USER, SET_USER} from "@/util/user"; const useUserStore = defineStore('User',{ state(){ return { token: GET_TOKEN(), menuList: GET_MENU(), userInfo:GET_USER() } }, actions:{ async userLogin(data:UserFormData){ let res:UserLoginResponseData = await reqUserLogin(data) if (res.code == 200){ this.token = res.data.authorization SET_TOKEN(res.data.authorization) // 存储token SET_MENU(res.data.menuList) // 存储菜单数据 SET_USER(res.data.currentUser) // 存储用户信息 return 'ok' } else { return Promise.reject(res.msg) } }, async userLogout(){ // 退出登录 let res:any = await reqUserLogout() if (res.code == 200){ // 清空本地存储 window.localStorage.clear() return 'ok' } else { return Promise.reject(res.msg) } } } }) export default useUserStore
7.浏览器测试
路由守卫功能实现
前端如果没有登录过 也就没有token 则自动跳转到登录页面 这个就是路由守卫
我们通过 router.beforeEach((to,from,next) => {}) 实现
1.router目录下新建permission.ts
import router from "@/router/index" import useUserStore from "@/stores/modules/user" router.beforeEach((to,from,next) => { // 用户相关小仓库 const userStore = useUserStore() // 白名单 const whiteList = ['/login'] if (userStore.token){ next() } else { if (whiteList.includes(to.path)){ next() } else { next('/login') } } })
2.在main.ts中引入使用
// 引入路由鉴权文件 import '@/router/permission'
3.此时若是未登录则不能直接访问除/login之外的路由
动态路由实现
我们vue路由信息,需要通过后端查询的menuList 动态设置到router里面去
1.layout/index.vue加下<router-view/>
<template>
<div class="app-wrapper">
<el-container>
...
<el-main>
<Tabs />
<router-view />
</el-main>
...
</el-container>
</div>
</template>
2.在src/views下创建对应文件夹和文件
3.修改src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'layout',
component: () => import('../layout/index.vue'),
redirect:'/index',
children:[ // 为动态添加路由做准备
{
path: '/index',
name: '首页',
component: () => import('../views/index/index.vue')
},
{
path: '/userCenter',
name: '用户中心',
component: () => import('../views/userCenter/index.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
}
]
})
export default router
4.在用户仓库中添加属性
...
const useUserStore = defineStore('User',{
state(){
return {
...
hasRoutes:false //标志着是否已经动态添加过路由了 只需动态添加一次就好了
}
},
actions:{
...
}
})
export default useUserStore
5.修改src/router/permission.ts
import router from "@/router/index" import useUserStore from "@/stores/modules/user" import type {MenuInfo} from "@/api/user/type" // 使用 import.meta.glob 预加载所有可能的组件 const components = import.meta.glob('../views/**/**/*.vue'); router.beforeEach((to,from,next) => { // 用户相关小仓库 const userStore = useUserStore() let menuList = userStore.menuList // 白名单 const whiteList = ['/login'] if (userStore.token){ next() } else { if (whiteList.includes(to.path)){ next() } else { next('/login') } } }) // 动态绑定路由 const bindRoute = (menuList:MenuInfo[]) => { let newRoutes = router.options.routes menuList.forEach(menu => { if (menu.children){ //是否有子组件 menu.children.forEach(m => { let route = menuToRoute(m,menu.name) //将子菜单对象转成路由对象 console.log(route) if (route){ // 若为空 则不动态添加 //@ts-ignore newRoutes[0].children.push(route) //将路由都添加到第一个路由的子路由中 } }) } }) // 重新添加到路由 newRoutes.forEach(route => { router.addRoute(route) }) } // 菜单对象转成路由对象 const menuToRoute = (menu:MenuInfo,parentName:string) => { if (!menu.component){ // 若没有组件信息字段 则直接返回null return null } else { const componentPath = `../views/${menu.component}.vue`; const component = components[componentPath]; let route ={ name:menu.name, path:menu.path, component, meta:{ parentName:parentName } } return route } }
async function initializeRoutes() { const userStore = useUserStore(pinia); if (!userStore.hasRoutes && userStore.token) { await bindRoute(userStore.menuList); userStore.hasRoutes = true; } } initializeRoutes(); //为了避免重复添加路由,可以考虑在应用初始化时就执行一次bindRoute,而不是放在beforeEach守卫里。这样可以保证路由仅被添加一次,而不会因为页面刷新或其他原因重复添加
6.路由显示结果如下 都是在/下
动态标签页的实现
1.创建tabsStore 用于管理标签的相关数据[因为涉及到菜单组件和标签选项卡组件的通信]
import {defineStore} from "pinia" const useTabsStore = defineStore('Tabs',{ state(){ return { editableTabsValue:'/index', // 存储当前选中的可编辑选项卡的值 editableTabs:[ // 存储可编辑选项卡的列表 { title:'首页',// 显示的标签名字 name:'/index' // 选项卡的唯一标识 } ] } }, actions:{ addTabs(tab:any){ if (this.editableTabs.findIndex(e => e.name === tab.path) === -1) { this.editableTabs.push({ title: tab.name, name: tab.path }) } this.editableTabsValue = tab.path }, resetTabs(){ this.editableTabsValue = '/index' this.editableTabs = [ { title:'首页',// 显示的标签名字 name:'/index' // } ] } } }) export default useTabsStore
2. 修改src/layout/menu/index.vue 实现点击左侧菜单项 显示对应 标签选项卡
<script setup lang="ts">
...
import type {MenuInfo} from "@/api/user/type";
import useTabsStore from "@/stores/modules/tabs";
let tabsStore = useTabsStore()
let userStore = useUserStore()
const showTabs = (menu:MenuInfo) => {
tabsStore.addTabs(menu)
}
</script>
<template>
<el-menu
active-text-color="#ffd04b"
background-color="#2d3a4b"
text-color="#fff"
router
>
<el-menu-item index="/index">
<el-icon>
<component is="House"/>
</el-icon>
<span>首页</span>
</el-menu-item>
<el-sub-menu :index="menu.path" v-for="menu in userStore.menuList">
<template #title>
<el-icon>
<component :is="menu.icon" />
</el-icon>
<span>{
{ menu.name }}</span>
</template>
<el-menu-item :index="item.path" v-for="item in menu.children" @click="showTabs(item)">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{
{ item.name }}</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<style scoped lang="scss">
</style>
3.修改src/layout/header/avator/index.vue 实现退出登录后将数据复原
import useTabsStore from "@/stores/modules/tabs" let tabsStore = useTabsStore()const logout = async () => { await userStore.userLogout() // 重置tab tabsStore.resetTabs() // 将控制是否动态加载路由的值复原 userStore.hasRoutes = false router.replace('/login') }
4.修改src/layout/tabs/index.vue 实现点击标签浏览器地址栏显示对应路径 选中对应菜单项
<script lang="ts" setup> import { ref,watch } from 'vue' import useTabsStore from "@/stores/modules/tabs" import {useRouter} from "vue-router" import router from "@/router"; let tabsStore = useTabsStore() let $router = useRouter() // 点击标签页 const tabClick = (target:any) => { // 路由跳转 $router.push({name:target.props.label}) } const removeTab = (targetName: string) => { let tabs = tabsStore.editableTabs let activeName = tabsStore.editableTabsValue if (activeName === '/index'){ // 不能删除首页标签页 return } if (activeName === targetName) { tabs.forEach((tab, index) => { if (tab.name === targetName) { const nextTab = tabs[index + 1] || tabs[index - 1] if (nextTab) { activeName = nextTab.name } } }) } // 过滤出除当前点击的标签页生成的数组 tabsStore.editableTabs = tabs.filter(i => i.name !== targetName) router.push({path:activeName}) } </script> <template> <el-tabs v-model="tabsStore.editableTabsValue" type="card" class="demo-tabs" closable @tab-remove="removeTab" @tab-click="tabClick" > <el-tab-pane v-for="item in tabsStore.editableTabs" :key="item.name" :label="item.title" :name="item.name" > </el-tab-pane> </el-tabs> </template> <style> .demo-tabs > .el-tabs__content { padding: 32px; color: #6b778c; font-size: 32px; font-weight: 600; } </style>
5.浏览器测试
动态面包屑实现
1.修改src/layout/header/breadCrumb/index.vue
<script setup lang="ts"> import {ref, watch} from "vue" import {useRoute} from "vue-router" let $route = useRoute() let breadcrumbList = ref([]) let parentName = ref('') const initBreadcrumbList = () => { // 获取当前匹配的路由 //@ts-ignore breadcrumbList.value = $route.matched //@ts-ignore parentName.value = $route.meta.parentName } // 深度监听 watch($route, ()=>{ initBreadcrumbList() },{deep:true,immediate:true}) </script> <template> <el-icon> <component is="House"/> </el-icon> <el-breadcrumb separator="/"> <el-breadcrumb-item v-for="(item,index) in breadcrumbList" :key="index"> <span v-if="parentName && index > 0">{ {parentName}} / </span> <span>{ { item.name }}</span> </el-breadcrumb-item> </el-breadcrumb> </template> <style scoped> </style>
2.在views/index/index.vue中随便写点东西
<template> <div class="home"> 欢迎使用,java大帝 通用权限系统 ! </div> </template> <script> export default { name: "index" }; </script> <style lang="scss" scoped> </style>
3.浏览器测试
4.menu菜单默认激活菜单项
<script setup lang="ts">
...
import {useRoute} from "vue-router"
...
let $route = useRoute()
...
</script>
<template>
<el-menu
active-text-color="#ffd04b"
background-color="#2d3a4b"
text-color="#fff"
router
:default-active="$route.path"
>
...
</el-menu>
</template>
<style scoped lang="scss">
</style>
个人中心功能实现
路由与导航动态绑定实现
1.修改App.vue
<script setup lang="ts"> import { ref ,watch} from 'vue' import { useRoute,useRouter } from 'vue-router' import useTabsStore from "@/stores/modules/tabs" let tabsStore = useTabsStore() const route=useRoute() const router=useRouter() const whitePath=['/login','/index','/'] watch(() => route.path, (newPath) => { if (whitePath.indexOf(newPath) === -1) { let obj = { name: route.name as string, path: newPath }; tabsStore.addTabs(obj); } }, { immediate: true }); </script>
2.修改 src/layout/header/avator/index.vue
<script setup lang="ts">
...
</script>
<template>
<el-avatar shape="square" :size="40" :src="squareUrl" style="margin-right: 10px"/>
<el-dropdown>
...
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<router-link :to="{name:'个人中心'}" >个人中心</router-link>
</el-dropdown-item>
<el-dropdown-item @click="logout">安全退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style scoped>
.el-dropdown-link {
...
</style>
个人中心页面构建实现
1.修改src/views/userCenter/index.vue
<script setup lang="ts"> import useUserStore from "@/stores/modules/user" import {formatDate} from "@/util/formatDate" import {ref} from "vue"; let userStore = useUserStore() // 默认选中的标签页 let activeTab = ref('userinfo') </script> <template> <div class="app-container"> <el-row :gutter="20"> <el-col :span="6"> <el-card class="box-card"> <template v-slot:header> <div class="clearfix"> <span>个人信息</span> </div> </template> <div> <div class="text-center"> 修改头像 </div> <ul class="list-group list-group-striped"> <li class="list-group-item"> <el-icon> <component is="User"/> </el-icon> 用户名称 <div class="pull-right"> { {userStore.userInfo.username}} </div> </li> <li class="list-group-item"> <el-icon> <component is="Histogram"/> </el-icon> 手机号码 <div class="pull-right"> { {userStore.userInfo.phonenumber}} </div> </li> <li class="list-group-item"> <el-icon> <component is="Tickets"/> </el-icon> 用户邮箱 <div class="pull-right"> { {userStore.userInfo.email}} </div> </li> <li class="list-group-item"> <el-icon> <component is="Avatar"/> </el-icon> 所属角色 <div class="pull-right"> { {userStore.userInfo.roles}} </div> </li> <li class="list-group-item"> <el-icon> <component is="Timer"/> </el-icon> 创建日期 <div class="pull-right"> { {formatDate(userStore.userInfo.loginDate)}} </div> </li> </ul> </div> </el-card> </el-col> <el-col :span="18"> <el-card class="box-card"> <template v-slot:header> <div class="clearfix"> <span>修改密码</span> </div> </template> <el-tabs v-model="activeTab"> <el-tab-pane label="基本资料" name="userinfo"> 基本资料 </el-tab-pane> <el-tab-pane label="修改密码" name="resetPwd"> 修改密码 </el-tab-pane> </el-tabs> </el-card> </el-col> </el-row> </div> </template> <style scoped lang="scss"> .list-group-striped>.list-group-item { border-left: 0; border-right: 0; border-radius: 0; padding-left: 0; padding-right: 0; } .list-group-item { border-bottom: 1px solid #e7eaec; border-top: 1px solid #e7eaec; margin-bottom: -1px; padding: 11px 0; font-size: 13px; } .pull-right{ float: right!important; } ::v-deep .el-card__body{ height:230px; } ::v-deep .box-card{ height:450px; } </style>
2.创建格式化日期的工具方法
export function formatDate(val:string){ let date = new Date(Number(val)) let Y = date.getFullYear() + '-' let M = (date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1) + "-" let D = date.getDate() + " " let h = date.getHours() + ':' let m = date.getMinutes() + ':' let s = (date.getSeconds() < 10 ? "0" + (date.getSeconds()) : date.getSeconds()) return Y + M + D + h + m + s }
3.浏览器显示效果
个人中心页面数据显示
1.抽取出来三个子组件 分别处理更换头像、重置密码、用户基本信息的业务
2.在src/views/userCenter/index.vue中引入三个子组件并使用
<script setup lang="ts"> ... import Avatar from './avatar/index.vue' import userInfo from './userInfo/index.vue' import resetPwd from './resetPwd/index.vue' ... </script> <template> <div class="app-container"> <el-row :gutter="20"> <el-col :span="6"> <el-card class="box-card"> <template v-slot:header> <div class="clearfix"> <span>个人信息</span> </div> </template> <div> <div class="text-center"> <avatar /> </div> <ul class="list-group list-group-striped"> ... </ul> </div> </el-card> </el-col> <el-col :span="18"> <el-card class="box-card"> <template v-slot:header> <div class="clearfix"> <span> 基本资料 </span> </div> </template> <el-tabs v-model="activeTab"> <el-tab-pane label="基本资料" name="userinfo"> <user-info/> </el-tab-pane> <el-tab-pane label="修改密码" name="resetPwd"> <reset-pwd/> </el-tab-pane> </el-tabs> </el-card> </el-col> </el-row> </div> </template> <style scoped lang="scss"> .... </style>
3.后端:修改SysUser实体类 添加roles属性 存储拥有的角色信息
/** * 角色信息 * 使用逗号分割的字符串 * */ @TableField(exist = false) private String roles;
4.后端:在LoginSuccessHandler中设置角色信息
... @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Resource private SysUserService sysUserService; @Resource private SysRoleService sysRoleService; @Resource private SysMenuService sysMenuService; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { ... for (SysRole sysRole : roleList) { ... } // 使用stream流式计算将角色列表处理成使用逗号间隔的字符串 String roleStr = roleList.stream().map(SysRole::getName).collect(Collectors.joining(",")); // 设置给当前用户 currentUser.setRoles(roleStr); ... } }
5.浏览器测试
基本资料修改功能实现
1.在父组件中将用户信息传递进去
<script setup lang="ts">
...
</script>
<template>
<div class="app-container">
<el-row :gutter="20">
...
<el-col :span="18">
<el-card class="box-card">
...
<el-tabs v-model="activeTab">
<el-tab-pane label="基本资料" name="userinfo">
<user-info :user="userStore.userInfo"/>
</el-tab-pane>
...
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped lang="scss">
...
</style>
2.补齐views/userCenter/userInfo/index.vue
<script setup lang="ts"> import {ref} from "vue" import {ElMessage} from "element-plus"; import {reqAddOrUpdate} from "@/api/user"; import useUserStore from "@/stores/modules/user"; let userStore = useUserStore() let props = defineProps({ user:{ type:Object, default:()=> {}, required:true } }) let form = ref({ id:-1, phonenumber:'', email:'' }) let userRef= ref() let rules = ref({ email: [ { required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] } ], phonenumber: [ { required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" } ], }) form.value= props.user; const handleSubmit= async ()=>{ await userRef.value.validate() try { let res = await reqAddOrUpdate(form.value) if (res.code == 200){ ElMessage.success(form.value.id ? '修改成功' : '添加成功') userStore.userInfo = form.value }else { ElMessage.error(form.value.id ? '修改失败' : '添加失败') } } catch (e) { ElMessage.error((e as Error).message) } } </script> <template> <el-form ref="userRef" :model="form" :rules="rules" label-width="100px" > <el-form-item label="手机号码:" prop="phonenumber"> <el-input v-model="form.phonenumber" maxlength="11" /> </el-form-item> <el-form-item label="用户邮箱:" prop="email"> <el-input v-model="form.email" maxlength="50" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSubmit">保存</el-button> </el-form-item> </el-form> </template> <style scoped lang="scss"> </style>
3.在src/api/user/index.ts中新增相关API接口
// 用户相关的API接口 import request from "@/util/request" import type {UserFormData, UserInfo, UserLoginResponseData} from "@/api/user/type" enum API { ... ADD_OR_UPDATE_URL = 'sys/user/saveOrUpdate' } ... export const reqAddOrUpdate = (data:UserInfo) => request.post<any,any>(API.ADD_OR_UPDATE_URL,data)
// 修改用户信息接口 export interface UserInfo { phonenumber: string, loginDate?: number, updateTime?: number, remark?: string, avatar?: string, password?: string, createTime?: number, id?: number, email: string, username?: string, status?: string, roles?:string }
4.后端:新增controller/SysUserController.java
package com.jd.jdadmin.controller; import com.jd.jdadmin.entity.R; import com.jd.jdadmin.entity.SysUser; import com.jd.jdadmin.service.SysUserService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Date; /** * @author java大帝 * @ClassName SysUserController * @description: 用户Controller控制器 * @date 2025年 01月 16日 * @version: 1.0 */ @RestController @RequestMapping("/sys/user") public class SysUserController { @Resource private SysUserService sysUserService; /** * 添加或修改 * */ @RequestMapping("saveOrUpdate") @PreAuthorize("hasAuthority('system:user:add')"+"||"+"hasAuthority('system:user :edit')") public R saveOrUpdate(@RequestBody SysUser sysUser){ if (sysUser.getId() == null || sysUser.getId() == -1){ }else { sysUser.setUpdateTime(new Date()); sysUserService.updateById(sysUser); } return R.success(); } }
5.浏览器测试
修改密码功能实现
1.在父组件中将用户信息传递进去
<script setup lang="ts">
...
</script>
<template>
<div class="app-container">
<el-row :gutter="20">
...
<el-col :span="18">
<el-card class="box-card">
...
<el-tabs v-model="activeTab">
...
<el-tab-pane label="修改密码" name="resetPwd">
<reset-pwd :user="userStore.userInfo"/>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped lang="scss">
...
</style>
2.修改src/views/userCenter/resetPwd.vue
<script setup lang="ts"> import {defineProps, ref} from "vue"; import requestUtil from "@/util/request"; import { ElMessage } from 'element-plus' import useUserStore from "@/stores/modules/user" import {reqUpdatePwd} from "@/api/user"; let userStore = useUserStore() const props = defineProps( { user:{ type:Object, default:()=>{}, required:true } }) const form = ref({ id:-1, oldPassword:'', newPassword:'', confirmPassword:'' }) const pwdRef= ref() form.value = props.user const equalToPassword = (rule, value, callback) => { if (form.value.newPassword !== value) { callback(new Error("两次输入的密码不一致")); } else { callback() } } const rules = ref({ oldPassword: [ { required: true, message: "旧密码不能为空", trigger: "blur" } ], newPassword: [ { required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" } ], confirmPassword: [ { required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" } ] }) const handleSubmit = async ()=> { await pwdRef.value.validate() let result = await reqUpdatePwd(form.value) if (result.code == 200) { ElMessage.success("密码修改成功,下一次登录生效!") userStore.userInfo = form.value }else{ ElMessage.error(result.msg) } } </script> <template> <el-form ref="pwdRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="旧密码" prop="oldPassword"> <el-input v-model="form.oldPassword" placeholder="请输入旧密码" type="password" show-password /> </el-form-item> <el-form-item label="新密码" prop="newPassword"> <el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" show-password /> </el-form-item> <el-form-item label="确认密码" prop="confirmPassword"> <el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" show-password/> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSubmit">保存</el-button> </el-form-item> </el-form> </template> <style scoped lang="scss"> </style>
3.在src/api/user/index.ts中新增相关API接口
// 用户相关的API接口 import request from "@/util/request" import type {UserFormData, UserInfo, UserLoginResponseData} from "@/api/user/type" enum API { ... UPDATE_PWD_URL = 'sys/user/updateUserPwds' } ... export const reqUpdatePwd = (data:UserInfo) => request.post<any,any>(API.UPDATE_PWD_URL,data)
// 修改用户信息接口
export interface UserInfo {
...
oldPassword?:string,
newPassword?:string
}
4.后端:修改SysUser.java 添加两个oldPassword和newPassword字段
/** * 旧密码 * */ @TableField(exist = false) private String oldPassword; /** * 确认新密码 * */ @TableField(exist = false) private String newPassword;
5.在SysUserController中新增修改密码的接口
@Resource private BCryptPasswordEncoder bCryptPasswordEncoder;/** * 修改密码 * */ @RequestMapping("updateUserPwds") @PreAuthorize("hasAuthority('system:user:edit')") public R updateUserPwds(@RequestBody SysUser sysUser){ SysUser currentUser = sysUserService.getById(sysUser.getId()); // 比较旧密码是否与数据库中的一致 if (bCryptPasswordEncoder.matches(sysUser.getOldPassword(),currentUser.getPassword())){ currentUser.setPassword(bCryptPasswordEncoder.encode(sysUser.getNewPassword())); currentUser.setUpdateTime(new Date()); sysUserService.updateById(currentUser); return R.success(); }else{ return R.fail("输入旧密码错误"); } }
6.浏览器测试
头像更换功能实现
1.在父组件中将用户信息传递进去
<script setup lang="ts">
...
</script>
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="box-card">
<template v-slot:header>
<div class="clearfix">
<span>个人信息</span>
</div>
</template>
<div>
<div class="text-center">
<avatar :user="userStore.userInfo"/>
</div>
<ul class="list-group list-group-striped">
...
</ul>
</div>
</el-card>
</el-col>
...
</el-row>
</div>
</template>
<style scoped lang="scss">
...
</style>
2.修改src/views/userCenter/resetPwd.vue
<script setup lang="ts"> import {defineProps, ref} from "vue"; import { ElMessage } from 'element-plus' import useUserStore from "@/stores/modules/user" import {baseUrl} from "@/util/request" import {reqUpdateAvatar} from "@/api/user"; let userStore = useUserStore() const props=defineProps( { user:{ type:Object, default:()=>{}, required:true } }) const headers= ref({ token:userStore.token }) const form=ref({ id:-1, avatar:'' }) const formRef=ref(null) const imageUrl=ref("") form.value = props.user; imageUrl.value= baseUrl+'image/userAvatar/'+form.value.avatar const handleAvatarSuccess= (res:any) =>{ console.log(res) imageUrl.value= baseUrl+res.data.src form.value.avatar=res.data.title; } const beforeAvatarUpload = (file:any) => { const isJPG = file.type === 'image/jpeg' const isLt2M = file.size / 1024 / 1024 < 2 if (!isJPG) { ElMessage.error('图片必须是jpg格式') } if (!isLt2M) { ElMessage.error('图片大小不能超过2M!') } return isJPG && isLt2M } const handleConfirm=async()=>{ let result = await reqUpdateAvatar(form.value) if(result.code==200){ ElMessage.success("执行成功!") }else{ ElMessage.error(result.msg); } } </script> <template> <el-form ref="formRef" :model="form" label-width="100px" style="text-align: center;padding-bottom:10px" > <!-- :action="baseUrl+'sys/user/uploadImage'" 图片上传 --> <el-upload :headers="headers" class="avatar-uploader" :action="baseUrl+'sys/user/uploadImage'" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"> <img v-if="imageUrl" :src="imageUrl" class="avatar" /> <el-icon v-else class="avatar-uploader-icon"> <component is="Plus"/> </el-icon> </el-upload> <el-button @click="handleConfirm" >确认更换</el-button> </el-form> </template> <style scoped lang="scss"> .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; } .el-icon.avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; } .avatar { width: 120px; height: 120px; display: block; } </style>
3.在src/api/user/index.ts中新增相关API接口
// 用户相关的API接口 import request from "@/util/request" import type {UserFormData, UserInfo, UserLoginResponseData} from "@/api/user/type" enum API { ... UPDATE_AVATAR_URL = 'sys/user/updateAvatar' } ... export const reqUpdateAvatar = (data:any) => request.post<any,any>(API.UPDATE_AVATAR_URL,data)
4.后端: 在SysUserController中新增上传头像和修改用户头像的接口
@Value("${avatarImagesFilePath}") private String avatarImagesFilePath;/*** 上传用户头像图片 * * @param file * * @return * @throws Exception * */ @RequestMapping("/uploadImage") @PreAuthorize("hasAuthority('system:user:edit')") public Map<String,Object> uploadImage(MultipartFile file)throws Exception{ Map<String,Object> resultMap = new HashMap<>(); if(!file.isEmpty()){ // 获取文件名 String originalFilename = file.getOriginalFilename(); // 获取源文件的后缀 String suffixName=originalFilename.substring(originalFilename.lastIndexOf(".")); // 拼接新的文件名 防止名字重复 String newFileName= DateUtil.getCurrentDateStr()+suffixName; // 将文件拷贝到对应位置 FileUtils.copyInputStreamToFile(file.getInputStream(),new File(avatarImagesFilePath+newFileName)); resultMap.put("code",0); resultMap.put("msg","上传成功"); Map<String,Object> dataMap=new HashMap<>(); dataMap.put("title",newFileName); dataMap.put("src","image/userAvatar/"+newFileName); resultMap.put("data",dataMap); } return resultMap; } /** * 修改用户头像 * @param sysUser * @return * */ @RequestMapping("/updateAvatar") @PreAuthorize("hasAuthority('system:user:edit')") public R updateAvatar(@RequestBody SysUser sysUser){ SysUser currentUser = sysUserService.getById(sysUser.getId()); currentUser.setUpdateTime(new Date()); currentUser.setAvatar(sysUser.getAvatar()); sysUserService.updateById(currentUser); return R.success(); }
5.在application.yml中增加属性
avatarImagesFilePath: D://jd/img/userAvatar/
6. 在util下新增DateUtil.java
package com.jd.jdadmin.util; import java.text.SimpleDateFormat; import java.util.Date; /** * 日期工具类 * @author Administrator * */ public class DateUtil { /** * 日期对象转字符串 * @param date * @param format * @return */ public static String formatDate(Date date,String format){ String result=""; SimpleDateFormat sdf=new SimpleDateFormat(format); if(date!=null){ result=sdf.format(date); } return result; } /** * 字符串转日期对象 * @param str * @param format * @return * @throws Exception */ public static Date formatString(String str,String format) throws Exception{ if(StrUtil.isEmpty(str)){ return null; } SimpleDateFormat sdf=new SimpleDateFormat(format); return sdf.parse(str); } public static String getCurrentDateStr(){ Date date=new Date(); SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMddhhmmssSSSSSSSSS"); return sdf.format(date); } public static String getCurrentDatePath()throws Exception{ Date date=new Date(); SimpleDateFormat sdf=new SimpleDateFormat("yyyy/MM/dd/"); return sdf.format(date); } public static void main(String[] args) { try { System.out.println(getCurrentDateStr()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
7.浏览器测试
用户管理实现
列表分页显示
1.创建entity/PageBean.java封装分页参数
package com.jd.jdadmin.entity; /** * @author java大帝 * @ClassName PageBean * @description: 封装分页数据 * @date 2025年 01月 17日 * @version: 1.0 */ public class PageBean { // 第几页 private Integer pageNum; // 每页记录数 private Integer pageSize; // 起始页 private Integer start; // 查询参数 private String query; public PageBean() { } public PageBean(Integer pageNum, Integer pageSize, String query) { this.pageNum = pageNum; this.pageSize = pageSize; this.query = query; } public Integer getStart() { // 当前页码减一*一页显示多少条 比如 1 - 1 * 5 = 5 所以起始页是5 return (pageNum - 1) * pageSize; } public void setStart(Integer start) { this.start = start; } public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } public String getQuery() { return query; } public void setQuery(String query) { this.query = query; } }
2.装配分页拦截器
package com.jd.jdadmin.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author java大帝 * @ClassName MybatisPlusConfig * @description: 开启mp的分页功能 * @date 2025年 01月 17日 * @version: 1.0 */ @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页拦截器 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } }
3.在SysUserController中新增分页查询接口
/** * 根据条件分页查询用户信息 */ @PostMapping("/list") @PreAuthorize("hasAuthority('system:user:query')") public R list(@RequestBody PageBean pageBean){ Page<SysUser> sysUserPage = new Page<>(pageBean.getPageNum(), pageBean.getPageSize()); Page<SysUser> pageResult = sysUserService.page(sysUserPage); // 数据集合 List<SysUser> userList = pageResult.getRecords(); Map<String,Object> resultMap = new HashMap<>(); resultMap.put("userList",userList); resultMap.put("total",pageResult.getTotal()); return R.success(resultMap); }
4.前端: 在src/api/user下创建相关接口和ts类型
src/api/user/index.ts
// 用户相关的API接口 ... enum API { ... QUERY_URL = 'sys/user/list' } ... export const reqQueryList = (data:QueryPageParams) => request.post<any,QueryResponse>(API.QUERY_URL,data)
src/api/user/type.ts
... export interface QueryPageParams { pageNum:number, pageSize:number, start?:number, query?:string } export interface QueryResponse extends ResponseData { data:{ total:number, userList:UserInfo[] } }
5.前端:修改src/views/sys/user/index.vue
<script setup lang="ts"> import {ref, onMounted, reactive} from "vue" import type {QueryPageParams, QueryResponse, UserInfo} from "@/api/user/type"; import {reqQueryList} from "@/api/user"; let tableData = ref<UserInfo[]>([]) let queryForm = reactive<QueryPageParams>({ pageNum:1, pageSize:3, query:'' }) let total = ref(0) const initUserList = async (pager=1)=>{ queryForm.pageNum = pager let res:QueryResponse = await reqQueryList(queryForm) if (res.code == 200){ tableData.value = res.data.userList total.value = res.data.total } } const handleSizeChange = () => { initUserList() } onMounted(()=>{ initUserList() }) </script> <template> <div class="app-container"> <el-table border :data="tableData" stripe style="width: 100%"> <el-table-column prop="username" label="用户名" width="180" /> </el-table> <el-pagination v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize" :page-sizes="[3, 5, 7, 10]" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="initUserList" /> </div> </template> <style lang="scss" scoped> .header{ padding-bottom: 16px; box-sizing: border-box; } .el-pagination{ float: right; padding: 20px; box-sizing: border-box; } ::v-deep th.el-table__cell{ word-break: break-word; background-color: #f8f8f9 !important; color: #515a6e; height: 40px; font-size: 13px; } .el-tag--small { margin-left: 5px; } </style>
6.element-plu国际化配置
main.ts
... import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' ... app.use(ElementPlus, { locale: zhCn, }) ...
7.浏览器显示效果
搜索功能实现
1.在SysUser中新增sysRoleList 存储角色列表[方便前端遍历显示]
/** * 所有角色集合 * */ @TableField(exist = false) public List<SysRole> sysRoleList;
2.修改SysUserController的list方法
@Resource private SysRoleService roleService;/** * 根据条件分页查询用户信息 */ @PostMapping("/list") @PreAuthorize("hasAuthority('system:user:query')") public R list(@RequestBody PageBean pageBean){ String query = pageBean.getQuery().trim(); LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); if (StrUtil.isNotEmpty(query)){ queryWrapper.like(SysUser::getUsername,query); } Page<SysUser> sysUserPage = new Page<>(pageBean.getPageNum(), pageBean.getPageSize()); Page<SysUser> pageResult = sysUserService.page(sysUserPage,queryWrapper); // 数据集合 List<SysUser> userList = pageResult.getRecords(); for (SysUser sysUser : userList) { List<SysRole> roleList = roleService.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + sysUser.getId())); sysUser.setSysRoleList(roleList); } Map<String,Object> resultMap = new HashMap<>(); resultMap.put("userList",userList); resultMap.put("total",pageResult.getTotal()); return R.success(resultMap); }
3.前端: 在src/api/user下新增ts类型
src/api/user/type.tsexport interface UserInfo { phonenumber: string, loginDate?: number, updateTime?: number, remark?: string, avatar?: string, password?: string, createTime?: number, id?: number, email: string, username?: string, status?: string, roles?:string, oldPassword?:string, newPassword?:string, sysRoleList?:RoleInfo[] } export interface RoleInfo { id: number, createTime: string, updateTime: string, remark: string, name: string, code: string }
4.前端:修改src/views/sys/user/index.vue
<script setup lang="ts"> import {ref, onMounted, reactive} from "vue" import type {QueryPageParams, QueryResponse, UserInfo} from "@/api/user/type"; import {reqQueryList} from "@/api/user"; import {baseUrl} from "@/util/request"; let tableData = ref<UserInfo[]>([]) let queryForm = reactive<QueryPageParams>({ pageNum:1, pageSize:3, query:'' }) let total = ref(0) const initUserList = async (pager=1)=>{ queryForm.pageNum = pager let res:QueryResponse = await reqQueryList(queryForm) if (res.code == 200){ tableData.value = res.data.userList total.value = res.data.total } } const handleSizeChange = () => { initUserList(queryForm.pageNum) } onMounted(()=>{ initUserList() }) </script> <template> <div class="app-container"> <el-row :gutter="20" class="header"> <el-col :span="7"> <el-input placeholder="请输入用户名..." v-model="queryForm.query" clearable ></el-input> </el-col> <el-button type="primary" icon="Search" @click="initUserList(1)">搜索</el-button> </el-row> <el-table border :data="tableData" stripe style="width: 100%"> <el-table-column type="selection" width="55" /> <el-table-column prop="avatar" label="头像" width="80" align="center"> <template v-slot="scope"> <img :src="baseUrl+'image/userAvatar/'+scope.row.avatar" width="50" height="50"/> </template> </el-table-column> <el-table-column prop="username" label="用户名" width="100" align="center"/> <el-table-column prop="roles" label="拥有角色" width="200" align="center"> <template v-slot="scope"> <el-tag size="small" type="warning" v-for="item in scope.row.sysRoleList"> { {item.name}} </el-tag> </template> </el-table-column> <el-table-column prop="email" label="邮箱" width="200" align="center"/> <el-table-column prop="phonenumber" label="手机号" width="120" align="center"/> <el-table-column prop="status" label="状态?" width="200" align="center"> <template v-slot="{row}" > <el-switch v-model="row.status" @change="statusChangeHandle(row)" active-text="正常" inactive-text="禁用" active-value="0" inactive-value="1"> </el-switch> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="200" align="center"/> <el-table-column prop="loginDate" label="最后登录时间" width="200" align="center"/> <el-table-column prop="remark" label="备注" /> <el-table-column prop="action" label="操作" width="400" fixed="right" align="center"> <template v-slot="scope" > <el-button type="primary" icon="Tools" >分配角色</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize" :page-sizes="[3, 5, 7, 10]" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="initUserList" /> </div> </template> <style lang="scss" scoped> ... </style>
5.浏览器显示效果
添加修改功能实现
1.在SysUserController修改saveOrUpdate接口和新增两个新的接口
/** * 添加或修改 * */ @RequestMapping("saveOrUpdate") @PreAuthorize("hasAuthority('system:user:add')"+"||"+"hasAuthority('system:user :edit')") public R saveOrUpdate(@RequestBody SysUser sysUser){ if (sysUser.getId() == null || sysUser.getId() == -1){ sysUser.setCreateTime(new Date()); sysUserService.save(sysUser); }else { sysUser.setUpdateTime(new Date()); sysUserService.updateById(sysUser); } return R.success(); }/** * 根据id查询 * */ @GetMapping("/{id}") @PreAuthorize("hasAuthority('system:user:query')") public R findById(@PathVariable("id") Integer id){ SysUser sysUser = sysUserService.getById(id); Map<String, Object> map = new HashMap<>(); map.put("sysUser",sysUser); return R.success(map); } /** * 验证用户名是否已经被占用了 * */ @PostMapping("/checkUserName") @PreAuthorize("hasAuthority('system:user:query')") public R checkUserName(@RequestBody SysUser sysUser){ if (sysUserService.getByUsername(sysUser.getUsername()) == null){ // 用户名没有被占用 返回成功 return R.success(); } return R.fail(); }
2. 前端:在src/api/user下新增两个接口
// 用户相关的API接口 ... enum API { ... FIND_BY_ID_URL = 'sys/user/', CHECK_USERNAME_URL = 'sys/user/checkUserName' } ... export const reqFindById = (id:number) => request.get<any,any>(API.FIND_BY_ID_URL + id) export const reqCheckUserName = (data:UserInfo) => request.post<any,any>(API.CHECK_USERNAME_URL,data)
3.前端:在src/views/sys/user下新建子组件 用于显示添加/修改的对话框,并完成添加/修改用户
<script setup lang="ts"> import {defineEmits, defineProps,ref,watch } from "vue" import { ElMessage } from 'element-plus' import {reqAddOrUpdate, reqCheckUserName, reqFindById} from "@/api/user"; import type {UserInfo} from "@/api/user/type"; let props = defineProps( { id:{ type:Number, default:-1, required:true }, dialogTitle:{ type:String, default:'', required:true }, dialogVisible:{ type:Boolean, default:false, required:true } }) let formRef = ref() let form = ref<UserInfo>({ id:-1, username:"", password:"123456", status:"0", phonenumber:"", email:"", remark:"" }) const checkUsername = async (rule, value, callback) => { if(form.value.id == -1){ // 新增用户 const res= await reqCheckUserName(form.value) if (res.code==500) { callback(new Error("用户名已存在!")); } else { callback(); } }else{ callback(); } } let rules = ref({ username:[ { required: true, message: '请输入用户名'}, { required: true, validator: checkUsername, trigger: "blur" } ], email: [ { required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] } ], phonenumber: [ { required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" } ], }) const initFormData = async (id:number) =>{ const res = await reqFindById(id) form.value = res.data.sysUser; } watch(() => props.dialogVisible, () => { if (formRef.value){ // 首次打开对话框时formRef暂未初始化 formRef.value.clearValidate('phonenumber') formRef.value.clearValidate('email') formRef.value.clearValidate('username') } let id = props.id; if(id!=-1){ initFormData(id); }else{ form.value= { id:-1, username:"", password:"123456", status:"0", phonenumber:"", email:"", remark:"" } } }) const emits = defineEmits(['update:modelValue','initUserList']) const handleClose = ()=>{ emits('update:modelValue',false) } const handleConfirm = async ()=>{ await formRef.value.validate() let res = await reqAddOrUpdate(form.value) if(res.code==200){ ElMessage.success("执行成功!") formRef.value.resetFields(); emits("initUserList") handleClose(); }else{ ElMessage.error(res.msg); } } </script> <template> <!--不要在 dialogVisible 上使用 v-model,因为它是一个 prop 需要使用v-on 和 v-bind[绑定dialogVisible]--> <el-dialog :visible="dialogVisible" :title="dialogTitle" width="30%" @close="handleClose" > <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" > <el-form-item label="用户名" prop="username"> <el-input v-model="form.username" :disabled="form.id==-1? false:'disabled'" /> <el-alert v-if="form.id==-1" title="默认初始密码:123456" :closable="false" style="line-height: 10px;" type="success" > </el-alert> </el-form-item> <el-form-item label="手机号" prop="phonenumber"> <el-input v-model="form.phonenumber" /> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="form.email" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-radio-group v-model="form.status"> <el-radio :label="'0'">正常</el-radio> <el-radio :label="'1'">禁用</el-radio> </el-radio-group> </el-form-item> <el-form-item label="备注" prop="remark"> <el-input v-model="form.remark" type="textarea" :rows="4"/> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleConfirm">确认</el-button> <el-button @click="handleClose" >取消</el-button > </span> </template> </el-dialog> </template> <style scoped lang="scss"> </style>
4.前端:src/views/sys/user/index.vue中引入对话框子组件并显示添加修改按钮
<script setup lang="ts"> import {ref, onMounted, reactive} from "vue" import type {QueryPageParams, QueryResponse, UserInfo} from "@/api/user/type" import {reqQueryList} from "@/api/user" import {baseUrl} from "@/util/request" import Dialog from './components/index.vue' let tableData = ref<UserInfo[]>([]) let queryForm = reactive<QueryPageParams>({ pageNum:1, pageSize:3, query:'' }) let total = ref(0) let dialogVisible = ref(false) let dialogTitle = ref("") let id = ref(-1) const initUserList = async (pager=1)=>{ queryForm.pageNum = pager let res:QueryResponse = await reqQueryList(queryForm) if (res.code == 200){ tableData.value = res.data.userList total.value = res.data.total } } const handleSizeChange = () => { initUserList(queryForm.pageNum) } const handleDialogValue = (userId:number) => { if(userId){ id.value = userId dialogTitle.value = '修改用户' } else { id.value = -1 dialogTitle.value = '新增用户' } dialogVisible.value = true } onMounted(()=>{ initUserList() }) </script> <template> <div class="app-container"> <el-row :gutter="20" class="header"> <el-col :span="7"> <el-input placeholder="请输入用户名..." v-model="queryForm.query" clearable ></el-input> </el-col> <el-button type="primary" icon="Search" @click="initUserList(1)">搜索</el-button> <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button> </el-row> <el-table border :data="tableData" stripe style="width: 100%"> <el-table-column type="selection" width="55" /> <el-table-column prop="avatar" label="头像" width="80" align="center"> <template v-slot="scope"> <img :src="baseUrl+'image/userAvatar/'+scope.row.avatar" width="50" height="50"/> </template> </el-table-column> <el-table-column prop="username" label="用户名" width="100" align="center"/> <el-table-column prop="roles" label="拥有角色" width="200" align="center"> <template v-slot="scope"> <el-tag size="small" type="warning" v-for="item in scope.row.sysRoleList"> { {item.name}} </el-tag> </template> </el-table-column> <el-table-column prop="email" label="邮箱" width="200" align="center"/> <el-table-column prop="phonenumber" label="手机号" width="120" align="center"/> <el-table-column prop="status" label="状态?" width="200" align="center"> <template v-slot="{row}" > <el-switch v-model="row.status" @change="statusChangeHandle(row)" active-text="正常" inactive-text="禁用" active-value="0" inactive-value="1"> </el-switch> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="200" align="center"/> <el-table-column prop="loginDate" label="最后登录时间" width="200" align="center"/> <el-table-column prop="remark" label="备注" /> <el-table-column prop="action" label="操作" width="400" fixed="right" align="center"> <template v-slot="scope" > <el-button type="primary" icon="Tools" >分配角色</el-button> <el-button v-if="scope.row.username != 'tom'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)">修改用户</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize" :page-sizes="[3, 5, 7, 10]" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="initUserList" /> </div> <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initUserList="initUserList"/> </template> <style lang="scss" scoped> .header{ padding-bottom: 16px; box-sizing: border-box; } .el-pagination{ float: right; padding: 20px; box-sizing: border-box; } ::v-deep th.el-table__cell{ word-break: break-word; background-color: #f8f8f9 !important; color: #515a6e; height: 40px; font-size: 13px; } .el-tag--small { margin-left: 5px; } </style>
5.浏览器测试
删除或批量删除
1.在SysUserController新增删除接口
@Resource private SysUserRoleService sysUserRoleService;/** * 删除或批量删除 * 需要删除两个表的数据 * */ @Transactional @PostMapping("/delete") @PreAuthorize("hasAuthority('system:user:delete')") public R delete(@RequestBody Long[] ids){ sysUserService.removeByIds(Arrays.asList(ids)); sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("user_id",ids)); return R.success(); }
2. 前端:在src/api/user下新增一个接口
// 用户相关的API接口 ... enum API { ... REMOVE_URL = 'sys/user/delete' } ... export const reqRemove = (ids:number[]) => request.post<any,any>(API.REMOVE_URL,ids)
3.修改src/views/sys/user/index.vue显示批量删除和单挑删除按钮及业务逻辑
<script setup lang="ts"> ... import {ElMessage} from "element-plus"; .... let multipleSelection = ref([]) ... const handleSelectionChange = (selection:any) => { console.log(selection) multipleSelection.value = selection delBtnStatus.value = selection.length == 0 } const handleDelete = async (id:any) =>{ let ids = [] if(id){ ids.push(id) } else { multipleSelection.value.forEach( row =>ids.push(row.id)) } const res = await reqRemove(ids) if(res.code==200){ ElMessage({ type: 'success', message: '执行成功!' }) initUserList(); }else{ ElMessage({ type: 'error', message: res.msg }) } } </script> <template> <div class="app-container"> <el-row :gutter="20" class="header"> <el-col :span="7"> <el-input placeholder="请输入用户名..." v-model="queryForm.query" clearable ></el-input> </el-col> <el-button type="primary" icon="Search" @click="initUserList(1)">搜索</el-button> <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button> <el-popconfirm title="您确定批量删除这些记录吗?" @confirm="handleDelete(null)"> <template #reference> <el-button type="danger" :disabled="delBtnStatus" icon="Delete" >批量删除</el-button> </template> </el-popconfirm> </el-row> <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange"> ... <el-table-column prop="action" label="操作" width="250" fixed="right" align="center"> <template v-slot="scope" > <el-button size="small" type="primary" icon="Tools" >分配角色</el-button> <el-button size="small" v-if="scope.row.username != 'tom'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)">修改用户</el-button> <el-popconfirm v-if="scope.row.username!='tom'" title="您确定要删除这条记录 吗?" @confirm="handleDelete(scope.row.id)"> <template #reference> <el-button size="small" type="danger" icon="Delete">删除用户</el-button> </template> </el-popconfirm> </template> </el-table-column> </el-table> ... </template> <style lang="scss" scoped> ... </style>
4.浏览器测试
重置密码和修改状态
1.在SysUserController新增重置密码和修改状态的接口
/** * 重置密码 * */ @GetMapping("/resetPassword/{id}") @PreAuthorize("hasAuthority('system:user:edit')") public R resetPassword(@PathVariable("id")Integer id){ SysUser sysUser = sysUserService.getById(id); sysUser.setPassword(bCryptPasswordEncoder.encode(Constant.DEFAULT_PASSWORD)); sysUser.setUpdateTime(new Date()); sysUserService.updateById(sysUser); return R.success(); } /** * 更新status状态 * */ @GetMapping("/updateStatus/{id}/status/{status}") @PreAuthorize("hasAuthority('system:user:edit')") public R updateStatus(@PathVariable("id")Integer id,@PathVariable("status")String status){ SysUser sysUser = sysUserService.getById(id); sysUser.setStatus(status); sysUserService.saveOrUpdate(sysUser); return R.success(); }
2.在common/constant/Constant.java存储默认密码
public class Constant { // 默认密码 public final static String DEFAULT_PASSWORD = "123456"; }
3. 前端:在src/api/user下新增两个接口
// 用户相关的API接口 ... enum API { ... RESET_PWD_URL = 'sys/user/resetPassword/', UPDATE_STATUS_URL = 'sys/user/updateStatus/' } ... export const reqResetPwd = (id:number) => request.get<any,any>(API.RESET_PWD_URL + id) export const reqUpdateStatus = (id:number,status:string) => request.get<any,any>(API.UPDATE_STATUS_URL + `${id}/status/${status}`)
4. 前端:修改src/views/sys/user/index.vue
<script setup lang="ts">
...
const handleResetPwd = async (id:number) => {
let res = await reqResetPwd(id)
if (res.code == 200){
ElMessage({
type:'success',
message:'执行成功'
})
} else {
ElMessage({
type:'error',
message:res.msg
})
}
}
const statusChangeHandle = async (row:any) => {
let res = await reqUpdateStatus(row.id,row.status)
if (res.code == 200){
ElMessage({
type:'success',
message:'执行成功'
})
} else {
ElMessage({
type:'error',
message:res.msg
})
}
}
onMounted(()=>{
initUserList()
})
</script>
<template>
<div class="app-container">
...
<el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
...
<el-table-column prop="status" label="状态?" width="200" align="center">
<template v-slot="{row}" >
<el-switch v-model="row.status"
@change="statusChangeHandle(row)"
active-text="正常"
inactive-text="禁用"
active-value="0"
inactive-value="1">
</el-switch>
</template>
</el-table-column>
...
<el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
<template v-slot="scope" >
<el-button size="small" type="primary" icon="Tools" >分配角色</el-button>
<el-button size="small" v-if="scope.row.username != 'tom'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)"></el-button>
<el-popconfirm v-if="scope.row.username!='tom'" :title="`您确定要删除【${scope.row.username}】吗?`" @confirm="handleDelete(scope.row.id)">
<template #reference>
<el-button size="small" type="danger" icon="Delete"></el-button>
</template>
</el-popconfirm>
<el-popconfirm v-if="scope.row.username!='tom'" :title="`您确定要对这个用户重置密码吗?`" @confirm="handleResetPwd(scope.row.id)">
<template #reference>
<el-button size="small" type="warning" icon="WarnTriangleFilled">重置密码</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
...
</template>
<style lang="scss" scoped>
...
</style>
5.浏览器测试
给用户分配角色
1.后端:在SysUserController新增分配角色接口
/** * 分配角色 * */ @Transactional @PostMapping("/grantRole/{userId}") @PreAuthorize("hasAuthority('system:user:role')") public R grantRole(@PathVariable("userId")Long userId,@RequestBody Long[] roleIds){ List<SysUserRole> userRoleList = new ArrayList<>(); Arrays.stream(roleIds).forEach(r -> { SysUserRole sysUserRole = new SysUserRole(); sysUserRole.setRoleId(r); sysUserRole.setUserId(userId); userRoleList.add(sysUserRole); }); // 先将当前用户关联的角色删除 sysUserRoleService.remove(new QueryWrapper<SysUserRole>().eq("user_id",userId)); // 再重新把新的管理的用户角色id设置进去 sysUserRoleService.saveBatch(userRoleList); return R.success(); }
2.后端:新建controller/SysRoleController.java
package com.jd.jdadmin.controller; import com.jd.jdadmin.entity.R; import com.jd.jdadmin.entity.SysRole; import com.jd.jdadmin.service.SysRoleService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author 叶蕊 * @ClassName SysRoleController * @description: TODO * @date 2025年 01月 17日 * @version: 1.0 */ @RestController @RequestMapping("/sys/role") public class SysRoleController { @Resource private SysRoleService sysRoleService; /* * 查询所有角色信息 * */ @GetMapping("/listAll") @PreAuthorize("hasAuthority('system:role:query')") public R listAll(){ Map<String,Object> resultMap = new HashMap<>(); List<SysRole> roleList = sysRoleService.list(); resultMap.put("roleList",roleList); return R.success(resultMap); } }
3.前端: 在src/api/user下新增两个接口
// 用户相关的API接口 ... ... enum API { ... GRANT_ROLE_URL = 'sys/user/grantRole/', ROLE_LIST_URL = 'sys/role/listAll' } ... export const reqGrantRole = (userId:number,roleIds:number[]) => request.post<any,any>(API.GRANT_ROLE_URL + userId,roleIds ) export const reqRoleList = () => request.get<any,any>(API.ROLE_LIST_URL)
4.前端:在src/views/sys/user/components下新增roleDialog.vue 显示分配角色的对话框
<script setup lang="ts"> import {defineEmits, defineProps, ref, watch} from "vue" import { ElMessage } from 'element-plus' import {reqGrantRole, reqRoleList} from "@/api/user"; const props = defineProps( { id :{ type:Number, default:-1, required:true }, roleDialogVisible:{ type:Boolean, default:false, required:true }, sysRoleList :{ type:Array, default:[], required:true } }) const form=ref({ id:-1, roleList:[], checkedRoles:[] }) const formRef=ref() const initFormData=async(id:number)=>{ const res= await reqRoleList() console.log(res) form.value.roleList=res.data.roleList; form.value.id=id; } watch(()=>props.roleDialogVisible, ()=>{ let id=props.id; console.log("id="+id) if(id!=-1){ form.value.checkedRoles=[] props.sysRoleList.forEach(item => form.value.checkedRoles.push(item.id)) initFormData(id) } }) const emits=defineEmits(['update:modelValue','initUserList']) const handleClose=()=>{ emits('update:modelValue',false) } const handleConfirm = async ()=>{ await formRef.value.validate() let res = await reqGrantRole(form.value.id,form.value.checkedRoles) if(res.code==200){ ElMessage.success("执行成功!") emits("initUserList") handleClose(); }else{ ElMessage.error(res.msg); } } </script> <template> <el-dialog :visible="roleDialogVisible" title="分配角色" width="30%" @close="handleClose" > <el-form ref="formRef" :model="form" label-width="100px" > <el-checkbox-group v-model="form.checkedRoles"> <el-checkbox v-for="role in form.roleList" :id="role.id" :key="role.id" :label="role.id" name="checkedRoles" > { {role.name}} </el-checkbox> </el-checkbox-group> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleConfirm">确认</el-button> <el-button @click="handleClose">取消</el-button> </span> </template> </el-dialog> </template> <style scoped lang="scss"> </style>
5.前端:在src/views/sys/user/index.vue引入RoleDialog子组件 实现点击分配角色显示对话框
<script setup lang="ts"> ... const handleRoleDialogValue = (userId:number,roleList:any) => { console.log(userId) id.value = userId sysRoleList.value = roleList roleDialogVisible.value = true } ... </script> <template> <div class="app-container"> ... <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange"> ... <el-table-column prop="action" label="操作" width="400" fixed="right" align="center"> <template v-slot="scope" > <el-button size="small" type="primary" icon="Tools" @click="handleRoleDialogValue(scope.row.id,scope.row.sysRoleList)">分配角色</el-button> ... </el-table-column> </el-table> ... <RoleDialog v-model="roleDialogVisible" :sysRoleList="sysRoleList" :roleDialogVisible="roleDialogVisible" :id="id" @initUserList="initUserList"/> </template> <style lang="scss" scoped> ... </style>
6.浏览器测试
角色管理实现
列表分页显示[根据用户管理模块做相应修改即可]
1.前端:复制user下的文件及文件夹到role中
2.后端:复制SysUserController的分页查询接口到SysRoleController中 做相应处理即可
/** * 根据条件分页查询角色信息 */ @PostMapping("/list") @PreAuthorize("hasAuthority('system:role:query')") public R list(@RequestBody PageBean pageBean){ String query = pageBean.getQuery().trim(); LambdaQueryWrapper<SysRole> queryWrapper = new LambdaQueryWrapper<>(); if (StrUtil.isNotEmpty(query)){ queryWrapper.like(SysRole::getName,query); } Page<SysRole> sysUserPage = new Page<>(pageBean.getPageNum(), pageBean.getPageSize()); Page<SysRole> pageResult = sysRoleService.page(sysUserPage,queryWrapper); // 数据集合 List<SysRole> roleList = pageResult.getRecords(); Map<String,Object> resultMap = new HashMap<>(); resultMap.put("roleList",roleList); resultMap.put("total",pageResult.getTotal()); return R.success(resultMap); }
3.前端:在src/api下创建role文件夹及index.ts和type.ts
index.ts
import request from "@/util/request"; import type {QueryPageParams, QueryResponse, RoleInfo} from "@/api/role/type"; enum API { QUERY_URL = 'sys/role/list' } export const reqQueryList = (data:QueryPageParams) => request.post<any,QueryResponse>(API.QUERY_URL,data)
type.ts
export interface ResponseData { msg:string code:number } export interface RoleInfo { id?: number, createTime?: string, updateTime?: string, remark?: string, name: string, code: string } export interface QueryPageParams { pageNum:number, pageSize:number, start?:number, query?:string } export interface QueryResponse extends ResponseData { data:{ total:number, roleList:RoleInfo[] } }
4.浏览器测试
添加或修改[根据用户管理模块做相应修改即可]
1.后端:复制SysUserController的添加或修改接口及根据id查询角色信息接口到SysRoleController中 做相应处理即可
/** * 添加或修改 * */ @RequestMapping("saveOrUpdate") @PreAuthorize("hasAuthority('system:role:add')"+"||"+"hasAuthority('system:role:edit')") public R saveOrUpdate(@RequestBody SysRole sysRole){ if (sysRole.getId() == null || sysRole.getId() == -1){ sysRole.setCreateTime(new Date()); sysRoleService.save(sysRole); }else { sysRole.setUpdateTime(new Date()); sysRoleService.updateById(sysRole); } return R.success(); } /** * 根据id查询 * */ @GetMapping("/{id}") @PreAuthorize("hasAuthority('system:role:query')") public R findById(@PathVariable("id") Integer id){ SysRole sysRole = sysRoleService.getById(id); Map<String, Object> map = new HashMap<>(); map.put("sysRole",sysRole); return R.success(map); }
2.前端:在src/api/role/index.ts中新增接口
... enum API { ... ADD_OR_UPDATE_URL = 'sys/role/saveOrUpdate', FIND_BY_ID_URL = 'sys/role/' } ... export const reqQueryList = (data:QueryPageParams) => request.post<any,QueryResponse>(API.QUERY_URL,data) export const reqRemove = (ids:number[]) => request.post<any,any>(API.REMOVE_URL,ids)
3.前端:修改src/views/role/components/index.vue
<script setup lang="ts"> import {defineEmits, defineProps,ref,watch } from "vue" import { ElMessage } from 'element-plus' import {reqAddOrUpdate,reqFindById} from "@/api/role"; import type {RoleInfo} from "@/api/role/type"; let props = defineProps( { id:{ type:Number, default:-1, required:true }, dialogTitle:{ type:String, default:'', required:true }, dialogVisible:{ type:Boolean, default:false, required:true } }) let formRef = ref() let form = ref<RoleInfo>({ id:-1, name:"", remark:"", code:'' }) let rules = ref({ name:[ { required: true, message: '请输入角色名称'} ], code:[ { required: true, message: '请输入权限字符'} ] }) const initFormData = async (id:number) =>{ const res = await reqFindById(id) form.value = res.data.sysRole; } watch(() => props.dialogVisible, () => { if (formRef.value){ // 首次打开对话框时formRef暂未初始化 formRef.value.clearValidate('code') formRef.value.clearValidate('name') } let id = props.id; if(id!=-1){ initFormData(id); }else{ form.value= { id:-1, name:"", remark:"", code:'' } } }) const emits = defineEmits(['update:modelValue','initRoleList']) const handleClose = ()=>{ emits('update:modelValue',false) } const handleConfirm = async ()=>{ await formRef.value.validate() let res = await reqAddOrUpdate(form.value) if(res.code==200){ ElMessage.success("执行成功!") formRef.value.resetFields(); emits("initRoleList") handleClose(); }else{ ElMessage.error(res.msg); } } </script> <template> <!--不要在 dialogVisible 上使用 v-model,因为它是一个 prop 需要使用v-on 和 v-bind[绑定dialogVisible]--> <el-dialog :visible="dialogVisible" :title="dialogTitle" width="30%" @close="handleClose" > <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" > <el-form-item label="角色名称" prop="name"> <el-input v-model="form.name" :disabled="form.id==-1? false:'disabled'" /> </el-form-item> <el-form-item label="权限字符" prop="code"> <el-input v-model="form.code" /> </el-form-item> <el-form-item label="备注" prop="remark"> <el-input v-model="form.remark" type="textarea" :rows="4"/> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleConfirm">确认</el-button> <el-button @click="handleClose" >取消</el-button > </span> </template> </el-dialog> </template> <style scoped lang="scss"> </style>
4.浏览器测试
删除或批量删除[根据用户管理模块做相应修改即可]
1.后端:复制SysUserController的删除或批量删除接口到SysRoleController中 做相应处理即可
@Resource private SysUserRoleService sysUserRoleService;/** * 删除或批量删除 * 需要删除两个表的数据 * */ @Transactional @PostMapping("/delete") @PreAuthorize("hasAuthority('system:role:delete')") public R delete(@RequestBody Long[] ids){ sysRoleService.removeByIds(Arrays.asList(ids)); sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("role_id",ids)); return R.success(); }
2.前端:在src/api/role/index.ts中新增接口
... enum API { ... REMOVE_URL = 'sys/role/delete' } ... export const reqFindById = (id:number) => request.get<any,any>(API.FIND_BY_ID_URL + id)
3.前端:src/views/role/index.vue
<script setup lang="ts"> import {ref, onMounted, reactive} from "vue" import type {QueryPageParams, QueryResponse, RoleInfo} from "@/api/role/type" import {reqQueryList, reqRemove} from "@/api/role" import Dialog from './components/index.vue' import {ElMessage} from "element-plus" import RoleDialog from "@/views/sys/role/components/roleDialog.vue" let tableData = ref<RoleInfo[]>([]) let queryForm = reactive<QueryPageParams>({ pageNum:1, pageSize:3, query:'' }) let total = ref(0) let dialogVisible = ref(false) let dialogTitle = ref("") let id = ref(-1) let delBtnStatus = ref(true) let multipleSelection = ref([]) let sysRoleList=ref([]) let roleDialogVisible=ref(false) const initRoleList = async (pager=1)=>{ queryForm.pageNum = pager let res:QueryResponse = await reqQueryList(queryForm) if (res.code == 200){ tableData.value = res.data.roleList total.value = res.data.total } } const handleSizeChange = () => { initRoleList(queryForm.pageNum) } const handleDialogValue = (roleId:number) => { if(roleId){ id.value = roleId dialogTitle.value = '修改角色' } else { id.value = -1 dialogTitle.value = '新增角色' } dialogVisible.value = true } const handleSelectionChange = (selection:any) => { console.log(selection) multipleSelection.value = selection delBtnStatus.value = selection.length == 0 } const handleDelete = async (id:any) =>{ let ids = [] if(id){ ids.push(id) } else { multipleSelection.value.forEach( row => ids.push(row.id)) } const res = await reqRemove(ids) if(res.code==200){ ElMessage({ type: 'success', message: '执行成功!' }) initRoleList(); }else{ ElMessage({ type: 'error', message: res.msg }) } } const handleRoleDialogValue = (roleId:number,roleList:any) => { console.log(roleId) id.value = roleId sysRoleList.value = roleList roleDialogVisible.value = true } onMounted(()=>{ initRoleList() }) </script> <template> <div class="app-container"> <el-row :gutter="20" class="header"> <el-col :span="7"> <el-input placeholder="请输入角色名..." v-model="queryForm.query" clearable ></el-input> </el-col> <el-button type="primary" icon="Search" @click="initRoleList(1)">搜索</el-button> <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button> <el-popconfirm title="您确定批量删除这些记录吗?" @confirm="handleDelete(null)"> <template #reference> <el-button type="danger" :disabled="delBtnStatus" icon="Delete" >批量删除</el-button> </template> </el-popconfirm> </el-row> <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" /> <el-table-column prop="name" label="角色名" width="100" align="center"/> <el-table-column prop="code" label="权限字符" width="200" align="center"/> <el-table-column prop="createTime" label="创建时间" width="200" align="center"/> <el-table-column prop="remark" label="备注" /> <el-table-column prop="action" label="操作" width="400" fixed="right" align="center"> <template v-slot="scope" > <el-button size="small" type="primary" icon="Tools" @click="handleRoleDialogValue(scope.row.id,scope.row.sysRoleList)">分配权限</el-button> <el-button size="small" v-if="scope.row.code != 'admin'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)"></el-button> <el-popconfirm v-if="scope.row.code!='admin'" :title="`您确定要删除【${scope.row.name}】吗?`" @confirm="handleDelete(scope.row.id)"> <template #reference> <el-button size="small" type="danger" icon="Delete"></el-button> </template> </el-popconfirm> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryForm.pageNum" v-model:page-size="queryForm.pageSize" :page-sizes="[3, 5, 7, 10]" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="initRoleList" /> </div> <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initRoleList="initRoleList"/> <RoleDialog v-model="roleDialogVisible" :sysRoleList="sysRoleList" :roleDialogVisible="roleDialogVisible" :id="id" @initRoleList="initRoleList"/> </template> <style lang="scss" scoped> .header{ padding-bottom: 16px; box-sizing: border-box; } .el-pagination{ float: right; padding: 20px; box-sizing: border-box; } ::v-deep th.el-table__cell{ word-break: break-word; background-color: #f8f8f9 !important; color: #515a6e; height: 40px; font-size: 13px; } .el-tag--small { margin-left: 5px; } </style>
4.浏览器测试
显示权限菜单树
1.后端:在controller下创建SysMenuController控制器并提供返回所有的菜单树的接口
@RestController @RequestMapping("/sys/menu") public class SysMenuController { @Resource private SysMenuService sysMenuService; /** * 返回所有的菜单树 * */ @RequestMapping("/treeList") @PreAuthorize("hasAuthority('system:menu:query')") public R treeList(){ List<SysMenu> menuList = sysMenuService.list(new QueryWrapper<SysMenu>().orderByAsc("order_num")); List<SysMenu> treeMenu = sysMenuService.buildTreeMenu(menuList); return R.success().put("treeMenu",sysMenuService.buildTreeMenu(treeMenu)); } }
2.前端:在src/api/role 新增接口和接口类型
index.ts ... enum API { ... TREE_MENU_URL = 'sys/menu/treeList' } ... export const reqTreeMenu = () => request.get<any,TreeMenuData>(API.TREE_MENU_URL)
type.ts export interface MenuInfo { id:number, name:string, icon: string, orderNum: number, updateTime: number, remark: string, parentId: number, path: string, component: string, children: MenuInfo[] } export interface TreeMenuData extends ResponseData { data: { treeMenu:MenuInfo[] } }
3.前端:修改src/views/role/index.vue
<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import type {QueryPageParams, QueryResponse, RoleInfo} from "@/api/role/type"
import {reqQueryList, reqRemove} from "@/api/role"
import Dialog from './components/index.vue'
import {ElMessage} from "element-plus"
import MenuDialog from "@/views/sys/role/components/menuDialog.vue"
let tableData = ref<RoleInfo[]>([])
let queryForm = reactive<QueryPageParams>({
pageNum:1,
pageSize:3,
query:''
})
let total = ref(0)
let dialogVisible = ref(false)
let dialogTitle = ref("")
let id = ref(-1)
let delBtnStatus = ref(true)
let multipleSelection = ref([])
let menuDialogVisible=ref(false)
const initRoleList = async (pager=1)=>{
queryForm.pageNum = pager
let res:QueryResponse = await reqQueryList(queryForm)
if (res.code == 200){
tableData.value = res.data.roleList
total.value = res.data.total
}
}
const handleSizeChange = () => {
initRoleList(queryForm.pageNum)
}
const handleDialogValue = (roleId:number) => {
if(roleId){
id.value = roleId
dialogTitle.value = '修改角色'
} else {
id.value = -1
dialogTitle.value = '新增角色'
}
dialogVisible.value = true
}
const handleSelectionChange = (selection:any) => {
console.log(selection)
multipleSelection.value = selection
delBtnStatus.value = selection.length == 0
}
const handleDelete = async (id:any) =>{
let ids = []
if(id){
ids.push(id)
} else {
multipleSelection.value.forEach( row => ids.push(row.id))
}
const res = await reqRemove(ids)
if(res.code==200){
ElMessage({ type: 'success', message: '执行成功!' })
initRoleList();
}else{
ElMessage({ type: 'error', message: res.msg })
}
}
const handleMenuDialogValue = (userId:number) => {
console.log(userId)
id.value = userId
menuDialogVisible.value = true
}
onMounted(()=>{
initRoleList()
})
</script>
<template>
<div class="app-container">
<el-row :gutter="20" class="header">
<el-col :span="7">
<el-input placeholder="请输入角色名..." v-model="queryForm.query" clearable ></el-input>
</el-col>
<el-button type="primary" icon="Search" @click="initRoleList(1)">搜索</el-button>
<el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
<el-popconfirm title="您确定批量删除这些记录吗?" @confirm="handleDelete(null)">
<template #reference>
<el-button type="danger" :disabled="delBtnStatus" icon="Delete" >批量删除</el-button>
</template>
</el-popconfirm>
</el-row>
<el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="角色名" width="100" align="center"/>
<el-table-column prop="code" label="权限字符" width="200" align="center"/>
<el-table-column prop="createTime" label="创建时间" width="200" align="center"/>
<el-table-column prop="remark" label="备注" />
<el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
<template v-slot="scope" >
<el-button size="small" type="primary" icon="Tools" @click="handleMenuDialogValue(scope.row.id)">分配权限</el-button>
<el-button size="small" v-if="scope.row.code != 'admin'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)"></el-button>
<el-popconfirm v-if="scope.row.code!='admin'" :title="`您确定要删除【${scope.row.name}】吗?`" @confirm="handleDelete(scope.row.id)">
<template #reference>
<el-button size="small" type="danger" icon="Delete"></el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryForm.pageNum"
v-model:page-size="queryForm.pageSize"
:page-sizes="[3, 5, 7, 10]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="initRoleList"
/>
</div>
<Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initRoleList="initRoleList"/>
<MenuDialog v-model="menuDialogVisible" :menuDialogVisible="menuDialogVisible" :id="id" @initRoleList="initRoleList"/>
</template>
<style lang="scss" scoped>
.header{
padding-bottom: 16px;
box-sizing: border-box;
}
.el-pagination{
float: right;
padding: 20px;
box-sizing: border-box;
}
::v-deep th.el-table__cell{
word-break: break-word;
background-color: #f8f8f9 !important;
color: #515a6e;
height: 40px;
font-size: 13px;
}
.el-tag--small {
margin-left: 5px;
}
</style>
4.前端:修改src/views/role/components/menuDialog.vue
<script setup lang="ts"> import {defineEmits, defineProps, ref, watch} from "vue" import { ElMessage } from 'element-plus' import {reqTreeMenu} from "@/api/role"; let props = defineProps( { id :{ type:Number, default:-1, required:true }, menuDialogVisible:{ type:Boolean, default:false, required:true } }) let form=ref({ id:-1 }) let formRef=ref() let treeData=ref([]) let defaultProps = { children: 'children', label: 'name' } let treeRef = ref() const initFormData=async(id:number)=>{ const res= await reqTreeMenu() console.log(res) treeData.value = res.data.treeMenu form.value.id=id; } watch(()=>props.menuDialogVisible, ()=>{ let id=props.id; console.log("id="+id) if(id!=-1){ initFormData(id) } }) const emits=defineEmits(['update:modelValue','initRoleList']) const handleClose=()=>{ emits('update:modelValue',false) } const handleConfirm = async ()=>{ await formRef.value.validate() // let res = await reqGrantMenu(form.value.id,form.value.checkedMenus) // if(res.code==200){ // ElMessage.success("执行成功!") // emits("initRoleList") // handleClose(); // }else{ // ElMessage.error(res.msg); // } } </script> <template> <el-dialog :visible="menuDialogVisible" title="分配权限" width="30%" @close="handleClose" > <el-form ref="formRef" :model="form" label-width="100px" > <el-tree ref="treeRef" :data="treeData" :props="defaultProps" show-checkbox :default-expand-all="true" node-key="id" :check-strictly="true" /> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleConfirm">确认</el-button> <el-button @click="handleClose">取消</el-button> </span> </template> </el-dialog> </template> <style scoped lang="scss"> </style>
5.浏览器测试
*分配权限的实现
1.后端:在SysMenuController提供根据id查询菜单权限列表的接口和给角色分配权限的接口
@Resource private SysRoleMenuService sysRoleMenuService;/** * 获取当前角色的权限菜单id集合[即当前角色拥有的权限] * */ @GetMapping("/menus/{id}") @PreAuthorize("hasAuthority('system:role:menu')") public R menus(@PathVariable("id") Long id){ List<SysRoleMenu> roleMenuList = sysRoleMenuService.list(new QueryWrapper<SysRoleMenu>().eq("role_id", id)); List<Long> menuIds = roleMenuList.stream().map(i -> i.getMenuId()).collect(Collectors.toList()); return R.success().put("menuIds",menuIds); } /** * 给角色分配权限 * */ @Transactional @PostMapping("/update/menus/{id}") @PreAuthorize("hasAuthority('system:role:menu')") public R updateMenus(@PathVariable("id")Long id,@RequestBody Long[] menuIds){ sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("role_id",id)); List<SysRoleMenu> sysRoleMenus = new ArrayList<>(); Arrays.stream(menuIds).forEach(menuId -> { SysRoleMenu sysRoleMenu = new SysRoleMenu(); sysRoleMenu.setRoleId(id); sysRoleMenu.setMenuId(menuId); sysRoleMenus.add(sysRoleMenu); }); sysRoleMenuService.saveBatch(sysRoleMenus); return R.success(); }
2.前端:在src/api/role/index.ts 新增接口
... enum API { ... HAS_MENU_URL = 'sys/role/menus/', GRANT_MENU_URL = 'sys/role/update/menus/' } ... export const reqHasMenu = (id:number) => request.get<any,any>(API.HAS_MENU_URL + id) export const reqGrantMenu = (id:number,menuIds:number[]) => request.post<any,any>(API.GRANT_MENU_URL + id,menuIds)
3.前端: 修改src/views/role/components/menuDialog.vue
<script setup lang="ts"> import {defineEmits, defineProps, ref, watch} from "vue" import { ElMessage } from 'element-plus' import {reqGrantMenu, reqHasMenu, reqTreeMenu} from "@/api/role"; let props = defineProps( { id :{ type:Number, default:-1, required:true }, menuDialogVisible:{ type:Boolean, default:false, required:true } }) let form=ref({ id:-1 }) let formRef=ref() let treeData=ref([]) let defaultProps = { children: 'children', label: 'name' } let treeRef = ref() const initFormData= async(id:number)=>{ let res= await reqTreeMenu() treeData.value = res.data.treeMenu form.value.id=id let res2 = await reqHasMenu(id) console.log(res2) // 通过 key 设置某个节点的当前选中状态,使用此方法必须设置 node-key 属性 treeRef.value.setCheckedKeys(res2.data.menuIds) } watch(()=>props.menuDialogVisible, ()=>{ let id=props.id; console.log("id="+id) if(id!=-1){ initFormData(id) } }) const emits=defineEmits(['update:modelValue','initRoleList']) const handleClose=()=>{ emits('update:modelValue',false) } const handleConfirm = async ()=>{ await formRef.value.validate() // 获取所有选中的节点的key[权限id] let menuIds = treeRef.value.getCheckedKeys() // 分配权限 let res = await reqGrantMenu(form.value.id,menuIds) if(res.code==200){ ElMessage.success("执行成功!") emits("initRoleList") handleClose(); }else{ ElMessage.error(res.msg); } } </script> <template> <el-dialog :visible="menuDialogVisible" title="分配权限" width="30%" @close="handleClose" > <el-form ref="formRef" :model="form" label-width="100px" > <el-tree ref="treeRef" :data="treeData" :props="defaultProps" show-checkbox :default-expand-all="true" node-key="id" :check-strictly="true" /> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleConfirm">确认</el-button> <el-button @click="handleClose">取消</el-button> </span> </template> </el-dialog> </template> <style scoped lang="scss"> </style>
4.浏览器测试
菜单管理实现
树形表格菜单信息显示
1.前端:将src/views/sys/user下的文件夹及文件拷贝到menu中 将roleDialog.vue删除
2.前端: 修改src/views/sys/menu/index.ts
<script setup lang="ts"> import {ref, onMounted, reactive} from "vue" import Dialog from './components/index.vue' import {ElMessage} from "element-plus" import {reqTreeMenu} from "@/api/role"; let tableData=ref([]) let dialogVisible=ref(false) let dialogTitle=ref("") let id=ref(-1) const initMenuList= async()=>{ const res= await reqTreeMenu() console.log() tableData.value = res.data.treeMenu } const handleDialogValue=(userId:number)=>{ if(userId){ id.value=userId; dialogTitle.value="用户修改" }else{ id.value=-1; dialogTitle.value="用户添加" } dialogVisible.value=true } const handleDelete=async (id:number)=>{ /*let ids = [] if(id){ ids.push(id) }else{ multipleSelection.value.forEach(row=>{ ids.push(row.id) }) } const res=await requestUtil.post("sys/user/delete",ids) if(res.data.code==200){ ElMessage({ type: 'success', message: '执行成功!' }) initMenuList(); }else{ ElMessage({ type: 'error', message: res.data.msg }) }*/ } onMounted(()=>{ initMenuList() }) </script> <template> <div class="app-container"> <el-row class="header"> <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button> </el-row> <el-table :data="tableData" row-key="id" stripe style="width: 100%; margin-bottom: 20px;" border default-expand-all :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"> <el-table-column prop="name" label="菜单名称" width="200"/> <el-table-column prop="icon" label="图标" width="70" align="center"> <template v-slot="scope"> <el-icon v-if="scope.row.icon != '#'"> <component :is="scope.row.icon"/> </el-icon> </template> </el-table-column> <el-table-column prop="orderNum" label="排序" width="70" align="center"/> <el-table-column prop="perms" label="权限标识" width="200"/> <el-table-column prop="path" label="组件路径" width="180"/> <el-table-column prop="menuType" label="菜单类型" width="120" align="center"> <template v-slot="scope"> <el-tag size="small" v-if="scope.row.menuType === 'M'" type="danger" effect="dark">目录</el-tag> <el-tag size="small" v-else-if="scope.row.menuType === 'C'" type="success" effect="dark">菜单</el-tag> <el-tag size="small" v-else-if="scope.row.menuType === 'F'" type="warning" effect="dark">按钮</el-tag> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" align="center"/> <el-table-column prop="action" label="操作" width="400" fixed="right" align="center"> <template v-slot="scope" > <el-button type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)" /> <el-popconfirm title="您确定要删除这条记录吗?" @confirm="handleDelete(scope.row.id)"> <template #reference> <el-button type="danger" icon="Delete"/> </template> </el-popconfirm> </template> </el-table-column> </el-table> </div> <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initMenuList="initMenuList"/> </template> <style lang="scss" scoped> .header{ padding-bottom: 16px; box-sizing: border-box; } .el-pagination{ float: right; padding: 20px; box-sizing: border-box; } ::v-deep th.el-table__cell{ word-break: break-word; background-color: #f8f8f9 !important; color: #515a6e; height: 40px; font-size: 13px; } .el-tag--small { margin-left: 5px; } </style>
3.浏览器测试[因为后台已经写好了所以只需要改前端页面]
修改和添加功能
1.后端:在SysMenuController提供添加或修改接口及根据id查询菜单接口
/** * 添加或修改 * */ @RequestMapping("saveOrUpdate") @PreAuthorize("hasAuthority('system:menu:add')"+"||"+"hasAuthority('system:menu:edit')") public R saveOrUpdate(@RequestBody SysMenu sysMenu){ if (sysMenu.getId() == null || sysMenu.getId() == -1){ sysMenu.setCreateTime(new Date()); sysMenuService.save(sysMenu); }else { sysMenu.setUpdateTime(new Date()); sysMenuService.updateById(sysMenu); } return R.success(); } /** * 根据id查询 * */ @GetMapping("/{id}") @PreAuthorize("hasAuthority('system:menu:query')") public R findById(@PathVariable("id") Integer id){ SysMenu sysMenu = sysMenuService.getById(id); Map<String, Object> map = new HashMap<>(); map.put("sysMenu",sysMenu); return R.success(map); }
2.前端: 在src/api/menu/index.ts 新增接口
index.ts import request from "@/util/request"; import type {MenuInfo} from "@/api/menu/type" enum API { ADD_OR_UPDATE_URL = 'sys/menu/saveOrUpdate', FIND_BY_ID_URL = 'sys/menu/' } export const reqAddOrUpdate = (data:MenuInfo) => request.post<any,any>(API.ADD_OR_UPDATE_URL,data) export const reqRemove = (id:number) => request.get<any,any>(API.REMOVE_URL + id)
type.ts export interface MenuInfo { id:number, name:string, icon: string, orderNum: number, updateTime: number, remark: string, parentId: number, path: string, component: string, children: MenuInfo[] }
3.前端: 修改src/views/sys/menu/components/index.vue
<script setup> import {defineEmits, defineProps, ref, watch} from "vue" import { ElMessage } from 'element-plus' import {reqAddOrUpdate, reqFindById} from "@/api/menu"; const tableData=ref([]) const props= defineProps( { id:{ type:Number, default:-1, required:true }, dialogTitle:{ type:String, default:'', required:true }, dialogVisible:{ type:Boolean, default:false, required:true }, tableData:{ type:Array, default:[], required:true } }) const form=ref({ id:-1, parentId:'', menuType:"M", icon:'', name:'', perms:'', component:'', orderNum:1 }) const rules=ref({ parentId:[{ required: true, message: '请选择上级菜单'}], name: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }] }) const formRef=ref(null) const initFormData = async(id)=>{ const res = await reqFindById(id) form.value = res.data.sysMenu } watch(()=>props.dialogVisible, ()=>{ let id=props.id tableData.value = props.tableData if(id!=-1){ initFormData(id) }else{ form.value = { id:-1, parentId:'', menuType:"M", icon:'', name:'', perms:'', component:'', orderNum:1 } } }) const emits=defineEmits(['update:modelValue','initMenuList']) const handleClose= ()=>{ emits('update:modelValue',false) } const handleConfirm=()=>{ formRef.value.validate(async(valid)=>{ if(valid){ let result=await reqAddOrUpdate(form.value) if(result.code==200){ ElMessage.success("执行成功!") formRef.value.resetFields() emits("initMenuList") handleClose() }else { ElMessage.error(result.msg); } }else{ console.log("fail") } }) } </script> <template> <el-dialog model-value="dialogVisible" :title="dialogTitle" width="30%" @close="handleClose" > <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form-item label="上级菜单" prop="parentId"> <el-select v-model="form.parentId" placeholder="请选择上级菜单"> <template v-for="item in tableData"> <el-option :label="item.name" :value="item.id"></el-option> <template v-for="child in item.children"> <el-option :label="child.name" :value="child.id"> <span>{ { " -- " + child.name }}</span> </el-option> </template> </template> </el-select> </el-form-item> <el-form-item label="菜单类型" prop="menuType" label-width="100px"> <el-radio-group v-model="form.menuType"> <el-radio :label="'M'">目录</el-radio> <el-radio :label="'C'">菜单</el-radio> <el-radio :label="'F'">按钮</el-radio> </el-radio-group> </el-form-item> <el-form-item label="菜单图标" prop="icon"> <el-input v-model="form.icon" /> </el-form-item> <el-form-item label="菜单名称" prop="name"> <el-input v-model="form.name" /> </el-form-item> <el-form-item label="权限标识" prop="perms"> <el-input v-model="form.perms" /> </el-form-item> <el-form-item label="组件路径" prop="component"> <el-input v-model="form.component" /> </el-form-item> <el-form-item label="显示顺序" prop="orderNum"> <el-input-number v-model="form.orderNum" :min="1" label="显示顺序"></el-input-number> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleConfirm">确认</el-button> <el-button @click="handleClose">取消</el-button> </span> </template> </el-dialog> </template> <style scoped> </style>
4.前端:修改 src/views/sys/menu/index.vue
<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import Dialog from './components/index.vue'
import {ElMessage} from "element-plus"
import {reqTreeMenu} from "@/api/role";
import {reqRemove} from "@/api/menu";
let tableData=ref([])
let dialogVisible=ref(false)
let dialogTitle=ref("")
let id=ref(-1)
const initMenuList= async()=>{
const res= await reqTreeMenu()
console.log()
tableData.value = res.data.treeMenu
}
const handleDialogValue=(userId:number)=>{
if(userId){
id.value=userId;
dialogTitle.value="用户修改"
}else{
id.value=-1;
dialogTitle.value="用户添加"
}
dialogVisible.value=true
}
const handleDelete=async (id:number)=>{
...
}
onMounted(()=>{
initMenuList()
})
</script>
<template>
<div class="app-container">
<el-row class="header">
<el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
</el-row>
<el-table :data="tableData" row-key="id" stripe
style="width: 100%; margin-bottom: 20px;"
border default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
<el-table-column prop="name" label="菜单名称" width="200"/>
<el-table-column prop="icon" label="图标" width="70" align="center">
<template v-slot="scope">
<el-icon v-if="scope.row.icon != '#'">
<component :is="scope.row.icon"/>
</el-icon>
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="70" align="center"/>
<el-table-column prop="perms" label="权限标识" width="200"/>
<el-table-column prop="path" label="组件路径" width="180"/>
<el-table-column prop="menuType" label="菜单类型" width="120" align="center">
<template v-slot="scope">
<el-tag size="small" v-if="scope.row.menuType === 'M'" type="danger" effect="dark">目录</el-tag>
<el-tag size="small" v-else-if="scope.row.menuType === 'C'" type="success" effect="dark">菜单</el-tag>
<el-tag size="small" v-else-if="scope.row.menuType === 'F'" type="warning" effect="dark">按钮</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" align="center"/>
<el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
<template v-slot="scope" >
<el-button type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)" />
<el-popconfirm title="您确定要删除这条记录吗?" @confirm="handleDelete(scope.row.id)">
<template #reference>
<el-button type="danger" icon="Delete"/>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<Dialog v-model="dialogVisible" :tableData="tableData" :dialogVisible="dialogVisible" :id="id" :dialogTitle="dialogTitle" @initMenuList="initMenuList"/>
</template>
<style lang="scss" scoped>
.header{
padding-bottom: 16px;
box-sizing: border-box;
}
.el-pagination{
float: right;
padding: 20px;
box-sizing: border-box;
}
::v-deep th.el-table__cell{
word-break: break-word;
background-color: #f8f8f9 !important;
color: #515a6e;
height: 40px;
font-size: 13px;
}
.el-tag--small {
margin-left: 5px;
}
</style>
5.浏览器测试
删除功能
1.后端:在SysMenuController提供删除菜单接口
/** * 删除 * 需要删除两个表的数据 * */ @GetMapping("/delete/{id}") @PreAuthorize("hasAuthority('system:menu:delete')") public R delete(@PathVariable("id") Long id){ int count = sysMenuService.count(new QueryWrapper<SysMenu>().eq("parent_id", id)); if (count > 0){// 若有子菜单则不能直接删除 return R.fail("请先删除子菜单"); } sysMenuService.removeById(id); return R.success(); }
2.前端:在src/api/menu/index.ts 新增接口
... enum API { REMOVE_URL = 'sys/menu/delete/' } ... export const reqFindById = (id:number) => request.get<any,any>(API.FIND_BY_ID_URL + id)
3.前端: 修改src/views/sys/menu/index.vue
<script setup lang="ts">
...
const handleDelete=async (id:number)=>{
const res=await reqRemove(id)
if(res.code==200){
ElMessage({
type: 'success',
message: '执行成功!'
})
initMenuList();
}else{
ElMessage({
type: 'error',
message: res.msg
})
}
}
onMounted(()=>{
initMenuList()
})
</script>
<template>
<div class="app-container">
...
<el-table :data="tableData" row-key="id" stripe
style="width: 100%; margin-bottom: 20px;"
border default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
...
<el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
<template v-slot="scope" >
<el-button type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)" />
<el-popconfirm title="您确定要删除这条记录吗?" @confirm="handleDelete(scope.row.id)">
<template #reference>
<el-button type="danger" icon="Delete"/>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<Dialog v-model="dialogVisible" :tableData="tableData" :dialogVisible="dialogVisible" :id="id" :dialogTitle="dialogTitle" @initMenuList="initMenuList"/>
</template>
<style lang="scss" scoped>
...
</style>