赞
踩
- spring-boot3 最低支持jdk17, 所以需要准备jdk17环境
- Idea 版本IntelliJ IDEA 2021.2及以后,版本关系参考[https://blog.csdn.net/m0_62258564/article/details/134527268](https://blog.csdn.net/m0_62258564/article/details/134527268)
- maven 版本参考以上链接
- MySql8
创建数据库 base_manage, 并创建表
CREATE TABLE user(
id INT NOT NULL AUTO_INCREMENT COMMENT '主键',
login_name VARCHAR(255) NOT NULL COMMENT '登录名(账号)',
password VARCHAR(255) NOT NULL COMMENT '密码',
name varchar(50) NOT NULL COMMENT '姓名',
sex char(1) NOT NULL COMMENT '性别',
phone VARCHAR(20) COMMENT '联系电话',
PRIMARY KEY(id)
)COMMENT '用户信息表';
用户表只是便于后续过程搭建操作,可根据需求修改
springboot版本可根据需求选择,这里选则默认的3.2.4
耐心等待项目构建完成, 构建完成pom文件如下,请注意,mysql驱动包<groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId>
较之前版本有所改变
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.buzhisuoyun</groupId> <artifactId>base_manage</artifactId> <version>0.0.1-SNAPSHOT</version> <name>base_manage</name> <description>base_manage</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
添加 jdbc依赖,防止项目启动时找不到Bean报错
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
添加druid依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-3-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
项目使用yml格式配置文件,修改resources下application.properties
为application.yml, 并删除文件内容
配置项目端口号
server:
port: 8099
配置数据源和druid 连接池
spring:
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/base_manage?serverTimezone=UTC
username: root
password: root
启动项目, 查看日志,端口和druid 初始化, 整合成功
官网地址: https://baomidou.com/
1、添加依赖
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
2、配置
# mybatis-plus
mybatis-plus:
configuration:
# sql日志, 开发调试时开启
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
启动项目,出现mybatis-plus日志
1.引入相关依赖
<!-- mybatis-plus 代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.5</version>
</dependency>
<!-- mybatis-plus代码生成器模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
package com.buzhisuoyun.base_manage.utils; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.io.File; import java.sql.Types; import java.util.Collections; public class MybatisPlusGenerator { static final String url = "jdbc:mysql://127.0.0.1:3306/base_manage?serverTimezone=UTC"; // 数据库地址 static final String username = "root"; // 数据库用户名 static final String password = "root"; // 数据库密码 static final String authorName = "buzhisuoyun"; // 作者名 static final String parentPackageNameResource = "com/buzhisuoyun/base_manage"; // mapper.xml路径 static final String parentPackageNameJava = "com.buzhisuoyun.base_manage"; // java 文件父包名 // 要生成代码对应的数据表名 static final String tableName = "user"; public static void main(String[] args) { FastAutoGenerator.create(url, username, password) // 1.全局配置 .globalConfig(builder -> { builder.author(authorName) // 设置作者 .enableSpringdoc() // 开启 swagger 模式 // 获取当前工程路径并定位到项目java目录下 .outputDir(System.getProperty("user.dir") + "/src/main/java"); // 指定输出目录 }) // 2.数据源配置 .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { int typeCode = metaInfo.getJdbcType().TYPE_CODE; if (typeCode == Types.SMALLINT) { // 自定义类型转换 return DbColumnType.INTEGER; } return typeRegistry.getColumnType(metaInfo); })) // 3.包名策略配置 .packageConfig(builder -> { builder.parent(parentPackageNameJava) // 设置父包名 .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .controller("controller") //.moduleName("system") // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/" + parentPackageNameResource + "/mapper")); // 设置mapperXml生成路径 }) // 策略配置 .strategyConfig(builder -> { builder.addInclude(tableName) // 设置需要生成的表名 // 覆盖已生成文件 .entityBuilder().enableFileOverride() .mapperBuilder().enableFileOverride() .serviceBuilder().enableFileOverride().formatServiceFileName("%sService"); //.addTablePrefix("t_", "c_"); // 设置过滤表前缀 }) // 配置模板 .templateConfig(builder -> { //builder.controller(""); // 不生成controller }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute(); } }
新建config包,并在下边创建配置类MybatisPlusConfig.java
package com.buzhisuoyun.base_manage.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com/buzhisuoyun/base_manage/mapper") public class MybatisPlusConfig { /** * 添加分页插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加 //interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType return interceptor; } }
@MapperScan(“com/buzhisuoyun/base_manage/mapper”) 扫描mapper路劲,也可在启动类配置
引入依赖
<!-- API 文档 knife4j -->
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
配置:
# springdoc-openapi 配置 springdoc: swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: alpha api-docs: path: /v3/api-docs group-configs: - group: 'default' paths-to-match: '/**' packages-to-scan: com.buzhisuoyun.base_manage # knife4j 配置 knife4j: # 是否启用增强 enable: true # 开启生产环境屏蔽 production: false # 是否认证登录 basic: # basic是否开启,默认为false enable: true username: knife4j password: knife4j setting: language: zh_cn enable-version: true enable-swagger-models: true
在config下创建配置类
package com.buzhisuoyun.base_manage.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class Knife4jConfig { @Bean public OpenAPI springShopOpenApi() { return new OpenAPI() // 接口文档标题 .info(new Info().title("接口文档") .description("api接口文档") .version("1.0版本") ); } }
配置完后,启动项目,访问路径http://localhost:9999/doc.html,用户名密码就是配置文件中的,结果如图
实体类使用@Schema 注解,在mybatis-plus选择的话会自动生成
Conntroller 层使用:
@Tag(name = "用户接口") @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; /** * 用户列表分页 * @param pageSize 每页显示的条数 * @param currentPage 要查询的页 * @param name 用户姓名 * @return Result<PageResultBean<List<User>>> */ @GetMapping("/pageList") @Operation(summary = "用户列表分页查询") @Parameters({ @Parameter(name = "Authorization", in = ParameterIn.HEADER, required = true, description = "token"), @Parameter(name = "pageSize", required = true, description = "每页显示的条数"), @Parameter(name = "currentPage", required = true, description = "要查询的页"), @Parameter(name = "name", description = "用户姓名", required = false) }) public Result<PageResultBean<User>> pageList(@RequestParam int pageSize, @RequestParam int currentPage, @Nullable @RequestParam String name) { IPage<User> page = userService.pageList(pageSize, currentPage, name); if (page == null) { return Result.error("查询失败"); } //PageResultBean<User> pageResultBean = new PageResultBean<User>(page.getTotal(), page.getRecords()); return Result.success(PageResultBean.getInstance(page.getTotal(), page.getRecords())); } }
封装返回数据封装类,放到common包下:
package com.buzhisuoyun.base_manage.common; import io.swagger.v3.oas.annotations.media.Schema; public class Result<T> { @Schema(description = "业务状态码 0:成功 1: 失败") private int code; // 业务状态码 0:成功 1: 失败 @Schema(description = "提示信息") private String message; // 提示信息 @Schema(description = "返回数据") private T data; // 响应数据 public Result(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } // 操作成功返回响应结果(带响应数据) public static <E> Result<E> success(E data) { return new Result<>(0, "操作成功", data); } public static <E> Result<E> success() { return new Result<>(0, "操作成功", null); } public static <E> Result<E> error(String message) { return new Result<>(1, message, null); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } @Override public String toString() { return "Result{" + "code=" + code + ", message='" + message + '\'' + ", data=" + data + '}'; } }
封装分页查询数据,放到common包下:
package com.buzhisuoyun.base_manage.common; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; public class PageResultBean<T> { @Schema(description = "数据总条数") private Long total; @Schema(description = "当前页数据集合") private List<T> items; public PageResultBean() { } public PageResultBean(Long total, List<T> items) { this.total = total; this.items = items; } public static <E> PageResultBean<E> getInstance(Long total, List<E> items) { return new PageResultBean<>(total, items); } public Long getTotal() { return total; } public void setTotal(Long total) { this.total = total; } public List<T> getItems() { return items; } public void setItems(List<T> items) { this.items = items; } }
创建xception包,在包下放全局异常处理类GlobalExceptionHandler.java,类名放置 位置可以随意,为便于分类整理, 同类包放到一个包下
使用springboot @RestControllerAdvice 注解配置
package com.buzhisuoyun.base_manage.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; // 全局异常处理 @RestControllerAdvice public class GlobalExceptionHandler { private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(Exception.class) public Result<String> handlerException(Exception e) { logger.warn(e.getMessage()); return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败"); } }
引入依赖
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
封装工具类, 放到utils包下:
package com.buzhisuoyun.base_manage.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import java.util.Date; import java.util.Map; public class JwtUtil { private static final String KEY = "buzhisuoyun"; // 密钥 // 接收数据,生成token并返回 public static String getToken(Map<String, Object> claims) { return JWT.create() .withClaim("claims", claims) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 失效时间1小时 .sign(Algorithm.HMAC256(KEY)); } // 接收token,验证并返回数据 public static Map<String, Object> parseToken(String token) { return JWT.require(Algorithm.HMAC256(KEY)) .build() .verify(token) .getClaim("claims") .asMap(); } }
因项目使用前后端分离,使用ThreadLocal 线程变量存储用户登录信息,替代session
package com.buzhisuoyun.base_manage.utils; public class ThreadLocalUtil { // 提供ThreadLocal 对象 private static final ThreadLocal<Object> THREAD_LOCAL = new ThreadLocal<>(); // 获取存储值 public static <T> T get() { return (T) THREAD_LOCAL.get(); } // 存储值 public static void set(Object value) { THREAD_LOCAL.set(value); } // 清除THREAD_LOCAL 防止内存泄漏 public static void remove() { THREAD_LOCAL.remove(); } }
package com.buzhisuoyun.base_manage.utils; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class Md5Util { /** * 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合 */ protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; protected static MessageDigest messagedigest = null; static { try { messagedigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException nsaex) { System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。"); nsaex.printStackTrace(); } } /** * 生成字符串的md5校验值 * * @param s * @return */ public static String getMD5String(String s) { return getMD5String(s.getBytes()); } /** * 判断字符串的md5校验码是否与一个已知的md5码相匹配 * * @param password 要校验的字符串 * @param md5PwdStr 已知的md5校验码 * @return */ public static boolean checkPassword(String password, String md5PwdStr) { String s = getMD5String(password); return s.equals(md5PwdStr); } public static String getMD5String(byte[] bytes) { messagedigest.update(bytes); return bufferToHex(messagedigest.digest()); } private static String bufferToHex(byte bytes[]) { return bufferToHex(bytes, 0, bytes.length); } private static String bufferToHex(byte bytes[], int m, int n) { StringBuffer stringbuffer = new StringBuffer(2 * n); int k = m + n; for (int l = m; l < k; l++) { appendHexPair(bytes[l], stringbuffer); } return stringbuffer.toString(); } private static void appendHexPair(byte bt, StringBuffer stringbuffer) { char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>> // 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同 char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换 stringbuffer.append(c0); stringbuffer.append(c1); } // 测试 public static void main(String[] args) { System.out.println(Md5Util.getMD5String("admin")); } }
1、创建包interceptors,在包下配置登录拦截器
package com.buzhisuoyun.base_manage.interceptors; import com.buzhisuoyun.base_manage.utils.JwtUtil; import com.buzhisuoyun.base_manage.utils.ThreadLocalUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Map; @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取token String token = request.getHeader("Authorization"); // 验证token try { Map<String, Object> claims = JwtUtil.parseToken(token); // 存储业务信息到线程变量 ThreadLocalUtil.set(claims); // 放行拦截 return true; } catch (Exception e) { // 登录信息异常或未登录,http响应状态码为401 response.setStatus(401); // 拦截请求 return false; } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 清除ThreadLocal业务数据 ThreadLocalUtil.remove(); } }
2、在config包下创建WebConfig配置类,注册登录拦截器
package com.buzhisuoyun.base_manage.config; import com.buzhisuoyun.base_manage.interceptors.LoginInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 配置放行资源 // 无需拦截的接口集合 List<String> ignorePath = new ArrayList<>(); // knife4j(swagger) ignorePath.add("/swagger-resources/**"); ignorePath.add("/doc.html"); ignorePath.add("/v3/**"); ignorePath.add("/webjars/**"); ignorePath.add("/static/**"); ignorePath.add("/templates/**"); ignorePath.add("/error"); // 登录页面 ignorePath.add("/user/login"); registry.addInterceptor(loginInterceptor).excludePathPatterns(ignorePath); } }
@PostMapping("/login") @Operation(summary = "用户登录") @Parameters({ @Parameter(name = "loginName", description = "登录名", required = true, schema = @Schema(type = "sting")), @Parameter(name = "password", description = "密码", required = true, schema = @Schema(type = "sting")) }) public Result<String> login(@RequestBody User user) { if (user == null || "".equals(user.getLoginName()) || user.getLoginName() == null || "".equals(user.getPassword()) || user.getPassword() == null) { return Result.error("用户名密码不能为空"); } // 检验用户名是否存在 User eruser = userService.findByLoginName(user.getLoginName()); if (eruser == null) { return Result.error("用户名不存在"); } // 检验用户密码是否正确 if (Md5Util.getMD5String(user.getPassword()).equals(eruser.getPassword())) { Map<String, Object> claims = new HashMap<>(); claims.put("id", eruser.getId()); claims.put("name", eruser.getName()); claims.put("loginName", eruser.getLoginName()); String token = JwtUtil.getToken(claims); return Result.success(token); } return Result.error("密码错误"); } /** * 获取当前登录用户信息 * @return User */ @GetMapping("/currentUser") @Operation(summary = "获取当前登录用户信息") @Parameter(name = "Authorization", in = ParameterIn.HEADER, required = true, description = "token") public Result<User> getCurrentUser() { Map<String, Object> userSession = ThreadLocalUtil.get(); int id = (int) userSession.get("id"); User user = userService.getUserById(id); if (user != null) { return Result.success(user); } return Result.error("用户状态异常"); }
因为是做简单后端项目搭建,所以token存在一定问题,如在修改用户密码后必须强制重新登录或采用redis缓存token,同时验证浏览器token和用户token,或采用第三方认证中心解决等
项目工程结构如图
- node安装
- vscode安装
在将要存放vue3项目的路径打开cmd, 使用以下命令创建项目
npm init vue@latest
此时项目创建完成,vscode打开项目目录,在资源目录空白右键,打开终端
执行命令 npm install
安装依赖,等待安装完成后执行 npm run dev
运行项目
访问路径可访问项目
在终端ctrl c 可停止运行项目
项目描述如图,图引自黑马开源教程PPT
说明:项目中使用组合式API
删掉components下所有文件,删除App.vue文件不需要的东西,最后如下所示
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>
npm install element-plus --save
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
npm install axios
在src目录下新建 utils, 并在utils 下创建request.js 进行axios配置
// 请求配置 import axios from "axios"; // 定义公共前缀,创建请求实例 // const baseUrl = "http://localhost:8080"; const baseURL = '/api/'; const instance = axios.create({baseURL}) import { ElMessage } from "element-plus" import { useTokenStore } from "@/stores/token.js" // 配置请求拦截器 instance.interceptors.request.use( (config) => { // 请求前回调 // 添加token const tokenStore = useTokenStore() // 判断有无token if (tokenStore.token) { config.headers.Authorization = tokenStore.token } return config }, (err) => { // 请求错误的回调 Promise.reject(err) } ) import router from "@/router"; // 添加响应拦截器 instance.interceptors.response.use( result => { // 判断业务状态码 if (result.data.code === 0) { return result.data; } // 操作失败 ElMessage.error(result.data.message ? result.data.message : '服务异常') // 异步操作的状态转换为失败 return Promise.reject(result.data) }, err => { // 判断响应状态码, 401为未登录,提示登录并跳转到登录页面 if (err.response.status === 401) { ElMessage.error('请先登录') router.push('/login') } else { ElMessage.error('服务异常') } // 异步操作的状态转换为失败 return Promise.reject(err) } ) export default instance
在vite.config.js 中defineConfig配置代理,实现跨域
server: {
proxy: {
'/api': { // 获取路径中包含了/api的请求
target: 'http://localhost:9999', // 服务端地址
changeOrigin: true, // 修改源
rewrite:(path) => path.replace(/^\/api/, '') // api 替换为 ''
}
}
}
npm install vue-router@4
// 导入vue-router import {createRouter, createWebHistory} from 'vue-router' // 导入组件 import LoginVue from '@/views/Login.vue' import LayoutVue from '@/views/Layout.vue' import UserList from '@/views/user/UserList.vue' import EditPassword from '@/views/user/EditPassword.vue' import DisplayUser from '@/views/user/DisplayUser.vue' // 定义路由关系 const routes = [ {path: '/login', component: LoginVue}, { path: '/', component: LayoutVue, redirect: '', children: [ {path: '/user/userlist', name: "/user/userlist", component: UserList, meta: { title: "用户列表" },}, {path: '/user/editpassword', name: "/user/editpassword", component: EditPassword, meta: { title: "修改密码" } }, {path: '/user/displayuser', name: "/user/displayuser", component: DisplayUser, meta: { title: "个人信息" }} ] } ] // 创建路由器 const router = createRouter({ history: createWebHistory(), routes: routes }) export default router
在main.js 中
import router from '@/router'
app.use(router)
在app.vue 中
<template>
<router-view></router-view>
</template>
npm install pinia
main.js
import {createPersistedState} from'pinia-persistedstate-plugin’ const persist = createPersistedState()
pinia.use(persist)
定义状态Store时指定持久化配置参数
在src/stores/下定义token.js和userInfo.js 用来存储token和用户相关信息
token.js // 定义 store import { defineStore } from "pinia" import {ref} from 'vue' /* 第一个参数:名字,唯一性 第二个参数:函数,函数的内部可以定义状态的所有内容 返回值: 函数 */ export const useTokenStore = defineStore('token', () => { // 响应式变量 const token = ref('') // 修改token值函数 const setToken = (newToken) => { token.value = newToken } // 移除token值函数 const removeToke = () => { token.value = '' } return { token, setToken, removeToke } }, { persist: true // 持久化存储 } )
userInfo.js import { defineStore } from "pinia" import {ref} from 'vue' const useUserInfoStore = defineStore('userInfo', () => { const info = ref({}) const setInfo = (newInfo) => { info.value = newInfo } const removeInfo = () => { info.value = {} } return {info, setInfo, removeInfo} }, { persist: true } ) export default useUserInfoStore;
import {userLoginService} from '@/api/user.js' import {useTokenStore} from '@/stores/token.js' import {useRouter} from 'vue-router' const router = useRouter() const tokenStore = useTokenStore(); const login = async ()=>{ // 校验表单 if (!ruleFormRef.value) return console.log("校验") await ruleFormRef.value.validate(async (valid) => { if (valid) { console.log("校验成功") // 调用接口,完成登录 let result = await userLoginService(registerData.value); /* if(result.code===0){ alert(result.msg? result.msg : '登录成功') }else{ alert('登录失败') } */ //alert(result.msg? result.msg : '登录成功') ElMessage.success(result.msg ? result.msg : '登录成功') //token存储到pinia中 tokenStore.setToken(result.data) //跳转到首页 路由完成跳转 router.push('/') } else { console.log("校验失败") } }) } // 配置请求拦截器 instance.interceptors.request.use( (config) => { // 请求前回调 // 添加token const tokenStore = useTokenStore() // 判断有无token if (tokenStore.token) { config.headers.Authorization = tokenStore.token } return config }, (err) => { // 请求错误的回调 Promise.reject(err) } )
import request from "@/utils/request.js" // 登录接口调用函数 export const userLoginService = (loginData) => { return request.post('/user/login', loginData) } // 获取当前登录用户信息 export const currentUserService = () => { return request.get('/user/currentUser') } // 获取所有用户信息 export const allUserService = () => { return request.get('/user/userList') } // 分页查询 export const pageListService = (pageParam) => { return request.get('/user/pageList', {params: pageParam}) } // 新增用户 export const addUserService = (addData) => { return request.post('/user/add', addData) } // 根据id获取用户信息 export const getUserById = (id) => { return request.get('/user/getuserById', {params: id}) } // 修改用户信息 export const updateUserService = (data) => { return request.put('/user/update', data) } // 删除用户 export const deleteByIdService = (id) => { console.log("deleteRequestid:", id) return request.delete('/user/delete/' + id) }
npm install sass -D
在src下创建views目录,用于存放vue页面组件
登录 Login.vue
<script setup> import { User, Lock } from '@element-plus/icons-vue' import { ref, reactive } from 'vue' import { ElMessage } from 'element-plus' //定义数据模型 const registerData = ref({ loginName: 'admin', password:'admin', rePassword: '' }) // 定义表单组件的引用 const ruleFormRef = ref(null) //定义表单校验规则 const rules = ref({ loginName: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 5, max: 16, message: '长度为5~16位非空字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 5, max: 16, 2: '长度为5~16位非空字符', trigger: 'blur' } ] }) //绑定数据,复用注册表单的数据模型 //表单数据校验 //登录函数 import {userLoginService} from '@/api/user.js' import {useTokenStore} from '@/stores/token.js' import {useRouter} from 'vue-router' const router = useRouter() const tokenStore = useTokenStore(); const login = async ()=>{ // 校验表单 if (!ruleFormRef.value) return console.log("校验") await ruleFormRef.value.validate(async (valid) => { if (valid) { console.log("校验成功") // 调用接口,完成登录 let result = await userLoginService(registerData.value); /* if(result.code===0){ alert(result.msg? result.msg : '登录成功') }else{ alert('登录失败') } */ //alert(result.msg? result.msg : '登录成功') ElMessage.success(result.msg ? result.msg : '登录成功') //token存储到pinia中 tokenStore.setToken(result.data) //跳转到首页 路由完成跳转 router.push('/') } else { console.log("校验失败") } }) } //定义函数,清空数据模型的数据 const clearRegisterData = ()=>{ registerData.value={ loginName: '', password:'', rePassword:'' } } </script> <template> <el-row class="login-page"> <el-col :span="12" class="bg"></el-col> <el-col :span="6" :offset="3" class="form"> <!-- 登录表单 --> <el-form ref="ruleFormRef" :model=registerData size="large" autocomplete="off" :rules="rules"> <el-form-item> <h1>登录</h1> </el-form-item> <el-form-item prop="loginName"> <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.loginName"></el-input> </el-form-item> <el-form-item prop="password"> <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> </el-form-item> <el-form-item class="flex"> <div class="flex"> <el-checkbox>记住我</el-checkbox> <!-- <el-link type="primary" :underline="false">忘记密码?</el-link> --> </div> </el-form-item> <!-- 登录按钮 --> <el-form-item> <el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button> </el-form-item> </el-form> </el-col> </el-row> </template> <style lang="scss" scoped> /* 样式 */ .login-page { height: 100vh; background-color: #fff; .bg { background: url('@/assets/login_bg.jpg') no-repeat center / cover; border-radius: 0 20px 20px 0; } .form { display: flex; flex-direction: column; justify-content: center; user-select: none; .title { margin: 0 auto; } .button { width: 100%; } .flex { width: 100%; display: flex; justify-content: space-between; } } } </style>
<script setup> import LeftLayout from './LeftLayout.vue' import Header from './Header.vue' import MainView from './MainView.vue' import {ref} from 'vue' const isCollapse = ref(false) const parentClick = (isCollapseValue) => { isCollapse.value = isCollapseValue; console.log(isCollapse.value) } </script> <template> <div class="common-layout"> <el-container> <LeftLayout :isCollapse='isCollapse' /> <el-container> <el-header style="padding: 0"><Header @parentClick='parentClick'/></el-header> <el-main style="padding: 16px 8px 6px 8px"><MainView/></el-main> <el-footer>后台 ©2024 Created by buzhisuoyun</el-footer> </el-container> </el-container> </div> </template> <style scoped> .el-footer { display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666; height: 38px; padding: 0; background-color: #FFFFFF; } </style>
<template> <el-row class="tac"> <el-col > <el-menu default-active="2" class="el-menu-vertical-demo" :collapse="isCollapse" :router="true" > <!-- 标题 --> <div class="containerdiv"> <img src="../assets/favicon.ico" alt="Your Image" class="image"> <span class="text">后台管理</span> </div> <!-- 菜单 --> <el-sub-menu index="1"> <template #title> <el-icon><Share /></el-icon> <span>API管理</span> </template> <el-menu-item index="/api/apilist">API列表</el-menu-item> <el-menu-item index="1-2">item two</el-menu-item> <el-menu-item index="1-3">item three</el-menu-item> <el-sub-menu index="1-4"> <template #title>item four</template> <el-menu-item index="1-4-1">item one</el-menu-item> </el-sub-menu> </el-sub-menu> <el-menu-item index="2"> <el-icon><icon-menu /></el-icon> <span>Navigator Two</span> </el-menu-item> <el-menu-item index="3" disabled> <el-icon><document /></el-icon> <span>Navigator Three</span> </el-menu-item> <el-menu-item index="4"> <el-icon><setting /></el-icon> <span>Navigator Four</span> </el-menu-item> <el-sub-menu index="5"> <template #title> <el-icon><UserFilled /></el-icon> <span>用户管理</span> </template> <el-menu-item index="/user/userlist">用户列表</el-menu-item> <el-menu-item index="/user/displayuser">个人信息</el-menu-item> <el-menu-item index="/user/editpassword">修改密码</el-menu-item> </el-sub-menu> </el-menu> </el-col> </el-row> </template> <script lang="ts" setup> import { Document, Menu as IconMenu, Location, Share, UserFilled, Setting, } from '@element-plus/icons-vue' import {ref, defineProps} from 'vue' type Props = { isCollapse: boolean } defineProps<Props>() </script> <style scoped> .el-menu-vertical-demo { height: 100vh; } .el-menu-item { min-width: 0; } .containerdiv { /* 你可以设置容器的样式,例如宽度、高度、背景色等 */ /* width: 300px; /* 示例宽度 */ height: 48px; padding: 10px; /* 内边距 */ border-bottom: 2px solid; /* 设置下边框宽度和样式 */ border-bottom-color: #F5F5F5; /* 设置下边框颜色为红色 */ } .image { display: inline-block; vertical-align: middle; /* 图片与文字垂直居中对齐 */ margin-right: 6px; /* 图片右边距,可选 */ width: 20px; } .text { display: inline-block; vertical-align: middle; /* 文字与图片垂直居中对齐 */ font-weight: bold; /* 加粗文字 */ font-size: 14px; } </style>
<template> <div class="container"> <!-- div left --> <div class="left"> <!-- 折叠按钮--> <div @click="toggleCollapse()"> <el-icon size="24" v-show="!isMenuOpen"><Fold /></el-icon> <el-icon size="24" v-show="isMenuOpen"><Expand /></el-icon> </div> <!-- 面包屑 --> <div> <el-breadcrumb separator="/"> <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item> <template v-for="(item, index) in breadList"> <el-breadcrumb-item v-if="item.name" :key="index" :to="item.path" >{{ item.meta.title }}</el-breadcrumb-item> </template> </el-breadcrumb> </div> </div> <!-- div right --> <div class="right"> <div> <span >账号:{{userData.loginName}}</span> </div> <div> <el-avatar> {{userData.name}} </el-avatar> </div> <div> <el-dropdown> <el-icon size="24"><MoreFilled /></el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item> <el-icon><UserFilled /></el-icon>个人信息 </el-dropdown-item> <el-dropdown-item> <el-icon><EditPen /></el-icon>修改密码 </el-dropdown-item> <el-dropdown-item> <el-icon><ArrowLeft /></el-icon>退出登录 </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </div> </div> </template> <script setup> import { Fold, Expand, MoreFilled, EditPen, ArrowLeft, UserFilled } from '@element-plus/icons-vue' import {ref, defineEmits, watch} from 'vue' // 面包屑 import { useRouter,useRoute } from 'vue-router' let router = useRouter() let route = useRoute() let breadList = ref() let getMatched=()=>{ console.log("route.matched:",route.matched); console breadList.value = route.matched.filter(item => item.meta && item.meta.title); } getMatched() watch(() => route.path, (newValue, oldValue) => { //监听路由路径是否发生变化,之后更改面包屑 console.log("======") breadList.value = route.matched.filter(item => item.meta && item.meta.title ); console.log("breadList.value", breadList.value) }) import useUserInfoStore from '@/stores/userInfo.js' const userInfoStore = useUserInfoStore(); // 用户数据模型 let userData = ref({ id: '', name: '', loginName: '' }) import {currentUserService} from '@/api/user.js' // 获取登录用户信息 const getUser = async () => { let result = await currentUserService() // console.log(result) userData.value = result.data; userInfoStore.setInfo(result.data) // console.log("userData:",userData) } getUser() // 折叠按钮处理 const emits = defineEmits(["parentClick"]) const isMenuOpen = ref(false) const toggleCollapse = () => { isMenuOpen.value = !isMenuOpen.value console.log(isMenuOpen.value) emits("parentClick", isMenuOpen.value) } </script> <style lang="scss" scope> .container { overflow: auto; /* 清除浮动影响 */ height: 48px; padding: 10px; /* 内边距 */ border-bottom: 2px solid; /* 设置下边框宽度和样式 */ border-bottom-color: #F5F5F5; /* 设置下边框颜色为红色 */ background-color: #FFFFFF; } .left { height: 48px; float: left; display: flex; align-items: center; /* 垂直居中子项 */ justify-content: center; /* 水平居中子项(如果需要)*/ } .left > div { padding-right: 10px; /* 设置直接子元素的 padding */ } .right { float: right; display: flex; align-items: center; /* 垂直居中子项 */ justify-content: center; /* 水平居中子项(如果需要)*/ } .right > div { padding-right: 10px; /* 设置直接子元素的 padding */ } </style>
<template> <div class="app-main"> <!-- <transition name="fade-transfrom" mode="out-in"> <router-view></router-view> </transition> --> <router-view v-slot="{ Component }"> <transition> <component :is="Component" /> </transition> </router-view> </div> </template> <style lang="scss" scope> .app-main{ width:100%; height:100%; background-color: #FFFFFF; } </style>
项目结构:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。