SpringBoot3+Vue3
前置条件
后端:JavaSE ,JavaWEb ,SSM框架
前端: html,css,javascript
使用工具以及环境版本
- JDK17+
- IDEA2021+
- maven3.5+
- vscode
打开cmd
查看maven版本:mvn -v
查看java版本:java -version
SpringBoot
是spring提供的一个子项目,用于快速构建Spring应用程序
快速Demo
查看demo构建过程
新建项目
- 创建一个
Spring initializr
的项目,参考如下配置
名称 | 位置 | 语言 | 类型 | 打包 | JDK |
---|---|---|---|---|---|
quickstart | …\workspace\springboot\ | java | Maven | jar | 17+ |
导入spring-boot-satrt-web
起步依赖
- 选择
SpringBoot3
以上的版本 依赖选择Web
->Spring Web
编写Controller ->”com.hnit.controller.Controller.java
“1
2
3
4
5
6
7
public class Controller {
public String hello() {
return "Hello World";
}
}
提供启动类
- 运行成功,访问http://localhost:8080/hello
- 可以看到浏览器上的helloWorld
创建Maven工程
- 引用如下Archetype模板
org.apache.maven.archetypes:maven-archetype-quickstart
在pom.xml
文件中添加如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!--父依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
</parent>
<!-- 添加web起步依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
项目下会生成一个App启动类
- 改一下名字格式: 项目名+Application
- 启动类固定格式如下
1
2
3
4
5
6
7
public class SpringBootCreateManualApplication{
public static void main( String[] args ){
//两个参数:启动类字节码文件,main方法数组参数
SpringApplication.run(SpringBootCreateManualApplication.class,args);
}
}
写一个和方式一样的controller,可以启动测试成功
注意!! 启动类必须放在最外面的包中,
比如包结构为com.hnit.controller/pojo/domain
那Application应该放com.hnit下
用这种方法创建会缺少的目录和文件
- 资源目录resource:手动创建如下目录
main/resources
- 配置文件application.properties:在
resources
目录下创建文件application.properties
或application.yml
或application.yaml
- 如果有多个配置文件而暂时想排除某一个,一个简单的办法是在文件扩展名后加
.bak
,如application.properties.bak
Application.properties配置文件
查看引用配置文件里的数据过程
application.yml中添加相关配置1
2
3
4
5email:
user: 2098998@qq.com
code: testaboutcode
host: smtp.qq.com
auth: true
引用只需在对应属性上加上注解1
2
3
4
private String user;
private String code;
在封装类上加上注解@ConfigurationProperties(prefix="前缀")
同时保证类里面的属性和配置文件的属性名要一致1
2
3
4
5
6
7
8# application.yml
# 配置信息书写,值的前边必须有空格,作为分隔符
# 使用空格作为缩进表示层级关系,相同的层级左对齐
student:
name: zhangshan
password: 123456
gender: 1
age: 21
1 |
|
测试1
2
3
4
5
6
7
8
9
10
11
12Controller
public class Controller {
private Student std;
public String hello() {
return "Hello"+std;
}
}
整合mybatis
查看整合mybatis过程
引入mybatis和mysql依赖,以及web依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置数据库 application.yml1
2
3
4
5
6spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
username: root
password: 123456
数据层 com/hnit/pojo/Student1
2
3
4
5
6
7public class Student {
private Integer id;
private String name;
private Integer gender;
private Integer age;
private String phone;
}
Mapper层 com/hnit/mapper/StudentMapper1
2
3
4
5
6
public interface StudentMapper {
public Student getByid(int id);
}
Service层 com/hnit/service/1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// com/hnit/service/StudentService
public interface StudentService {
public Student getByid(int id);
}
// com/hnit/service/imp/StudentServiceImpl
public class StudentServiceImpl implements StudentService {
private StudentMapper studentMapper;
public Student getByid(int id) {
return studentMapper.getByid(id);
}
}
Controller层 com/hnit/controller/StudentController1
2
3
4
5
6
7
8
9
10
11
public class StudentController {
private StudentService studentService;
public Student getByid({ Integer id)
return studentService.getByid(id);
}
}
运行结果:http://localhost:8080/getByid/21
2
3
4
5
6
7{
"id": 2,
"name": "李四",
"gender": 1,
"age": 22,
"phone": "90876543212"
}
Bean
查看整合mybatis过程
Bean的扫描
springboot默认扫描启动类所在的包及其子包Bean的注册
先在pom文件导入,然后导入
方式一:
com/hnit/config/CommonConfig.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CommonConfig{
//注入第三方Country对象
public Country country(){
return new Country();
}
//对象名字默认为方法名,也可以在注解中指定名字
//@Bean("aaa")
//如果方法的内部需要使用到ioc容器中已经存在的bean对象,那么只需要到方法上声明即可,spring会自动注入
public Province province(Country country){
System.out.println("province"+country)
return new Province();
}
}启动类
1
2
3
4
5
6
7
8
9
10//@Import(CommonConfig.class) //如果不在启动类及其子包下就import导入
public class SpringBootCreateManualApplication{
public static void main( String[] args ){
ApplicationContext context=SpringApplication.run(SpringBootCreateManualApplication.class,args);
Country country = context.getBean(Country.class);
System.out.println(context.getBean("province"))
}
}
注册条件@ConditionalOnProperty
配置文件中纯正对应的属性,才声明该bean@ConditionalMissingBean
当不存在当前类型的bean时,才声明该bean@ConditionalOnClass
当环境中存在指定的这个类时,才声明该bean1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CommonConfig{
// @ConditionalOnProperty(prefix = "country",name = {"name","system"}) //配置文件中纯正对应的属性,才声明该bean
public Country country({ String name, String system)
return new Country();
}
// @ConditionalMissingBean(Country.class) //当不存在当前类型的bean时,才声明该bean
// @ConditionalOnClass("xxx.xxx.xxx.xx.class") //当环境中存在指定的这个类时,才声明该bean
public Province province(){
return new Province();
}
}
SpringBoot自动配置的原理
查看原理
- 在主启动类上添加了SpringBootApplication注解,这个注解组合了EnableAutoConfiguration注解
- EnableAutoConfiguratio注解又组合了Import注解,导入了AutoConfigurationSelector类
- 实现selectimports方法,这个方法经过层层调用,最终会读取META-INF目录下的后缀名为imports的文件,当然,boot2.7以前的版本,读取的是spring.factories文件
- 读取到全类名了之后,会解析注册条件,也就是@Conditional及其衍生注解,把满足注册条件的Bean对象自动注册到IOC容器中
自定义starter
查看流程
创建两个maven工程
dmybatis-spring-boot-autoconfigure
dmybatis-spring-boot-starter
选择骨架:org.apache.maven.archetypes:maven-archetype-quickstart
组件:com.xnj
在dmybatis-spring-boot-autoconfigure中提供自动配置的功能
引入依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
创建自动配置类com/xnj/config/MybatisAutoConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//标识当前类是一个自动配置类
public class MybatisAutoConfig {
//SqlSessionFactoryBean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
//MapperScannerConfigurer
public MapperScannerConfigurer mapperScannerConfigurer(BeanFactory beanFactory){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
//设置扫描的包路径,启动类所在的包及其子包
List<String> packages = AutoConfigurationPackages.get(beanFactory);
String p = packages.get(0);
mapperScannerConfigurer.setBasePackage(p);
//扫描的注解
mapperScannerConfigurer.setAnnotationClass(Mapper.class);
return mapperScannerConfigurer;
}
}
创建以下目录和文件 :main/resources
资源目录main/resources/META-INF
META-INF目录main/resources/META-INF/spring
spring目录main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件
复制自动装配类资源的路径:com.xnj.config.MybatisAutoConfig
到org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中
在dmybatis-spring-boot-starter里引入dmybatis-spring-boot-autoconfigure
依赖和它的依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<dependency>
<groupId>com.xnj</groupId>
<artifactId>dmybatis-spring-boot-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
dmybatis-spring-boot-autoconfigure
工程中的APP和test可以删除dmybatis-spring-boot-starter
工程用来管理依赖,仅保留pom.xml文件
回到前面springboot整合mybatis的项目中,注释以下依赖并添加自定义starter,运行即可1
2
3
4
5
6
7
8
9
10
11
12<!-- <dependency>-->
<!-- <groupId>org.mybatis.spring.boot</groupId>-->
<!-- <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!-- <version>3.0.3</version>-->
<!-- </dependency>-->
<!--自定义starter-->
<dependency>
<groupId>com.xnj</groupId>
<artifactId>dmybatis-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
自定义mybatis的starter总结
创建dmybatis-spring-boot-autoconfig模块,提供自动装配功能,并自定义配置文件META-INF/spring/xxx.imports
创建dmybatis-spring-boot-starter模块,在starter中引入自动装配模块
项目 big-event 后端学习
项目包结构如下
com/xnj
为外层包,App启动类在这一层,它包含以子包
controller
,service及service/impl
,mapper
,pojo实体类
utils工具类包
,interceptors自定义拦截器包
,exception异常处理类
,config配置类
环境准备
查看环境准备工作
1 | create database big_event; |
- 导入依赖
- 创建包结构,和资源目录
pojo,mapper,service,service/impl,controller,utils
resource,resource/application.yml1
2
3
4
5
6spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/big_event?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456- 修改启动类
1
2
3
4
5
6
7
8
9
public class BigEventApplication
{
public static void main( String[] args )
{
SpringApplication.run(BigEventApplication.class, args);
}
}
- 修改启动类
引入lombook依赖来为实体类自动生成get,set方法1
2
3
4
5<!-- lombook依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
User 用户类1
2
3
4
5
6
7
8
9
10
11
public class User {
private Integer id;//主键id
private String username;//用户名
private String password;//密码
private String nickname;//昵称
private String email;//邮箱
private String userPic;//头像
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//修改时间
}
Category 文章分类类1
2
3
4
5
6
7
8
9
10
public class Category {
private Integer id;//主键id
private String categoryName;//分类名称
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//修改时间
}
Article 文章类1
2
3
4
5
6
7
8
9
10
11
12
public class Article {
private Integer id;//主键id
private String title;//标题
private String content;//内容
private String coverImg;//封面图
private String state;//发布状态 已发布|草稿
private Integer categoryId;//分类id
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//修改时间
}
统一响应结果类Result1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//统一响应结果
//如果这里加@Data,在controller时就不能识别转换为json对象返回
public class Result<T> {
private Integer code;//状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带数据)
public static <E> Result<E> success(E data){
return new Result<>(0,"操作成功",data);
}
//快速返回操作成功响应结果(不带数据)
public static Result success(){
return new Result(0,"操作成功",null);
}
//操作失败
public static Result error(String message){
return new Result(1,message,null);
}
}
MD5加密工具类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67public 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);
}
}
开发功能接口
用户注册功能
查看注册功能实现
1 |
|
1 | //UserService |
1 |
|
对注册功能测试
这里使用的是APIPOST工具1
2POST: http://localhost:8080/user/register
Body.urlencoded
参数名 | 参数值 |
---|---|
username | zhangshan |
password | 123456 |
第一次注册1
2
3
4
5{
"code": 0,
"message": "操作成功",
"data": null
}
第二次注册1
2
3
4
5{
"code": 1,
"message": "用户名已存在",
"data": null
}
然而,上述虽然实现了注册功能,但并没有对username和password进行校验
使用Spring Validation,对注册接口的参数进行合法性校验
1.引入Spring Validation起步依赖1
2
3
4
5<!-- validation验证依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.在参数前面添加@Pattern注解public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password)
3.在Controller类上添加@Validated注解1
2
public class UserController
但是此时如果传来的数据不符合格式则会报如下异常
register.username: 需要匹配正则表达式”^\S{5,16}$”
前端则会收到如下数据1
2
3
4
5
6{
"timestamp": "2024-08-16T06:19:59.073+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/user/register"
}
但这并不合我们的返回格式,所以需要定义全局异常处理器处理效验失败的异常
全局异常处理器
创建com/xnj/exception/GlobalExceptionhandler
1
2
3
4
5
6
7
8
9
10
public class GlobalExceptionhandler {
public Result handleException(Exception e) {
e.printStackTrace();//输出异常信息到控制台
//判断异常信息是否为空(有些异常没有返回信息),返回结果
return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败");
}
}
再次测试接口,才能得到我们想要的返回格式1
2
3
4
5{
"code": 1,
"message": "register.username: 需要匹配正则表达式\"^\\S{5,16}$\"",
"data": null
}
JWT令牌
全称 JSON Web Token
定义了一种简洁的,自包含的格式,用于通信双方以json数据格式安全的传输信息
组成第一部分:Header(头),记录令牌类型,签名算法等,如{“alg”:”HS256”,”type”:”JWT”}
第二部分:Payload(有效载荷),携带一些自定义信息,默认信息等。如{“id”:”1”,”username”:”Tom”}
第三部分:Signature(签名),防止Token被篡改,确保安全性。将header,payload,加入指定密钥,通过指定签名算法计算而来
查看jwt示例
引入测试依赖和jwt依赖1
2
3
4
5
6
7
8
9
10
11
12 <!-- java-jwt坐标-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!-- 单元测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
创建test/java/com/xnj/JwtTest1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class JwtTest {
public void testGen(){
Map<String, Object> claims =new HashMap<>();
claims.put("id", 1);
claims.put("username", "张三");
//生成jwt令牌
String token = JWT.create()
.withClaim("user",claims)//添加载荷
.withExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*24))//设置过期时间24小时
.sign(Algorithm.HMAC256("xnj"));//指定算法,配置密钥
System.out.println(token);
}
public void parseToken(){
//解析上面方法生成的令牌
String token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MjM4ODU4OTZ9" +
".jDS7CM_vlfwMNkn2Q15HSm8P7CVYG49HTEaSjmjGPqY";
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("xnj")).build();
DecodedJWT decodedJWT= verifier.verify(token);
Map<String, Claim> claims= decodedJWT.getClaims();
System.out.println(claims.get("user"));//能解析出{"id":1,"username":"张三"}
}
}
用户登录功能
查看用户登录功能实现
在utils包中引入JwtUtil工具类
1.引入jwt依赖1
2
3
4
5
6<!-- java-jwt坐标-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2.引入JwtUtil工具类 com/xnj/utils/JwtUtil1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class JwtUtil {
private static final String KEY = "xnj";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))//有效期为12小时
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
登录查询用户的findByUsername在注册功能时已经实现,直接调用即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Result login({ String username, String password)
User loginUser = userService.findByUsername(username);
//判断用户是否存在
if(loginUser==null){
return Result.error("用户不存在");
}
//判断密码是否正确,loginUser里的password是密文
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//密码正确,生成token
Map<String,Object> claims = new HashMap<>();
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
String token = JwtUtil.genToken(claims);
//返回token
return Result.success(token);
}else{
return Result.error("密码错误");
}
}
之后的每个功能接口都要经过token校验后才能访问
所以这里应该使用拦截器
拦截器
创建com/xnj/interceptors/LoginInterceptor1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//将我们创建的拦截器对象注入IOC容器
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证Token
try {
Map<String,Object> claims = JwtUtil.parseToken(token);
//放行
return true;
} catch (Exception e) {
//设置Http响应状态为401
response.setStatus(401);
//不放行
return false;
}
}
}
还需要注册我们的拦截器
创建com/xnj/config/WebConfig实现WebMvcConfigurer接口
重写addInterceptors方法来注册我们自定义的拦截器1
2
3
4
5
6
7
8
9
10
11
public class WebConfig implements WebMvcConfigurer {
private LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry){
//注册拦截器,并排除登录和注册
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","user/register");
}
}
获取用户详细信息
获取用户详细信息功能实现
1 | //获取用户信息 |
password需要隐藏 而createTime,updateTime需要开启驼峰命名1
2
3//注意别导错包`import com.fasterxml.jackson.annotation.JsonIgnore`
//让springmvc将当前对象转换成JSON字符串时,忽略该字段
private String password;//密码
1 | mybatis: |
但是依靠@RequestHeader("Authorization") String token
来实现的话会显得臃肿,所以可以使用ThreadLocal
ThreadLocal
ThreadLocal用来提供线程局部变量 下面创建一个测试来验证和演示,test引入的依赖为前面提到过的 查看ThreadLocal演示
spring-boot-starter-test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class ThreadLocalTest {
public void threadLocaltest(){
ThreadLocal tl = new ThreadLocal();
//开启两个线程
new Thread(()->{
tl.set("张三");
System.out.println(Thread.currentThread().getName()+" "+tl.get());
System.out.println(Thread.currentThread().getName()+" "+tl.get());
System.out.println(Thread.currentThread().getName()+" "+tl.get());
},"线程1").start();
new Thread(()->{
tl.set("李四");
System.out.println(Thread.currentThread().getName()+" "+tl.get());
System.out.println(Thread.currentThread().getName()+" "+tl.get());
System.out.println(Thread.currentThread().getName()+" "+tl.get());
},"线程2").start();
//控制台打印结果
/*线程1 张三
线程2 李四
线程1 张三
线程2 李四
线程1 张三
线程2 李四*/
}
}
可以看到同一个ThreadLocal对象为不同的线程提供了不同的存储。
了解了ThreadLocal后就开始对我们的业务进行优化
在项目中引入com/xnj/utils/ThreadLocal工具类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* ThreadLocal 工具类
*/
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}
回到拦截器com/xnj/interceptor/LoginInterceptor中
在放行前存入业务数据,在用户请求结束后,清空ThreadLocal数据,防止内存溢出1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//将我们创建的拦截器对象注入IOC容器
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证Token
try {
Map<String,Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//设置Http响应状态为401
response.setStatus(401);
//不放行
return false;
}
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//请求完成之后清除ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}
现在来 完善获取用户详细信息 的业务1
2
3
4
5
6
7
8
9
10//获取用户信息
@GetMapping("/userinfo")
public Result<User> userinfo(){
//从ThreadLocal中获取用户信息
Map<String,Object> claims= ThreadLocalUtil.get();
String username = (String) claims.get("username");
User user = userService.findByUsername(username);
return Result.success(user);
}
用户更新功能
用户更新功能实现
com/xnj/controller/UserController1
2
3
4
5
6//更新用户信息
public Result update({ User user)
userService.update(user);
return Result.success();
}
com/xnj/service层1
2
3
4
5
6
7
8
9
10
11//com/xnj/service/UserService
//更新用户信息
void update(User user);
//com/xnj/service/impl/UserServiceImpl
//更新用户信息
public void update(User user) {
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}
com/xnj/mapper/UserMapper1
2
3//更新用户
void update(User user);
测试1
2
3
4
5
6
7
8PUT: http://localhost:8080/user/update
Body.raw(json)
{
"id":3,
"username":"zhangsan",
"nickname":"小张三",
"email":"3838438@qq.com"
}
对用户信息更改进行优化
上面的功能并未对字段进行输入限制,通过Validated
进行字段约束
com/xnj/pojo/User1
2
3
4
5
6
7
8
private Integer id;//主键id
//1~10位的任意字母
private String nickname;//昵称
//邮箱格式
private String email;//邮箱
com/xnj/controller/UserController1
2
3
4
5
6//更新用户信息
public Result update({ User user)
userService.update(user);
return Result.success();
}
总结实体参数效验
- 实体类的成员变量上添加注解
@NotNull @NotEmpty @Email
- 接口方法的实体参数上添加
@Validated
注解
更新用户头像
更新用户头像功能实现
com/xnj/controller/UserController1
2
3
4
5
6
7
8
9//更新用户头像
public Result updateAvatar({ String avatarurl)//@URL约束传入参数为url格式
//获取当前用户id
Map<String,Object> claims = ThreadLocalUtil.get();
Integer id = (Integer) claims.get("id");
userService.updateAvatar(id,avatarurl);
return Result.success();
}
com/xnj/service/1
2
3
4
5
6
7
8
9
10//com/xnj/service/UserService
//更新用户头像
void updateAvatar(Integer id,String avatarurl);
//com/xnj/service/impl/UserServiceImpl
//更新用户头像
public void updateAvatar(Integer id,String avatarurl) {
userMapper.updateAvatar(id,avatarurl);
}
com/xnj/mapper/UserMapper1
2
3//更新用户头像
@Update("update user set user_pic=#{avatarurl},update_time=now() where id=#{id}")
void updateAvatar(Integer id, String avatarurl);
测试1
PATCH: http://localhost:8080/user/updateAvatar?avatarurl=https://picbed.xusir.fun/img/post_default_9.webp
更新用户密码
更新用户密码功能实现
com/xnj/controller/UserController1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33//更新用户密码
public Result updatePwd({ Map<String,String> params)
//获取参数
String old_pwd = params.get("old_pwd");//旧密码
String new_pwd = params.get("new_pwd");//新密码
String re_pwd = params.get("re_pwd");//确认密码
//判断输入的密码是否为空
if(!StringUtils.hasLength(old_pwd)||!StringUtils.hasLength(new_pwd)||!StringUtils.hasLength(re_pwd)){
return Result.error("密码不能为空");
}
//从ThreadLocal中获取用户信息
Map<String,Object> claims = ThreadLocalUtil.get();
String username = (String) claims.get("username");
//根据用户名查询用户
User user = userService.findByUsername(username);
if(!user.getPassword().equals(Md5Util.getMD5String(old_pwd))){
return Result.error("旧密码错误");
}
//判断两次输入的密码是否一致
if(!new_pwd.equals(re_pwd)){
return Result.error("两次输入的密码不一致");
}
//更新密码
userService.updatePwd(new_pwd);
return Result.success();
}
com/xnj/service/1
2
3
4
5
6
7
8
9
10
11
12
13
14//com/xnj/service/UserService
//更新密码
void updatePwd(String newPwd);
//com/xnj/service/impl/UserServiceImpl
//更新用户密码
public void updatePwd(String newPwd) {
Map<String,Object> claims= ThreadLocalUtil.get();
Integer id = (Integer) claims.get("id");
//密码需要加密后才能存储到数据库
userMapper.updatePwd(id,Md5Util.getMD5String(newPwd));
}
com/xnj/mapper/UserMapper1
2
3//更新用户密码
void updatePwd(Integer id,String newPwd);
功能测试1
2
3
4
5
6
7
8PATCH: http://localhost:8080/user/updatePwd
Body.raw(json)
{
"old_pwd":"123456",
"new_pwd":"654321",
"re_pwd":"654321"
}
添加文章分类
添加文章分类功能实现
创建以下文件CategoryController
,CategoryService
,CategoryServiceImpl
,CategoryMapper
com/xnj/controller/CategoryController1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CategoryController {
private CategoryService categoryService;
//添加文章分类
public Result add( { Category category)
categoryService.add(category);
return Result.success();
}
}
前端只会传分类名和分类别称,所以要加约束
com/xnj/pojo/Category1
2
3
4@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
com/xnj/service/CategoryService1
2
3
4public interface CategoryService {
//添加分类
void add(Category category);
}
com/xnj/service/impl/CategoryServiceImpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CategoryServiceImpl implements CategoryService {
private CategoryMapper categoryMapper;
//添加分类
public void add(Category category) {
//设置创建时间和更新时间
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
//设置创建人
Map<String,Object> claims = ThreadLocalUtil.get();
Integer id = (Integer) claims.get("id");
category.setCreateUser(id);
//添加
categoryMapper.add(category);
}
}
com/xnj/mapper/CategoryMapper1
2
3
4
5
6
7
public interface CategoryMapper {
//添加文章分类
void add(Category category);
}
测试1
2
3
4
5
6POST: http://localhost:8080/category
Body.raw(json)
{
"categoryName":"人文",
"categoryAlias":"rw"
}
查询文章分类列表
查询文章分类列表功能实现
com/xnj/controller/CategoryController1
2
3
4
5
6//查询当前用户创建的文章分类
public Result<List<Category>> list(){
List<Category> cs = categoryService.list();
return Result.success(cs);
}
com/xnj/service1
2
3
4
5
6
7
8
9
10
11
12
13
14//com/xnj/service/CategoryService
//查询当前用户创建的文章分类
List<Category> list();
//com/xnj/service/impl/CategoryServiceImpl
public List<Category> list() {
//获取当前用户id
Map<String,Object> claims = ThreadLocalUtil.get();
Integer id = (Integer) claims.get("id");
//查询并返回查询结果
return categoryMapper.list(id);
}
为了规范返回前端的文章分类列表的创建时间和更新时间
需要给com/xnj/pojo/Category的字段添加注解1
2
3
4
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//修改时间
com/xnj/mapper/CategoryMapper1
2
3//查询当前用户创建的文章分类
List<Category> list(Integer id);
测试1
GET: http://localhost:8080/category
查询分类详细信息
查询分类详细信息功能实现
com/xnj/controller/CategoryController1
2
3
4
5
public Result<Category> detail(Integer id){
Category c = categoryService.detail(id);
return Result.success(c);
}
com/xnj/service1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//com/xnj/service/CategoryService
//根据id查询分类详情
Category detail(Integer id);
//com/xnj/service/impl/CategoryServiceImpl
//根据id查询分类详情
public Category detail(Integer id) {
//获取当前用户id
Map<String,Object> claims = ThreadLocalUtil.get();
Integer userId = (Integer) claims.get("id");
//查询并返回查询结果
Category category = categoryMapper.detail(id,userId);
return category;
}
com/xnj/mapper/CategoryMapper1
2
3//根据id查询分类详情
Category detail(Integer id,Integer userId);
测试1
2GET: http://localhost:8080/category/detail
Query: id (Integer) :2
更新分类信息
更新分类信息功能实现
com/xnj/controller/CategoryController1
2
3
4
5
6//更新文章分类
public Result update({ Category category)
categoryService.update(category);
return Result.success();
}
com/xnj/service1
2
3
4
5
6
7
8
9
10
11
12//com/xnj/service/CategoryService
//更新文章分类
void update(Category category);
//com/xnj/service/impl/CategoryServiceImpl
//更新文章分类
public void update(Category category) {
category.setUpdateTime(LocalDateTime.now());
categoryMapper.update(category);
}
校验id字段
com/xnj/pojo/Category1
2
private Integer id;//主键id
com/xnj/mapper/CategoryMapper1
2
3//更新文章分类
void update(Category category);
测试1
2
3
4
5
6
7PUT: http://localhost:8080/category
Body.raw(json)
{
"id":1,
"categoryName":"人文",
"categoryAlias":"renwen"
}
更新文章分类功能是完成了,但是有个bug,此时再进行添加分类就会出现报错
因为在这里我们指定了id字段为@NotNull,该怎么解决冲突呢,使用valiated默认分组来解决
在com/xnj/pojo/Category中添加如下字段即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Category {
private Integer id;//主键id
private String categoryName;//分类名称
private String categoryAlias;//分类别名
//如果某个效验项没有指定分组,默认为Default分组
//分组之间可以继承,A extends B 则A分组可以继承B中所有的效验项
//每个接口代表一个分组
public interface Add extends Default{ }
public interface Update extends Default { }
}
在com/xnj/controller/CategoryController的添加和更新功能上指定Category.分组.class
1
2
3
4
5
6
7
8//添加分类
public Result add( Category category)
//更新文章分类
public Result update( Category category)
删除分类
删除分类功能实现
com/xnj/controller/CategoryController1
2
3
4
5
6//删除文章分类功能
public Result delete({ Integer id)
categoryService.delete(id);
return Result.success();
}
com/xnj/service1
2
3
4
5
6
7
8
9
10// com/xnj/service/CategoryService
//删除文章分类
void delete(Integer id);
// com/xnj/service/impl/CategoryServiceImpl
//删除分类
public void delete(Integer id) {
categoryMapper.deleteById(id);
}
com/xnj/mapper/CategoryMapper1
2
3//删除分类
void deleteById(Integer id);
测试1
DELETE: http://localhost:8080/category/3
新增文章
新增文章功能
新建文章的controller,com/xnj/controller/ArticleController1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ArticleController {
private ArticleService articleService;
public Result add({ Article article)
articleService.add(article);
return Result.success();
}
}
新建文章的service,com/xnj/service1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//com/xnj/service/ArticleService
public interface ArticleService {
// 新增文章
void add(Article article);
}
//com/xnj/service//impl/ArticleServiceImpl
public class ArticleServiceImpl implements ArticleService {
private ArticleMapper articleMapper;
public void add(Article article) {
// 填充更新时间,创建时间,创建人
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
Map<String,Object> claims = ThreadLocalUtil.get();
article.setCreateUser((Integer) claims.get("id"));
articleMapper.add(article);
}
}
新建文章的Mapper,com/xnj/mapper/ArticleMapper1
2
3
4
5
6
7
public interface ArticleMapper {
void add(Article article);
}
自定义校验(validation)
前端传来的数据要求有title,content,coverImg,state,categoryId,所以要做非空校验
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
title | 文章标题 | String | 是 | 1-10个非空字符 |
content | 文章内容 | String | 是 | |
coverImg | 封面图片地址 | String | 是 | 必须是url地址 |
state | 发布状态 | String | 是 | 已发布or草稿 |
categoryId | 分类ID | number | 是 |
开始为Article文章类设置校验
controller的添加方法参数上添加@Validated注解1
public Result add( Article article)
为Article文章类属性注解校验规则1
2
3
4
5
6
7
8
9
10
private String title;//标题
private String content;//内容
private String coverImg;//封面图
private Integer categoryId;//分类id
关于state文章发布状态字段只能是(已发布|草稿),可以自定义Validation校验注解
创建如下
com/xnj/anno/State
注解类
写的时候可以参照@NotEmpty
看它引入了哪些注解,以及实现前三个方法message()
,groups()
,payload() default
1
2
3
4
5
6
7
8
9
10
11
12//元注解
//y元注解作用于字段
//元注解
//指定提供校验规则的类
public State {
//提供校验失败的提示信息
String message() default "state参数只能是已发布或者草稿";
//指定分组
Class<?>[] groups() default {};
//负载 获取到State注解的附加信息
Class<? extends Payload>[] payload() default {};
}创建
com/xnj/validation/StateValidation
类来提供@State
注解校验规则1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//ConstraintValidator<给哪个注解提供校验规则,校验的数据类型>泛型
public class StateValidation implements ConstraintValidator<State,String> {
/**
*
* @param value 将来要校验的数据
* @param constraintValidatorContext
* @return 返回true表示校验通过,返回false表示校验失败
*/
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
//提供校验规则
if(value==null){
return false;
}
if(value.equals("已发布")||value.equals("草稿")){
return true;
}
return false;
}
}给
state字段
添加我们自定义的@State注解
1
2
private String state;//发布状态 已发布|草稿
文章列表(条件分页)
文章列表(条件分页)功能实现
参数名称 | 说明 | 类型 | 是否必须 | 备注 |
---|---|---|---|---|
pageNum | 当前页码 | number | 是 | |
pageSize | 每页条数 | number | 是 | |
categoryId | 分类ID | number | 否 | |
state | 发布状态 | String | 否 | 已发布or草稿 |
com/xnj/controller/ArticleController
文章分类id和文章发布状态,应该为可传参数,并不必需1
2
3
4
5
6
7
8
9
10
11
12//文章分页查询
public Result<PageBean<Article>> getPage(
Integer pageNum,//页码
Integer pageSize,//每页大小
Integer categoryId,//非必要参数
String state//非必要参数
){
PageBean<Article> pageBean = articleService.getPage(pageNum, pageSize, categoryId, state);
System.out.println(pageBean);
return Result.success(pageBean);
}
我们要借助pageHelper插件来完成分页查询,引入依赖1
2
3
4
5
6<!-- 分页插件依赖-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
com/xnj/service1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//com/xnj/service/ArticleService
// 文章分页查询
PageBean<Article> getPage(Integer pageNum, Integer pageSize, Integer categoryId, String state);
//com/xnj/service//impl/ArticleServiceImpl
public PageBean<Article> getPage(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
//1.创建PageBean对象
PageBean<Article> pageBean = new PageBean<>();
//2.开启分页查询,PageHelpper
PageHelper.startPage(pageNum,pageSize);
//3.执行mapper
Map<String,Object> claims = ThreadLocalUtil.get();
Integer createUser = (Integer) claims.get("id");
List<Article> ac = articleMapper.selectPage(createUser,categoryId,state);
//Page中提供了方法,可以获取PageHelper分页查询后,得到的总记录数和当前数据
Page<Article> p = (Page<Article>) ac;
//4.设置pageBean
pageBean.setTotal(p.getTotal());//总记录数
pageBean.setItems(p.getResult());//当前数据
System.out.println(pageBean);
return pageBean;
}
com/xnj/mapper/ArticleMapper1
List<Article> selectPage(Integer createUser, Integer categoryId, String state);
该分页查询为条件查询,引入ArticleMapper.xml文件
resource/com/xnj/mapper/ArticleMapper.xml 注意,这里创建多级文件目录不能用.来分隔,要用/
这里有几个坑,
namespace对应为mapper全路径
,id对应mapper里的方法名
,resultType对应返回类全路径
1 |
|
测试1
2
3
4
5
6
7
8
9
10GET: http://localhost:8080/article?categoryId=3&state=已发布&pageNum=1&pageSize=3
Query:
|参数名|类型|参数值|
|:---:|:---:|:---:|
|pageSize|Integer|3|
|pageNum|Integer|1|
|categoryId|Integer|3|
|state|String|已发布|
获取文章详情/更新文章/删除文章
查看详情
这三个功能比较简单,直接展示完整代码
com/xnj/controller/ArticleController1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//获取文章详情
public Result<Article> detail({ Integer id)
Article article = articleService.detail(id);
return Result.success(article);
}
//更新文章
public Result update({ Article article)
articleService.update(article);
return Result.success();
}
//删除文章
public Result delete({ Integer id)
articleService.delete(id);
return Result.success();
}
com/xnj/service/ArticleService1
2
3
4
5
6// 文章详情
Article detail(Integer id);
//更新文章
void update(Article article);
//删除文章
void delete(Integer id);
com/xnj/service/impl/ArticleServiceImpl
这里如果要更加严谨也可以在每个功能加一个id认证,然后在mapper里加and id=#{createUser}
即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//根据id查询文章详情
public Article detail(Integer id) {
Article article = articleMapper.selectById(id);
return article;
}
//更新文章
public void update(Article article) {
article.setUpdateTime(LocalDateTime.now());
articleMapper.update(article);
}
//删除文章
public void delete(Integer id) {
articleMapper.delete(id);
}
com/xnj/mapper/ArticleMapper1
2
3
4
5
6
7
8
9
Article selectById(Integer id);
void update(Article article);
void delete(Integer id);
功能测试1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//获取文章详情
GET: http://localhost:8080/article/3
//更新文章
PUT:http://localhost:8080/article
Body.raw(json)
{
"id":4,
"title":"巴黎奥运会运动员被骂",
"content":"严禁并预防饭圈文化,祸端之源a",
"coverImg":"https://picbed.xusir.fun/img/post_default_6.webp",
"state":"已发布",
"categoryId":3
}
//删除文章
DELETE:http://localhost:8080/article/5
文件上传
查看文件上传
创建com/xnj/controller/FileUploadController1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FileUploadController {
public Result<String> upload(MultipartFile file) throws IOException {
//把文件内容存储在本地磁盘上
String originalFilename = file.getOriginalFilename();
//为了保证文件名称唯一,使用uuid
String filename= UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
file.transferTo(new File("C:\\Users\\xnj\\Desktop\\big-event-img\\"+filename));
return Result.success("上传成功");
}
}
测试1
2
3
4
5
6POST: http://localhost:8080/upload
Body.form-data
参数名: file File
参数值: bg.jpg
可以看到bg.jpg文件下载到了C:\Users\xnj\Desktop\big-event-img\中,文件名为filename
阿里Oss(AliOss)
在阿里云右上角控制台,在服务里搜索OSS即可找到 点击右上角头像,生成AccessKey,建议下载到本地,且不要泄露给别人 在对象存储OSS左侧菜单栏下方有SDK下载 AliOss
在阿里云创建对象存储OSS
创建Bucket 读取权限设置为公共读
详情查看·SDK示例·,在·小窗口·中选择·文档中心·打开,这里选择java
点击·安装·在Maven项目中加入依赖项(推荐方式)
复制依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<!-- 阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<!--java9以上要导入以下依赖-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
看·对象/文件/上传文件/简单上传·看对应Demo
这里直接提供该项目根据demo而改成的工具类
com/utils/AiOssUtil1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41class AliOssUtil {
//区域节点
//点进你的bucket概览,往下可以看到访问端口
private static final String ENDPOINT = "";
//下面为你的AccessKey
private static final String ACCESS_KEY_ID = "";
private static final String SECRET_ACCESS_KEY = "";
//Bucket项目名字
private static final String BUCKET_NAME = "";
//上传文件,返回文件的公网访问地址
public static String uploadFile(String objectName, InputStream inputStream){
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ENDPOINT,ACCESS_KEY_ID,SECRET_ACCESS_KEY);
//公文访问地址
String url = "";
try {
// 创建存储空间。
ossClient.createBucket(BUCKET_NAME);
ossClient.putObject(BUCKET_NAME, objectName, inputStream);
//url组成:https://bucket名称.区域节点/objectName
url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objectName;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
在导入AiOss的依赖和AiOssUtil工具类,并且配置完你的信息后
回到FileLoadControllerController对代码进行如下改动1
2
3//file.transferTo(new File("C:\\Users\\xnj\\Desktop\\big-event-img\\"+filename));
String url = AliOssUtil.uploadFile(filename, file.getInputStream());
return Result.success(url);
在ApiPost进行接口测试,上传文件,返回”操作成功“和url
在浏览器中访问url即可下载刚刚上传的图片文件1
2
3
4
5{
"code": 0,
"message": "操作成功",
"data": "https://xnjbig-event.oss-cn-beijing.aliyuncs.com/b85e024a-a35d-4f2c-addb-4ad611ec2cfe.webp"
}
或者去你的阿里云的OSS,在Bucket列表里找到你的bucket,点进去可以看到文件上传上来了!
自此上传文件功能接口完成。
Redis
Redis
项目到这里还有一个bug
用户更改密码后,就算不用新生成的token,旧token在过期之前仍然有效
使用radis来让令牌主动失效
- 登陆成功后,给浏览器响应令牌的同时,把该令牌存储到redis中
- LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
- 当用户修改密码后,删除redis中存储的旧令牌
通过redis测试用例来了解redis
引入redis依赖1
2
3
4
5<!--redis坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件application.yml1
2
3
4
5spring:
data:
redis:
host: localhost
port: 6379
test/java/com/xnj/RedisTest1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36package com.xnj;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
//如果在测试类上添加了这个注解,那么将来单元测试方法执行之前,会先初始化Spring容器
public class RedisTest {
private StringRedisTemplate stringRedisTemplate;
public void testSet(){
//往redis中存储一个键值对 StringRedisTemplate
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set("username","xusir");
//打开redis绿色版 双击redis-server.exe启动redis,
//运行测试类
//redis-cli.exe 连接redis,输入 get username,能看到xusir
operations.set("id","1",15, TimeUnit.SECONDS);//设置键值对并设置过期时间,15s
}
public void testGet(){
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String username = operations.get("username");
System.out.println(username);//控制台打印结果: xusir
}
}
使用Redis完善用户登录和更新密码
redis优化token机制
- 在登录功能里面,生成token时,把token存入redis中,设置并失效时间
- 在拦截器中,验证token时比较redis中存入的token是否一致
- 在更改密码功能中,修改完密码后,删除redis中存储的旧令牌
具体实现代码如下
com/xnj/controller/UserController
改动:7,28~30,66~68 行com/xnj/interceptors/LoginInterceptor1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class UserController {
private StringRedisTemplate stringRedisTemplate;
private UserService userService;
//用户登录
public Result login({ String username, String password)
User loginUser = userService.findByUsername(username);
//判断用户是否存在
if(loginUser==null){
return Result.error("用户不存在");
}
//判断密码是否正确,loginUser里的password是密文
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//密码正确,生成token
Map<String,Object> claims = new HashMap<>();
//token中存入用户id和用户名username
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
String token = JwtUtil.genToken(claims);
//将token存入redis中,并设置过期时间为12小时
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token,token,12, TimeUnit.HOURS);
return Result.success(token);
}else{
return Result.error("密码错误");
}
}
//更新用户密码
public Result updatePwd({ Map<String,String> params, String token)
//获取参数
String old_pwd = params.get("old_pwd");//旧密码
String new_pwd = params.get("new_pwd");//新密码
String re_pwd = params.get("re_pwd");//确认密码
//判断输入的密码是否为空
if(!StringUtils.hasLength(old_pwd)||!StringUtils.hasLength(new_pwd)||!StringUtils.hasLength(re_pwd)){
return Result.error("密码不能为空");
}
//从ThreadLocal中获取用户信息
Map<String,Object> claims = ThreadLocalUtil.get();
String username = (String) claims.get("username");
//根据用户名查询用户
User user = userService.findByUsername(username);
if(!user.getPassword().equals(Md5Util.getMD5String(old_pwd))){
return Result.error("旧密码错误");
}
//判断两次输入的密码是否一致
if(!new_pwd.equals(re_pwd)){
return Result.error("两次输入的密码不一致");
}
//更新密码
userService.updatePwd(new_pwd);
//删除redis中的旧token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.getOperations().delete(token);
return Result.success();
}
}
改动:13~19行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38//将我们创建的拦截器对象注入IOC容器
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证Token
try {
//从redis中获取相同的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String redisToken = operations.get(token);
if(redisToken==null){
//token已经失效了,抛出异常,会被下面catch捕获,返回401错误
throw new RuntimeException();
}
Map<String,Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//设置Http响应状态为401
response.setStatus(401);
//不放行
return false;
}
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//请求完成之后清除ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}
到这里
big-event
项目的后端业务功能开发完毕了。
SpringBoot项目部署
部署流程
此处为Windows系统操作!!!
在pom.xml引入打包插件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<build>
<plugins>
<!-- 打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!--
我打包时冒如下错误,排查了一会发现应该是我测试类里代码的问题,引入如下插件将测试错误忽略掉就好。
SpringBoot打包失败:Please refer to XXX\target\surefire-reports for the individual test results.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
在右侧maven中刷新后,在生命周期中双击package打包。显示BUILD SUCCESS后
在idea左侧项目菜单的target
目录下就能看到打包好的jar包了。big-event-1.0-SNAPSHOT.jar
注意:要重新打jar包时。删除jar包可以在maven里
mvn clean
会删除加包,再mvn package
即可重新打包
这里在本地模拟服务器上启动项目。。。。
找到jar包的资源管理器目录处(在idea中可以直接右键jar包,打开于资源管理器),在上方地址输入cmd
再输入 java -jar big-event-1.0-SNAPSHOT.jar
按enter运行jar包. 按Ctrl+C终止
注意:jar包部署必须要有jre环境!
SpringBoot属性配置
打包好的项目是一个jar包,如果要改配置怎么办?
方式一: 命令行参数方式 方式二:环境变量方式 方式三: 外部配置文件方式 关于配置的优先级 点开查看
--键=值
java -jar big-event-1.0-SNAPSHOT.jar --server.port=9999
application.yml
配置文件
SpringBoot多环境开发-Pofiles
点开查看
方式一:多环境开发的单文件使用
- 使用
---
分隔不同环境的配置 - spring.config.activate.on-profile 配置所属的环境
- spring.profiles.active 激活环境
1 | # 通用信息,指定生效的环境 |
方式二:多配置文件
文件名称为 application-环境名称.yml
- 开发环境
application-dev.yml
- 测试环境
application-test.yml
- 生产环境
application-pro.yml
- 共性配置并激活指定环境
application.yml
激活测试环境如下
1
2
3spring:
profiles:
active: test # 当前为测试环境
SpringBoot多环境开发-Pofiles分组
application-服务名称.yml
比如服务器相关配置写在: application-devServer.yml里
数据源相关配置写在: application-devDB.yml里
自定义相关配置写在: application-devSelf.yml里
在application.yml中定义如下即可使用1
2
3
4
5
6spring:
profiles:
active: dev #选择使用哪个分组
group: #定义分组
"dev":devServer,devDB,devSelf
"test":testServer,testvDB,testSelf
Vue3+Element-Plus—big-event前端开发
JavaScript-导入导出
点开查看
创建Vue_code
文件夹,message.html
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>js导入导出</title>
</head>
<body>
<div id="app">
<button id="btn">点我展示信息</button>
</div>
<script type="module">
/* //简单导入 在js方法前面加`export`,这里{方法名},
import {simpleMessage} from './showMessage.js'
document.getElementById("btn").onclick= function(){
simpleMessage("我被点击了");
} */
//批量导入 在js下面写`export{方法1,方法2 as 别名}`,这里{方法名,别名},
// import {simpleMessage,cm} from './showMessage.js'
//如果这里给方法起别名,调用时就用别名
import {simpleMessage as sm,cm} from './showMessage.js'
document.getElementById("btn").onclick= function(){
// simpleMessage("批量导入,我被点击了");
cm("批量导入,被点击了");
sm("别名,被点击了")
}
/* //默认导出 此时`mesgMethods`代表了js中所有导出内容 `mesgMethods.方法`
import mesgMethods from './showMessage.js'
document.getElementById("btn").onclick= function(){
mesgMethods.complexMessage("我被点击了");
} */
</script>
</body>
</html>
同目录下showMessage.js
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//简单的展示信息
/* export */ function simpleMessage(msg){
console.log(msg)
}
//复杂的展示信息
/* export */ function complexMessage(msg){
console.log(new Date()+": "+msg)
}
//批量导出 这里起别名的话,在引用时就需要写别名
export {simpleMessage,complexMessage as cm}
//默认导出
// export default{simpleMessage,complexMessage}
在浏览器打开控制器根据查看F12 或 Fn+F12
局部使用Vue
vscode 格式化代码快捷键: 进入vue官网-》https://cn.vuejs.org/ 点击安装(使用 ES 模块构建版本) v-for: v-if和v-show的区别 v-on: v-model: 常用指令演示代码 生命周期八个阶段,每个阶段自动执行钩子函数。 经常用的是mounted函数,它在使用时是和data,methods,是平级的。alt+shift+f
点开查看
Vue入门案例
查看入门代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{msg}}</h1>
</div>
<!--引入vue模块-->
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建vue的应用实例
createApp({
data(){
return{
//定义数据
msg: 'helloVue3'
}
}
}).mount("#app");
</script>
</body>
</html>常用指令
点开查看
指令 作用 v-for 列表渲染,遍历容器的元素或者对象的属性 v-bind 为HTMl标签绑定属性值,如设置href,css样式 v-if/v-else-if/v-else 条件性的渲染某元素,判定为true时渲染 v-show 根据条件展示某元素,区别在于切换的是display属性的值 v-model 为表单元素上创建双向数据绑定 v-on 为HTMl标签绑定事件
语法: v-on:事件名=”函数名” 简写为 @事件名=”函数名”1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- v-model -->
文章分类:<input type="text" v-model="searchConditions.category"/><span>{{searchConditions.category}}</span>
发布状态:<input type="text" v-model="searchConditions.state"/><span>{{searchConditions.state}}</span>
<button>搜索</button>
<button @click="clear">重置</button>
<br/>
<br/>
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>序号</th>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<!-- v-for -->
<tr v-for="(item,index) in articleList ">
<td>{{index+1}}</td>
<td>{{item.title}}</td>
<td>{{item.category}}</td>
<td>{{item.time}}1</td>
<td>{{item.state}}</td>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
<!-- v-bind -->
<a v-bind:href="url">黑马官网</a>--
<a :href="url">黑马官网</a>
<br />
<!-- v-if -->
衬衫的价格是:<span v-if="customer.level>=0 && customer.level<=1">9榜15便士</span>
<span v-else-if="customer.level>= 2 && customer.level <=4">19榜15便士</span>
<span v-else>29榜15便士</span>
<br />
<!-- v-show -->
毛线的价格是:<span v-show="customer.level>=0 && customer.level<=1">15便士</span>
<span v-show="customer.level>= 2 && customer.level <=4">25便士</span>
<span v-show="customer.level>=5">35便士</span>
<br />
<!-- v-on -->
<button v-on:click="money">点我有惊喜</button>
<button @click="love">点我有惊喜</button>
</div>
<script type="module">
//导入vue模块
import { createApp } from
'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建应用实例
createApp({
data() {
return {
searchConditions:{
category:'',
state:''
},
articleList: [{
title: "医疗反腐绝非砍医护收入",
category: "时事",
time: "2023-09-5",
state: "已发布"
},
{
title: "中国男篮缘何一败涂地?",
category: "篮球",
time: "2023-09-5",
state: "草稿"
},
{
title: "华山景区已受大风影响阵风达7-8级,未来24小时将持续",
category: "旅游",
time: "2023-09-5",
state: "已发布"
}],
url: 'https://itheima.com',
customer: {
name: '张山',
level: 2
}
}
},
methods: {
money: function () {
alert("支付宝到账100¥")
},
love: function () {
alert("爱你一万年")
},
clear: function(){
//在methods对应的方法里面,用this就代表vue实例,可以使用this获取vue实例中的数据
this.searchConditions.category='';
this.searchConditions.state='';
}
}
}).mount("#app")//控制页面元素
</script>
</body>
</html>Vue的生命周期
查看
状态 阶段周期 beforeCreate 创建前 created 创建后 beforeMount 载入前 mounted 挂载完成 beforeUpdate 数据更新前 updated 数据更新后 beforeUnmount 组件销毁前 unmounted 组件销毁后
应用场景:在页面加载完毕时,发起异步请求,加载数据,渲染页面。
Axios
查看Axios异步请求
Axios官网:https://www.axios-http.cn/ axios对原生的ajax进行了封装,简化书写,快速开发。
CDN引用方式:
关于跨域:在你的后端代码controller上面添加
@CrossOrigin
来支持跨域
常见的发送请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24axios({
method:'get', //请求方式
url:'http://localhost:8080/user/getall'
}).then(result=>{
//成功的回调
//result代表了服务器响应的所有的数据,包含了响应头,响应体,result.data代表接口响应的核心数据
console.log(result.data);
}).catch(err=>{
//失败的回调
console.log(err);
})
axios({
method:'post', //请求方式
url:'http://localhost:8080/user/add'
data: this.user
}).then(result=>{
//成功的回调
//result代表了服务器响应的所有的数据,包含了响应头,响应体,result.data代表接口响应的核心数据
console.log(result.data);
}).catch(err=>{
//失败的回调
console.log(err);
})
别名方式发送请求1
2
3
4
5
6
7
8
9
10axios.get('http://localhost:8080/user/getAll').then((result)=>{
console.log(result.data);
}).catch((err)=>{
console.log(err);
});
axios.post('http://localhost:8080/user/findById','id=1').then((result)=>{
console.log(result.data);
}).catch((err)=>{
console.log(err);
});
Vue小项目
Vue小项目
原生html vue_case.html
,axios异步请求来实现渲染界面
html页面
1 |
|
后端代码
1 | package com.xnj.controller; |
1 |
|
整站使用Vue(工程化使用vue)
查看教程
下载nodejs 需要16版本以上
配置运行权限和淘宝镜像。
npm config set prefix “D:\Develop\NodeJS” 路径为nodejs的安装目录
配置在管理员方式运行,保证获取足够的权限
- 在自定义创建的code文件夹内执行
npm init vue@latest
来创建一个工程化的Vue项目
提示 | 含义 |
---|---|
Project name: | 项目名称,默认值:vue-project,可输入想要的项目名称 |
Add TypeScript? | 是否加入TypeScript组件?默认值:No |
Add JSX Support? | 是否加入JSX支持?默认值:No. |
Add Vue Router … | 是否为单页应用程序开发添加Vue Router路由管理组件?默认值:No |
Add Pinia .. | 是否添加Pinia组件来进行状态管理?默认值:No |
Add Vitest … | 是否添加Vitest来进行单元测试?默认值:No |
Add an End-to-End … | 是否添加端到端测试?默认值No |
Add ESLint for code quality? | 是否添加ESLint来进行代码质量检查?默认值:No |
可以按右箭头来选择yes或no,直接enter为默认。创建完成后会提示你三条命令1
2
3cd vue-project
npm install
npm run dev
- 进入项目目录
cd vue-project
,执行命令安装当前依赖:npm install
- 为了方便,使用vscode来打开,直接输入
code .
vue项目-目录结构
vue-project目录 | 内容 |
---|---|
node_modules | 下载的第三方包存放目录 |
public | 公共资源 |
src | 源码存放目录,即写代码的文件夹 |
index.html | 默认首页 |
package-lock.json | 项目配置文件(无需修改) |
package.json | 项目配置文件,包括项目名,版本号,依赖包,版本等 |
vite.config.js | Vue项目的配置信息,如端口号等 |
src目录 | 内容 |
---|---|
assets | 静态资源目录,存放图片,字体等 |
components | 组件目录,存放通用组件 |
App.vue | 根组件 |
main.js | 入口文件 |
- 启动项目(默认端口为5173,关闭为
Ctrl+C
)
- 方式一:在项目目录
vue-project
输入npm run dev
访问localhost:5173 - 方式二:vscode左下脚NPM脚本中,点击dev 运行。
- Vue.app文件包含三个部分
<script></script>
部分,用来写逻辑部分<template></template>
部分 用来写html元素部分<style></style>
部分 用来写css样式部分
Vue的API风格
查看风格
选项式API 结构更直观1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<script>
export default{
data(){ //声明响应式数据
return{
count: 0
},
},
methods:{ //声明方法,可以通过组件实例访问
add: function(){
this.count++;
}
},
mounted(){ //声明钩子函数
console.log('Vue mounted..')
}
}
</script>
<template>
<button @click="add">count:{{count}}</button>
</template>
组合式API 使用更灵活1
2
3
4
5
6
7
8
9
10
11
12
13<script setup> //setup是一个标识,告诉vue让我们更简洁的使用组合式api
import {onMounted,ref} from 'Vue';
const count = ref(0); //组合式api中数据定义为响应式数据
function add(){ //声明函数
count.value++; //响应式变量,响应式对象有一个内部属性value
}
onMounted(()=>{ //声明钩子函数
console.log('Vue Mounted..')
})
</script>
<template>
<button @click="add">count:{{count}}</button>
</template>
组合式api小demo
在src下创建API.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14<script setup>
import {ref,onMounted} from 'vue'
const count = ref(0);
function add(){
count.value++;
}
onMounted(()=>{
console.log("vue 挂载完成")
})
</script>
<template>
<button @click="add">count:{{ count }}</button>
</template>
然后在App.vue中引入API.vue
- 在
script
里添加import ApiVue from './API.vue'
- 在
template
里添加<ApiVue/>
即可
案例练习
查看案例练习
后端代码参照《Vue小项目》章节提供的代码。
- 创建Article.vue文件
- 两个input输入框,文章类别,发布状态
- 一个搜索按钮
- 一个表格:标题,分类,发表时间,状态,操作
- 在App.vue中引入Article.vue
- 和之前一样,使用axios,v-for,v-on,v-model,来完成页面
- 安装axios
- 在项目目录
VUE-PROJECT
中输入npm install axios
- vue会把下载的axios放在node_modules目录下
- 在
script
标签里添加import axios from 'axios'
完成源码展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63<script setup>
//导入axios npm install axios
import axios from 'axios';
import { ref } from "vue";
//定义响应式数据
const articleList = ref([]);
//发送异步请求获取全部文章数据
axios.get('http://localhost:8081/article/getAll').then((result)=>{
//响应成功
articleList.value=result.data;
}).catch((err)=>{
//响应失败
console.log(err);
})
//定义响应式数据 searchConditions
const searchConditions = ref({
category:'',
state:''
})
//搜索
const search=function(){
//发送异步请求,查询数据
// axios.get('http://localhost:8081/article/search?category='+searchConditions.value.category+'&state='+searchConditions.value.state)
axios.get('http://localhost:8081/article/search',{params:{...searchConditions.value}})
.then((result)=>{
articleList.value=result.data;
}).catch((err)=>{
console.log(err);
})
}
</script>
<template>
<div>
文章分类:<input type="text" v-model="searchConditions.category"/>
发布状态:<input type="text" v-model="searchConditions.state"/>
<button v-on:click="search">搜索</button>
<br />
<br />
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="(article,index) in articleList" :key="index">
<td>{{ article.title }}</td>
<td>{{ article.category }}</td>
<td>{{ article.time }}</td>
<td>{{ article.state }}</td>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
</div>
</template>结构优化一
- 为了代码的复用性,把接口请求写到一个js文件中。创建src/api/article.js
- 同时因为异步请求,为了同步数据,需要在js和vue的方法里面都加上
async
和await
- 定义一个变量抽取公开请求路径前缀。
baseURL
Article.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//导入axios npm install axios
import axios from 'axios';
//定义一个变量,记录公开前缀 baseURL
const baseURL = 'http://localhost:8081';
const instance = axios.create({baseURL});
//获取所有文章数据的函数
export async function articleGetAllService() {
//发送异步请求,获取所有文章
//同步等待服务器响应的结果,并返回,async await
return await instance.get('/article/getAll')
.then((result) => {
return result.data;
}).catch((err) => {
console.log(err);
})
}
//根据文章分类和发布状态搜索的函数
export async function articleSearchService(conditions) {
//发送异步请求,完成搜索
// axios.get('http://localhost:8081/article/search?category='+searchConditions.value.category+'&state='+searchConditions.value.state)
return await instance.get('/article/search', { params: conditions })
.then((result) => {
return result.data;
}).catch((err) => {
console.log(err);
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<script setup>
//导入article.js文件,引用src里的文件用@
import { articleGetAllService, articleSearchService } from "@/api/article.js";
import { ref } from "vue";
//定义响应式数据
const articleList = ref([]);
//获取全部文章数据
//同步获取articleGetAllService的返回结果 async,await
const getAllArticle=async function(){
let data = await articleGetAllService();
articleList.value = data;
}
getAllArticle()//调用
//定义响应式数据 searchConditions
const searchConditions = ref({
category: "",
state: "",
});
//搜索
const search = async function() {
let data = await articleSearchService({ ...searchConditions.value });
articleList.value = data;
};
</script>
- 结构优化二
- 创建src/util/request.js,创建拦截器,在请求或响应then或catch处理前拦截它们。
- 因为拦截器本身是异步的,article.js的方法就不需要再添加
async,await
了, .then和.catch也不需要了
只需要导入拦截器就可以,并把方法改xxx.get
就行。详见若如下
src/util/request.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//导入axios npm install axios
import axios from 'axios';
//定义一个变量,记录公开前缀 baseURL
const baseURL = 'http://localhost:8081';
const instance = axios.create({baseURL});
//添加响应拦截器
instance.interceptors.response.use(
result=>{
//http响应状态码为2xx会触发该函数
return result.data;
},
err=>{
//http响应状态码非2xx会触发该函数
alert('服务异常');
return Promise.reject(err);//异常的状态转化成失败的状态
}
)
export default instance;
src/api/article.js1
2
3
4
5
6
7
8
9
10
11
12// 导入拦截器
import request from '@/util/request.js'
//获取所有文章数据的函数
export function articleGetAllService() {
return request.get('/article/getAll')
}
//根据文章分类和发布状态搜索的函数
export function articleSearchService(conditions) {
return request.get('/article/search', { params: conditions })
}
ElementPlus
查看
element-plus官网-》https://element-plus.org/zh-CN/#zh-CN/
1.安装
- npm方式安装:
npm install element-plus --save
- 下载完成后,vue项目的
node_modules
目录下会有@element-plus
和element-plus
引入:在main.js中引入Element Plus(参照官方文档)
1
2
3
4
5
6
7
8
9
10
11// main.ts
import { createApp } from 'vue'//导入vue
import ElementPlus from 'element-plus'//导入element-plus
import 'element-plus/dist/index.css'//导入element-plus的样式
import App from './App.vue'//导入app.vue
import locale from 'element-plus/dist/local/zh-cn.js'//中文包,名字要求为locale
const app = createApp(App)//创建应用实例
app.use(ElementPlus,{locale})//使用element-plus,使用中文包
app.mount('#app')//控制html元素组件
- 访问官网的组件,调整成我们需要的样子即可
- 查看组件的API来调整属性
查看常用组件的Demo
1 | <script lang="ts" setup> |
src/main.js1
2
3
4
5
6
7
8
9
10import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import locale from 'element-plus/dist/locale/zh-cn.js'
const app = createApp(App)
app.use(ElementPlus,{locale})
app.mount('#app')
BigEvent前端开发
项目预览
登录界面:能对输入数据进行验证,点击注册右边会变为显示注册界面
文章管理界面:主体是展示当前用户文章列表
修改文章界面:抽屉风格,比添加文章多了数据回显
文章分类界面:主体是展示当前用户文章分类列表
修改分类界面:弹出表单,比添加分类多了数据回显
用户修改基本信息界面
用户修改头像界面
用户重置密码界面
环境准备
查看环境准备
- 创建Vue工程 :
npm init vue@latest
- 安装依赖
Element-Plus :
npm install element-plus --save
并参照官方文档导入1
2
3
4在main.js中做如下配置
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)Axios :
npm install axios
,并将前面vue案例练习结构优化
写的request.js
拷贝到utils文件夹下- Sass:
npm install sass -D
,并将main.js中引用的css改为scss
- 目录调整
- 删除src/components下面自动生成的内容
- 新建目录src/
api,utils,views
- 将静态资源图片拷贝到assets目录下(原有的不需要,下方提供了图片)
- 删除App.uve中自动生成的内容
assets下的静态资源图片
main.scss1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20body {
margin: 0;
background-color: #f5f5f5;
}
/* fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
transform: translateX(-30px);
opacity: 0;
}
.fade-slide-leave-to {
transform: translateX(30px);
opacity: 0;
}
建议自行转存图片到本地并按照如下命名
avatar.jpg
cover.jpg
default.png
login_bg.jpg
login_title.png
logo.png
logo2.png
注册
注册
初始登录注册页面
src/views/Login.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//控制注册与登录表单的显示, 默认显示登录
const isRegister = ref(false)
//绑定数据模型
</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="form" size="large" autocomplete="off" v-if="isRegister">
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="Lock" type="password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码"></el-input>
</el-form-item>
<!-- 注册按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space>
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
<!-- 登录表单 -->
<el-form ref="form" size="large" autocomplete="off" v-else>
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></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>登录</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true">
注册 →
</el-link>
</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/logo2.png') no-repeat 60% center / 240px auto,
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>
为注册部分绑定数据模型,并进行表单数据校验
- el-form标签是通过rules属性,绑定校验规则
- el-form-item标签上通过prop属性,指定校验项创建注册接口函数,绑定事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60<script>
//定义数据模型,注册 registerData
const registerData = ref({
username:'',
password:'',
rePassword:''
})
//校验密码的函数
const checkRePassword=(rule,value,callback)=>{
if(value===''){
callback(new Error('请再次确认密码'))
}else if(value !== registerData.value.password){
callback(new Error('两次输入的密码不一致'))
}else{
callback()
}
}
//定义表单效验规则
const registerRules = {
username:[
{required:true,message:'请输入用户名',trigger:'blur'},
{min:5,max:16,message:'长度为5~16位非空字符',trigger:'blur'},
],
password:[
{required:true,message:'请输入密码',trigger:'blur'},
{min: 5,max:16,message:'长度为5~16位非空字符',trigger:'blur'},
],
rePassword:[//自定义校验规则函数
{validator:checkRePassword,trigger:'blur'}
]
}
</script>
<!-- 注册表单 --><!--绑定注册数据 :model=>v-bind:model --><!--绑定校验规则 :rules="registerRules"-->
<el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :model="registerData" :rules="registerRules">
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item prop="username"><!--校验数据prop="username" -->
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
</el-form-item>
<el-form-item prop="password"><!--校验数据prop="password" -->
<el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
</el-form-item>
<el-form-item prop="rePassword"><!--校验数据prop="rePassword" -->
<el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>
</el-form-item>
<!-- 注册按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space>
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
src/api/user.js在Login.vue里面调用接口,为按钮绑定注册事件1
2
3
4
5
6
7
8
9
10
11
12//导入request.js请求工具
import request from '@/utils/request.js';
//提供调用注册接口的函数
export const userregisterService = (registerData)=>{
//借助UrlSearchParam完成传递
const params = new URLSearchParams()
for(let key in registerData){
params.append(key,registerData[key]);
}
return request.post('user/register',params);
}@click="register"
现在运行后端会出现跨域问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<script>
//调用后台接口完成注册
import {userregisterService} from '@/api/user.js'
const register = async()=>{
//registerData是一个响应式对象,如果要获取值,需要 .value
let result= await userregisterService(registerData.value);
if(result.code===0){
//注册成功
alert(result.message?result.message:'注册成功');
}else{
//注册失败
alert('注册失败')
}
}
</script>
<!--绑定注册事件-->
<el-button class="button" type="primary" auto-insert-space @click="register">注册</el-button>
跨域:由于浏览器的同源策略限制,向不同源(不同协议,不同域名,不同端口)发送ajax请求会失败
方法一: - 在request.js中将
baseURL
的值改为'/api'
;1
2
3//定义一个变量,记录公共的前缀 , baseURL
// const baseURL = 'http://localhost:8080';
const baseURL = '/api'; - 在
vite.config.js
的defineConfig方法中添加server1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
proxy:{
'/api':{//获取路径中包含了/api的请求
target:'http://localhost:8080',//后台服务器所在的源
changeOrigin:true,//修改源
rewrite:(path)=>path.replace(/^\/api/,'')//api替换为''
}
}
}
})
方法二: 在contrller类上面添加@CrossOrigin
注解来解决跨域
登录
登录
在vue.js中添加用户登录的后台接口函数
src/api/user.js1
2
3
4
5
6
7
8
9//提供调用登录接口的函数
export const userLoginService=(LoginData)=>{
//借助UrlSearchParam完成传递
const params = new URLSearchParams()
for(let key in LoginData){
params.append(key,LoginData[key]);
}
return request.post('user/login',params);
}
因为用户登录使用的还是username和password,可以复用registerData
所以数据绑定仍然为:model="registerData"
以及v-model="registerData.username"
和v-model="registerData.password"
校验规则也可以复用,在相应位置添加prop="username"
,还有为登录按钮添加登录事件@click="login"
src/views/Login.vue1
2
3
4
5
6
7
8
9
10//登录使用的username和password以及验证直接使用注册的
//调用后台接口完成登录
const login = async()=>{
let result = await userLoginService(registerData.value);
if(result.code === 0){
alert(result.message?result.message:'登陆成功');
}else{
alert(result.message?result.message:'登录失败');
}
}
同时,因为注册和登录绑定的是同一个数据模型,所以应该在转变界面时应该清空数据
添加clearRegisterData
函数,绑定在注册,返回
上1
2
3
4
5
6
7
8//清空数据registerData数据模型
const clearRegisterData=()=>{
registerData.value={
username:'',
password:'',
rePassword:''
}
}<el-link type="info" :underline="false" @click="isRegister = true;clearRegisterData()">注册 →</el-link>
<el-link type="info" :underline="false" @click="isRegister = true;clearRegisterData()">← 返回</el-link>
优化拦截器
优化拦截器
每次在login.vue的接口中,我们每个方法都需要判断code===0来输出提示信息
将这个过程直接提取到拦截器里判断完,到login.vue中就确保已经是成功的了
src/util/request.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//导入element通知
import { ElMessage } from 'element-plus'
//添加响应拦截器
instance.interceptors.response.use(
result=>{
// return result.data;
if(result.data.code===0){
return result.data;
}
//操作失败
// alert(result.data.message?result.data.message:'服务异常');
ElMessage.error(result.data.message?result.data.message:'服务异常');
//异步操作的状态转换为失败
return Promise.reject(result.data)
},
err=>{
alert('服务异常');
return Promise.reject(err);//异步的状态转化成失败的状态
}
)
在Login.vue中直接输出成功信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30//导入element通知
import { ElMessage } from 'element-plus'
//调用后台接口完成注册
import {userregisterService,userLoginService} from '@/api/user.js'
const register = async()=>{
//registerData是一个响应式对象,如果要获取值,需要 .value
let result= await userregisterService(registerData.value);
/* if(result.code === 0){
//注册成功
alert(result.message?result.message:'注册成功');
}else{
//注册失败
alert(result.message?result.message:'注册失败')
} */
ElMessage.success(result.message?result.message:'注册成功');
}
//登录使用的username和password以及验证直接使用注册的
//调用后台接口完成登录
const login = async()=>{
let result = await userLoginService(registerData.value);
/* if(result.code === 0){
alert(result.message?result.message:'登陆成功');
}else{
alert(result.message?result.message:'登录失败');
} */
ElMessage.success(result.message?result.message:'登陆成功');
}
主界面搭建
点击查看
创建src/Layout.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145<script setup>
import {
Management,
Promotion,
UserFilled,
User,
Crop,
EditPen,
SwitchButton,
CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside width="200px">
<div class="el-aside__logo"></div>
<!--elementplus的菜单标签-->
<el-menu active-text-color="#ffd04b" background-color="#232323" text-color="#fff"
router>
<el-menu-item >
<el-icon>
<Management />
</el-icon>
<span>文章分类</span>
</el-menu-item>
<el-menu-item >
<el-icon>
<Promotion />
</el-icon>
<span>文章管理</span>
</el-menu-item>
<el-sub-menu >
<template #title>
<el-icon>
<UserFilled />
</el-icon>
<span>个人中心</span>
</template>
<el-menu-item >
<el-icon>
<User />
</el-icon>
<span>基本资料</span>
</el-menu-item>
<el-menu-item >
<el-icon>
<Crop />
</el-icon>
<span>更换头像</span>
</el-menu-item>
<el-menu-item >
<el-icon>
<EditPen />
</el-icon>
<span>重置密码</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 右侧主区域 -->
<el-container>
<!-- 头部区域 -->
<el-header>
<div>用户:<strong>xnj</strong></div>
<!--element-plus的下拉菜单-->
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box">
<el-avatar :src="avatar" />
<el-icon>
<CaretBottom />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
<el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<!-- 中间区域 -->
<el-main>
<div style="width: 1290px; height: 570px;border: 1px solid red;">
内容展示区
</div>
</el-main>
<!-- 底部区域 -->
<el-footer>大事件 ©2024 Created by XNJ</el-footer>
</el-container>
</el-container>
</template>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
.el-aside {
background-color: #232323;
&__logo {
height: 120px;
background: url('@/assets/logo.png') no-repeat center / 120px auto;
}
.el-menu {
border-right: none;
}
}
.el-header {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown__box {
display: flex;
align-items: center;
.el-icon {
color: #999;
margin-left: 10px;
}
&:active,
&:focus {
outline: none;
}
}
}
.el-footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
}
}
</style>
Vue 路由
点击查看
- 安装vue-router
npm install vue-router@4
创建src/router/index.js,并创建路由器并导出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { createRouter,createWebHistory } from "vue-router";
//导入组件
import LoginVue from '@/views/Login.vue';
import LayoutVue from "@/views/Layout.vue";
//定义路由关系
const routes = [
{path: '/login',component: LoginVue},
{path: '/',component: LayoutVue}
]
//创建路由器
const router = createRouter({
history:createWebHistory(),
routes:routes
})
//导出路由
export default router在main.js中引入router,并使用
1
2
3
4import router from '@/router'
const app = createApp(App)
app.use(router)在vue应用实例中使用vue-router,声明router-view标签,展示组件内容
1
2
3
4
5
6
7
8
9<!-- App.vue -->
<script setup>
</script>
<template>
<!-- 声明router-view标签 -->
<router-view></router-view>
</template>
<style scoped>
</style>引入
useRouter
函数,在事件完成后跳转页面router.push('/')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// Login.vue
//引入路由中,useRouter函数
import {useRouter} from 'vue-router';
const router = useRouter()
//登录方法后完成跳转
const login = async()=>{
let result = await userLoginService(registerData.value);
/* if(result.code === 0){
alert(result.message?result.message:'登陆成功');
}else{
alert(result.message?result.message:'登录失败');
} */
ElMessage.success(result.message?result.message:'登陆成功');
//跳转到首页,路由完成跳转
router.push('/')
}
配置子路由
因为,在Layout.vue主界面中,当点击左侧菜单时,内容要切换为相应的页面
- 创建views/article/(ArticleCategory.vue|ArticleManage.vue),views/user/(UserInfo.vue|UserAvatar.vue|/UserResetPassword.vue)
- 在router/index.js中引入,创建为Layout.vue的子路由,并配置默认重定向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//导入组件
import ArticleCategoryVue from '@/views/article/ArticleCategory.vue';
import ArticleManage from '@/views/article/ArticleManage.vue';
import userUserAvatar from '@/views/user/UserAvatar.vue';
import UserInfo from '@/views/user/UserInfo.vue';
import UserResetPassword from '@/views/user/UserResetPassword.vue';
//定义路由关系
const routes = [
{path: '/login',component: LoginVue},
//配置子路由,并设置默认(重定向-》redirect:'/article/manage',)
{path: '/',component: LayoutVue,redirect:'/article/manage',children:[
{path: '/article/category',component: ArticleCategoryVue},
{path: '/article/manage',component: ArticleManage},
{path: '/user/avatar',component: userUserAvatar},
{path: '/user/info',component: UserInfo},
{path: '/user/restPassword',component: UserResetPassword},
]}
] - 在Layout.vue中的内容展示区域引入
<router-view></router-view>
1
2
3
4
5
6
7<!-- 中间区域 -->
<el-main>
<!-- <div style="width: 1290px; height: 570px;border: 1px solid red;">
内容展示区
</div> -->
<router-view></router-view>
</el-main> - 在菜单标签
<el-menu-item >
里添加路由index="/article/category"
获取文章分类
点击查看
创建src/api/article.js,引入request.js,实现获取文章分类接口函数1
2
3
4
5import request from '@/utils/request.js'
export const articleCategoryListService = ()=>{
return request.get('/category');
}
在ArticleCategory.vue中调用接口函数1
2
3
4
5
6
7
8
9/导入
import {articleCategoryListService} from '@/api/article.js'
//声明异步函数获取所有分类
const articleCategoryList= async ()=>{
let result = await articleCategoryListService();
categorys.value = result.data;
}
//页面载入即调用
articleCategoryList();
现在页面会报401,服务异常,马上实现token
使用Pinia获取token
点击查看
- 安装pinia
npm install pinia
在vue应用实例中(main.js)使用pinia
1
2
3import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)在src/stores/token.js中定义store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import { defineStore } from "pinia";
import { ref } from "vue";
/**
* defineStore参数描述:
* 第一个参数:给状态起名,具有唯一性
* 第二个参数:函数,可以把定义该状态中拥有的内容
* defineStore返回值描述:
* 返回的是一个函数,将来可以调用该函数,得到第二个参数中返回的内容
*/
export const useTokenStore = defineStore('token',()=>{
//1.定义描述token
const token = ref('')
//2.定义修改token的方法
const setToken = (newToken)=>{
token.value= newToken
}
//3.定义移除token的方法
const removeToken = ()=>{
token.value=''
}
return{
token,setToken,removeToken
}
})在组件中使用store
在Login.vue中登录成功时应该设置token1
2
3
4
5
6
7
8
9
10
11
12import {useTokenStore} from '@/stores/token.js' //引入token
const tokenStore = useTokenStore();
//登录使用的username和password以及验证直接使用注册的
//调用后台接口完成登录
const login = async()=>{
let result = await userLoginService(registerData.value);
ElMessage.success(result.message?result.message:'登陆成功');
//把得到的token存储到pinia中
tokenStore.setToken(result.data);
//跳转到首页,路由完成跳转
router.push('/')
}获取文章分类列表的时候需要验证token,在article.js中添加如下
1
2
3
4
5
6
7
8import request from '@/utils/request.js'
import { useTokenStore } from '@/stores/token.js';
//文章分类查询
export const articleCategoryListService = ()=>{
const tokenStore = useTokenStore();
//在pinia中定义的响应式数据,都不需要.value
return request.get('/category',{headers:{'Authorization':tokenStore.token}});
}
axios拦截器,拦截请求
在拦截器里对token拦截,就不需要每个方法都传递一遍token了
在src/util/request.js中添加如下请求拦截器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { useTokenStore } from '@/stores/token';
//添加请求拦截器
instance.interceptors.request.use(
(config)=>{
//请求前的回调
//添加token
const tokenStore = useTokenStore();
//判断有没有token
if(tokenStore.token){
config.headers.Authorization = tokenStore.token
}
return config;
},
(err)=>{
//请求错误的回调
Promise.reject(err)
}
)
article.js里方法token可以不用再传递了1
2
3
4
5
6
7
8// import { useTokenStore } from '@/stores/token.js';
//文章分类查询
export const articleCategoryListService = ()=>{
//const tokenStore = useTokenStore();
//在pinia中定义的响应式数据,都不需要.value
// return request.get('/category',{headers:{'Authorization':tokenStore.token}});
return request.get('/category');
}
Pinia持久化插件-persist
- 上述拦截器完成后,刷新浏览器会出错
- Pinia默认是内存存储,当刷新浏览器时会丢失数据
- Persist插件可以将pinia中的数据持久化的存储
- 安装persist
npm install pinia-persistedstate-plugin
在pinia中使用persist
1
2
3
4import { createPersistedState } from 'pinia-persistedstate-plugin'
const pinia = createPinia();
app.use(pinia)定义状态Store时指定持久化配置参数
src/stores/token.js1
2
3
4
5
6return{
token,setToken,removeToken
}
},{
persist:true//持久化存储
});
未登录统一处理
继续修复bug,上述用户直接访问路径就算未登录也能成功,只是报服务异常(status=401)
在request.js
的响应拦截器中,异常时,如果捕获到401,就应该提示重新登录,并跳转到登陆界面
注:由于浏览器有缓存,所以换一个浏览器即可感受到上述问题的修复1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import router from '@/router'
//添加响应拦截器
instance.interceptors.response.use(
result=>{
// return result.data;
if(result.data.code===0){
return result.data;
}
//操作失败
// alert(result.data.message?result.data.message:'服务异常');
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);//异步的状态转化成失败的状态
}
)
添加文章分类
点击查看
在
ArticleCategory.vue
中添加分类弹窗页面,绑定数据模型和校验规则1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!-- 添加分类弹窗 -->
<el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
<el-form :model="categoryModel" :rules="rules" label-width="100px" style="padding-right: 30px">
<el-form-item label="分类名称" prop="categoryName">
<el-input v-model="categoryModel.categoryName" minlength="1" maxlength="10"></el-input>
</el-form-item>
<el-form-item label="分类别名" prop="categoryAlias">
<el-input v-model="categoryModel.categoryAlias" minlength="1" maxlength="15"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary"> 确认 </el-button>
</span>
</template>
</el-dialog>数据模型和校验规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//控制添加分类弹窗
const dialogVisible = ref(false)
//添加分类数据模型
const categoryModel = ref({
categoryName: '',
categoryAlias: ''
})
//添加分类表单校验
const rules = {
categoryName: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
],
categoryAlias: [
{ required: true, message: '请输入分类别名', trigger: 'blur' },
]
}添加分类按钮单击事件
1
<el-button type="primary" @click="dialogVisible = true">添加分类</el-button>
在
article.js
中提供添加分类接口函数1
2
3
4//添加文章分类
export const articleAddCategoryService =(categoryData)=>{
return request.post('category',categoryData)
}为添加分类单击事件调用接口添加分类,刷新界面
1
2
3
4
5
6
7
8
9
10
11
12
13
14//导入
import { articleAddCategoryService } from "@/api/article.js";
//导入element通知
import { ElMessage } from 'element-plus'
//调用接口,添加分类表单
const addCategory =async ()=>{
let result = await articleAddCategoryService(categoryModel.value);
ElMessage.success(result.message?result.message:'添加成功');
//刷新分类列表,关闭弹窗
articleCategoryList();
dialogVisible.value=false;
}
修改分类
点击查看
修改分类和添加分类的弹窗所需要的数据模型是一样的,弹窗样式也只有弹窗标题不一样
- 为弹窗标题绑定数据模型,
<el-dialog v-model="dialogVisible" :title="title" width="30%">
- 在点击添加或修改按钮时给它不同的值
title='添加分类'
,@click="showDialog(row)"
- 添加分类按钮:
<el-button type="primary" @click="showAddDialog">添加分类</el-button>
- 修改分类按钮
<el-button :icon="Edit" circle plain type="primary" @click="showEditDialog(row)"></el-button>
- 当点击修改按钮时,还需要通过row来将数据回显到弹窗表单中
- 注意:当在标签上时,响应式数据不需要.value来修改,而在其他地方需要.value!!!
1 | //弹窗标题数据模型 |
- 在弹窗的确定按钮点击事件中,title的值来判断执行修改还是添加
<el-button type="primary" @click="title==='添加分类'?addCategory():updateCategory()"> 确认 </el-button>
在article.js中提供修改分类接口1
2
3
4//修改文章分类
export const articleUpdateCategoryService =(categoryData)=>{
return request.put('/category',categoryData)
}
在ArticleCategory.vue中调用修改接口函数1
2
3
4
5
6
7
8
9
10//导入
import {articleUpdateCategoryService } from "@/api/article.js";
//调用接口,修改分类
const updateCategory =async ()=>{
let result = await articleUpdateCategoryService(categoryModel.value);
ElMessage.success(result.message?result.message:'修改成功');
//刷新分类列表,隐藏弹窗
articleCategoryList();
dialogVisible.value=false;
}
删除分类
点击查看
在article.js中创建删除分类接口1
2
3
4//删除文章分类
export const artilceDeleteCategoryService=(id)=>{
return request.delete('/category/'+id)
}
为删除按钮绑定点击事件,<el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button>
弹出提示,调用删除接口,通过row传递分类id1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//导入
import { artilceDeleteCategoryService } from "@/api/article.js";
//导入确认框
import { ElMessageBox } from 'element-plus'
//删除分类提示框
const deleteCategory= (row)=>{
ElMessageBox.confirm(
'操作不可逆,确定要删除这个分类吗?',
'温馨提示:',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {//异步操作 async
//调用接口,执行删除文章操作
let result=await artilceDeleteCategoryService(row.id);
ElMessage({
type: 'success',
message: result.message?result.message:'删除成功',
})
//刷新分类列表
articleCategoryList();
})
.catch(() => {
ElMessage({
type: 'info',
message: '用户取消删除操作',
})
})
}
文章管理页面
点击查看
初始页面复制到ArticleManage.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121<script setup>
import {
Edit,
Delete
} from '@element-plus/icons-vue'
import { ref } from 'vue'
//文章分类数据模型
const categorys = ref([
{
"id": 3,
"categoryName": "美食",
"categoryAlias": "my",
"createTime": "2023-09-02 12:06:59",
"updateTime": "2023-09-02 12:06:59"
}
])
//用户搜索时选中的分类id
const categoryId=ref('')
//用户搜索时选中的发布状态
const state=ref('')
//文章列表数据模型
const articles = ref([
{
"id": 5,
"title": "陕西旅游攻略",
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "草稿",
"categoryId": 2,
"createTime": "2023-09-03 11:55:30",
"updateTime": "2023-09-03 11:55:30"
}
])
//分页条数据模型
const pageNum = ref(1)//当前页
const total = ref(20)//总条数
const pageSize = ref(3)//每页条数
//当每页条数发生了变化,调用此函数
const onSizeChange = (size) => {
pageSize.value = size
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
pageNum.value = num
}
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>文章管理</span>
<div class="extra">
<el-button type="primary">添加文章</el-button>
</div>
</div>
</template>
<!-- 搜索表单 -->
<el-form inline>
<el-form-item label="文章分类:">
<el-select placeholder="请选择" v-model="categoryId" style="width:240px;">
<el-option
v-for="c in categorys"
:key="c.id"
:label="c.categoryName"
:value="c.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="发布状态:">
<el-select placeholder="请选择" v-model="state" style="width:240px;">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary">搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
<!-- 文章列表 -->
<el-table :data="articles" style="width: 100%">
<el-table-column label="文章标题" width="400" prop="title"></el-table-column>
<el-table-column label="分类" prop="categoryId"></el-table-column>
<el-table-column label="发表时间" prop="createTime"> </el-table-column>
<el-table-column label="状态" prop="state"></el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button :icon="Edit" circle plain type="primary"></el-button>
<el-button :icon="Delete" circle plain type="danger"></el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="没有数据" />
</template>
</el-table>
<!-- 分页条 -->
<el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5 ,10, 15]"
layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange"
@current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
</el-card>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100%;
box-sizing: border-box;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>
使用中文语言包,解决分页条中文问题, 在main.js中完成1
2import locale from 'element-plus/dist/locale/zh-cn.js'
app.use(ElementPlus,{locale})ArticleManage.vue
搜索栏分类下拉列表:文章分类数据回显1
2
3
4
5
6
7
8
9//导入
import {articleCategoryListService} from '@/api/article.js'
//分类数据回显
const getArticleCategoryList = async ()=>{
let result =await articleCategoryListService();
categorys.value=result.data;
}
//页面挂载完成即调用
getArticleCategoryList();
获取文章列表
点击查看
在article.js
提供文章分页查询接口1
2
3
4//获取所有文章列表
export const articleGetAllService=(params)=>{
return request.get('/article',{params:params})
}
文章分页查询的参数包括,页码,页面大小,分类(可选),状态(可选)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26//导入
import {articleGetAllService} from '@/api/article.js'
const articleList =async ()=>{
let params = {
pageNum:pageNum.value,
pageSize:pageSize.value,
categoryId:categoryId.value?categoryId.value:null,
state:state.value?state.value:null
};
let result =await articleGetAllService(params);
//渲染视图,当前所有文章数据,文章总数
articles.value = result.data.items;
total.value=result.data.total;
//处理数据,给数据模型扩展一个属性categoryName,分类名称
for(let i=0;i<articles.value.length;i++){
let article = articles.value[i];
for(let j=0;j<categorys.value.length;j++){
if(article.categoryId == categorys.value[j].id){
article.categoryName=categorys.value[j].categoryName;
}
}
}
};
articleList();//该方法必须在获取分类列表之后运行
为表单分类这一列绑定categoryName<el-table-column label="分类" prop="categoryName" ></el-table-column>
搜索按钮事件调用articleList()
即可-》<el-button type="primary" @click="articleList()">搜索</el-button>
重置按钮事件清空数据即可<el-button @click="state='',categoryId=''">重置</el-button>
页脚的页码和页面大小同理1
2
3
4
5
6
7
8
9
10//当每页条数发生了变化,调用此函数
const onSizeChange = (size) => {
pageSize.value = size
articleList();
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
pageNum.value = num
articleList();
}
添加文章
点击查看
添加文章抽屉组件1
2
3
4
5
6
7
8
9
10
11import {Plus} from '@element-plus/icons-vue'
//控制抽屉是否显示
const visibleDrawer = ref(false)
//添加表单数据模型
const articleModel = ref({
title: '',
categoryId: '',
coverImg: '',
content:'',
state:''
})
将抽屉放在card卡片内1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31<!-- 抽屉 -->
<el-drawer v-model="visibleDrawer" title="添加文章" direction="rtl" size="50%">
<!-- 添加文章表单 -->
<el-form :model="articleModel" label-width="100px" >
<el-form-item label="文章标题" >
<el-input v-model="articleModel.title" placeholder="请输入标题"></el-input>
</el-form-item>
<el-form-item label="文章分类">
<el-select placeholder="请选择" v-model="articleModel.categoryId">
<el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="文章封面">
<el-upload class="avatar-uploader" :auto-upload="false" :show-file-list="false">
<img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="文章内容">
<div class="editor">富文本编辑器</div>
</el-form-item>
<el-form-item>
<el-button type="primary">发布</el-button>
<el-button type="info">草稿</el-button>
</el-form-item>
</el-form>
</el-drawer>
添加抽屉的css样式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/* 抽屉样式 */
.avatar-uploader {
:deep() {
.avatar {
width: 178px;
height: 178px;
display: block;
}
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color:
width: 178px;
height: 178px;
text-align: center;
}
}
}
为添加文章按钮添加单击事件,展示抽屉<el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>
富文本编辑器
输入文章内容模块使用富文本编辑器,这里咱们使用一个开源的富文本编辑器 Quill
- 安装
npm install @vueup/vue-quill@latest --save
在ArticleManage.vue里导入导入组件和样式:
1
2import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'在’富文本编辑器’位置使用quill组件
1
2
3
4
5
6
7<!-- 富文本编辑器 -->
<quill-editor
theme="snow"
v-model:content="articleModel.content"
contentType="html"
>
</quill-editor>富文本编辑器的css样式美化
1
2
3
4
5
6.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
图片上传
1 | <el-form-item label="文章封面"> |
1 | import {useTokenStore} from '@/stores/token.js' |
功能实现
在article.js中提供接口函数1
2
3
4//添加文章
export const addArticleService=(articleData)=>{
return request.post('/article',articleData)
}
为发布和草稿按钮添加点击事件<el-button type="primary" @click="addArticle('已发布')">发布</el-button>
<el-button type="info" @click="addArticle('草稿')">草稿</el-button>
调用接口函数,实现添加文章功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//导入接口函数
import {addArticleService} from "@/api/article.js";
//导入通知
import {ElMessage} from 'element-plus'
//添加文章
const addArticle =async (articleState)=>{
articleModel.value.state=articleState;
let result = await addArticleService(articleModel.value);
//提示信息
ElMessage.success(result.message?result.message:'添加成功');
//关闭抽屉
visibleDrawer.value=false;
//刷新文章列表
articleList();
}
文章修改,删除
点击查看
文章修改
article.js提供接口函数1
2
3
4//修改文章
export const updateArticleService=(articleData)=>{
return request.put('/article',articleData)
}1
2//导入
import {updateArticleService} from "@/api/article.js";
添加文章和修改文章可以复用同一个抽屉和数据模型
给抽屉的title绑定响应式数据,当点击添加和点击修改时给予不同的标题
同一个数据模型,点击修改按钮时要进行数据回显,点击添加时要清空数据模型的值
这也也可以在点击发布或草稿按钮时对标题判断来执行添加还是修改操作
给各按钮绑定事件
添加文章:<el-button type="primary" @click="showVisibleDrawer">添加文章</el-button>
修改文章:<el-button :icon="Edit" circle plain type="primary" @click="showupdateArticle(row)"></el-button>
抽屉标题::title="title"
发布按钮:<el-button type="primary" @click="title==='添加文章'?addArticle('已发布'):updateArticle('已发布')">发布</el-button>
草稿按钮:<el-button type="info" @click="title==='添加文章'?addArticle('草稿'):updateArticle('草稿')">草稿</el-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53//回响式数据,抽屉标题
const title = ref('');
//添加文章按钮事件--清空文章表单数据
const showVisibleDrawer=()=>{
visibleDrawer.value = true;//显示抽屉
title.value='添加文章';//设置抽屉标题
//清空文章表单数据
articleModel.value.title='';
articleModel.value.categoryId='';
articleModel.value.coverImg='';
articleModel.value.content="<p></p>";
articleModel.value.state='';
}
//导入通知
import {ElMessage} from 'element-plus'
//添加文章
const addArticle =async (articleState)=>{
articleModel.value.state=articleState;
let result = await addArticleService(articleModel.value);
//提示信息
ElMessage.success(result.message?result.message:'添加成功');
//关闭抽屉
visibleDrawer.value=false;
//刷新文章列表
articleList();
}
//修改文章数据回显
const showupdateArticle = (row)=>{
title.value='修改文章'//抽屉标题
//显示抽屉
visibleDrawer.value=true;
//回显数据
articleModel.value.title=row.title;
articleModel.value.categoryId=row.categoryId;
articleModel.value.coverImg=row.coverImg;
articleModel.value.content=row.content;
articleModel.value.state=row.state;
articleModel.value.id=row.id;//别忘了传文章id
}
//调用接口 修改文章
const updateArticle = async(articleState)=>{
articleModel.value.state=articleState;
let result = await updateArticleService(articleModel.value);
//提示信息
ElMessage.success(result.message?result.message:'修改成功');
//关闭抽屉
visibleDrawer.value=false;
//刷新文章列表
articleList();
}
删除文章
article.js提供接口1
2
3
4//删除文章
export const deleteArticleService=(id)=>{
return request.delete('/article/'+id)
}
ArticleManage.vue1
2//导入
import {deleteArticleService} from "@/api/article.js";
为按钮绑定事件<el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></el-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31//导入确认框
import { ElMessageBox } from 'element-plus'
//调用接口 删除文章
const deleteArticle =async (row)=>{
ElMessageBox.confirm(
'操作不可逆,确定要删除这篇文章吗?',
'温馨提示:',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
//调用接口,执行删除文章操作
let result =await deleteArticleService(row.id)
ElMessage({
type: 'success',
message: result.message?result.message:'删除成功',
})
//刷新分类列表
articleList();
})
.catch(() => {
ElMessage({
type: 'info',
message: '用户取消删除操作',
})
})
}
顶部导航栏昵称和用户头像
点击查看
定义stores/userinfo.js来存储用户信息,和token.js
原理一致1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import {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;
在api/user.js中,定义获取用户详细信息的接口1
2
3
4//获取用户详细信息
export const userInfoService = ()=>{
return request.get('/user/userinfo');
}
在Layout.vue中,调用接口1
2
3
4
5
6
7
8
9
10
11
12
13import avatar from '@/assets/default.png'//静态资源中默认头像图片
import { userInfoService } from '@/api/user.js';
import useUserInfoStore from '@/stores/userinfo.js';
const userInfoStore = useUserInfoStore();
//调用接口,获取用户详细信息
const getUserInfo =async ()=>{
//调用接口
let result =await userInfoService();
//将用户详细数据存储到pinia中
userInfoStore.setInfo(result.data);
}
getUserInfo();//挂载即调用
为用户昵称和头像绑定数据1
2
3
4
5
6
7
8
9
10
11 <!-- 头部区域 -->
<el-header>
<div>用户:<strong>{{ userInfoStore.info.nickname }}</strong></div>
<!--element-plus的下拉菜单-->
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box"><!--判断用户头像是否设置了头像-->
<el-avatar :src="userInfoStore.info.userPic?userInfoStore.info.userPic:avatar" />
<el-icon>
<CaretBottom />
</el-icon>
</span>
用户头像下拉菜单功能
点击查看
为下拉菜单绑定command,路径的值是src/router/index.js中设置的子路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!--element-plus的下拉菜单-->
<!--command:条目被点击之后触发,在事件函数上可以声明一个函数,接收条目对应的指令-->
<el-dropdown placement="bottom-end" @command="handlerCommand">
<span class="el-dropdown__box">
<el-avatar :src="userInfoStore.info.userPic?userInfoStore.info.userPic:avatar" />
<el-icon>
<CaretBottom />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item>
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
<el-dropdown-item command="restPassword" :icon="EditPen">重置密码</el-dropdown-item>
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
完成功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40//导入
import useUserInfoStore from '@/stores/userinfo.js';
const userInfoStore = useUserInfoStore();//前两个前面已经导入过,这里只是提醒一下
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
import { useRouter } from 'vue-router';
const router = useRouter();
import { ElMessage,ElMessageBox } from 'element-plus';
//条目被点击之后调用的函数
const handlerCommand = (command)=>{
if(command==='logout'){//退出登录
ElMessageBox.confirm(
'确定要退出吗?',
'温馨提示:',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
) .then( () => {
//删除pinia中存储的token,和suerInfo
userInfoStore.removeInfo();
tokenStore.removeToken();
//跳转到登录页面
router.push('/login');
ElMessage({
type: 'success',
message: '退出成功',
})
}).catch(() => {
ElMessage({
type: 'info',
message: '您取消了退出',
})
})
}else{//路由切换页面
router.push('/user/'+command)
}
}
修改用户基本信息
点击查看
用户基本资料界面src/views/user/UserInfo.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50<script setup>
import { ref } from 'vue'
const userInfo = ref({
id: 0,
username: 'zhangsan',
nickname: 'zs',
email: 'zs@163.com',
})
const rules = {
nickname: [
{ required: true, message: '请输入用户昵称', trigger: 'blur' },
{
pattern: /^\S{2,10}$/,
message: '昵称必须是2-10位的非空字符串',
trigger: 'blur'
}
],
email: [
{ required: true, message: '请输入用户邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
]
}
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>基本资料</span>
</div>
</template>
<el-row>
<el-col :span="12">
<el-form :model="userInfo" :rules="rules" label-width="100px" size="large">
<el-form-item label="登录名称">
<el-input v-model="userInfo.username" disabled></el-input>
</el-form-item>
<el-form-item label="用户昵称" prop="nickname">
<el-input v-model="userInfo.nickname"></el-input>
</el-form-item>
<el-form-item label="用户邮箱" prop="email">
<el-input v-model="userInfo.email"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">提交修改</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</el-card>
</template>
回显信息:用户基本信息已经存到pinia里面了。这里只需要取出来赋值给表单数据模型即可1
2
3
4
5//导入
import useUserInfoStore from '@/stores/userinfo'
const UserInfo = useUserInfoStore();
//将用户信息赋值给表单数据模型
const userInfo = ref({...UserInfo.info})
user.js提供修改函数接口1
2
3
4//修改用户信息
export const updateUserInfoService=(userInfoData)=>{
return request.put('/user/update',userInfoData)
}
调用接口,完成修改,同时修改pinia中存储的用户信息1
2
3
4
5
6
7
8
9
10
11
12
13//修改用户信息
import useUserInfoStore from '@/stores/userinfo'
const UserInfo = useUserInfoStore();
//将用户信息赋值给表单数据模型
const userInfo = ref({...UserInfo.info})
import { ElMessage } from 'element-plus';
import {updateUserInfoService} from '@/api/user.js'
const updateUserInfo =async ()=>{
let result =await updateUserInfoService(userInfo.value);
ElMessage.success(result.message?result.message:'修改成功');
//修改pinia中存储的用户信息
UserInfo.setInfo(userInfo.value);
}
用户头像修改
点击查看
用户头像回显,图片上传,头像修改
UserAvatar.vue界面的用户头像组件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72<script setup>
import { Plus, Upload } from '@element-plus/icons-vue'
import {ref} from 'vue'
import avatar from '@/assets/default.png'
const uploadRef = ref()
//用户头像地址
const imgUrl= avatar
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>更换头像</span>
</div>
</template>
<el-row>
<el-col :span="12">
<el-upload
ref="uploadRef"
class="avatar-uploader"
:show-file-list="false"
>
<img v-if="imgUrl" :src="imgUrl" class="avatar" />
<img v-else :src="avatar" width="278" />
</el-upload>
<br />
<el-button type="primary" :icon="Plus" size="large" @click="uploadRef.$el.querySelector('input').click()">
选择图片
</el-button>
<el-button type="success" :icon="Upload" size="large">
上传头像
</el-button>
</el-col>
</el-row>
</el-card>
</template>
<style lang="scss" scoped>
.avatar-uploader {
:deep() {
.avatar {
width: 278px;
height: 278px;
display: block;
}
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 278px;
height: 278px;
text-align: center;
}
}
}
</style>
头像回显:从pinia中取出用户信息,头像地址赋值给imgUrl
,组件已经通过v-if判断好了1
2
3
4
5
6//导入pinia中用户信息
import useUserInfoStore from '@/stores/userinfo.js'
const userInfoStore = useUserInfoStore();
//用户头像地址--实现用户头像回显
const imgUrl= ref(userInfoStore.info.userPic)
上传头像1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!--
auto-upload:设置是否自动上传
action:设置服务器接口路径
name: 设置上传的请求头
headers: 请求头
on-success: 设置上传成功的回调函数
-->
<el-upload
ref="uploadRef"
class="avatar-uploader"
:show-file-list="false"
:auto-upload="true"
action="/api/upload"
name="file"
:headers="{'Authorization':tokenStore.token}"
:on-success="uploadSuccess"
>
<img v-if="imgUrl" :src="imgUrl" class="avatar" />
<img v-else :src="avatar" width="278" />
</el-upload>1
2
3
4
5
6
7//导入token
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
//图片上传成功的回调函数
const uploadSuccess = (result)=>{
imgUrl.value= result.data;
}
头像修改
user.js中提供修改头像接口函数1
2
3
4
5
6
7
8
9//修改头像
export const updateAvatarService=(avatarURL)=>{
//参数是Query类型的,可以直接通过拼接在路径后面传递
// return request.patch('/user/updateAvatar?avatarurl='+avatarURL)
//也可以通过UrlSerachParams来完成
const params=new URLSearchParams();
params.append('avatarurl',avatarURL);
return request.patch('/user/updateAvatar',params)
}
在UserAvatar.vue中调用接口,完成修改,同时修改pinia中的头像信息1
2
3
4
5
6
7
8
9
10//修改头像
import { ElMessage } from 'element-plus';
import {updateAvatarService} from '@/api/user.js'
const updateAvater =async ()=>{
//调用接口
let result =await updateAvatarService(imgUrl.value)
ElMessage.success(result.message?result.message:'上传成功')
//修改pinia中用户的头像信息
userInfoStore.info.userPic = imgUrl.value
}
为上传按钮绑定事件<el-button type="success" :icon="Upload" size="large" @click="updateAvater">上传头像</el-button>
重置用户密码
点击查看
use.js提供重置密码函数接口1
2
3
4//修改密码
export const updatePasswordService=(Password)=>{
return request.patch('/user/updatePwd',Password)
}
重置密码UserRestPassword.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90<script setup>
import { ref } from 'vue'
//导入
const PassWord = ref({
old_pwd:'',
new_pwd:'',
re_pwd:''
})
const rules = {
old_pwd: [
{ required: true, message: '请输入旧密码', trigger: 'blur' },
{
pattern: /^\S{6,20}$/,
message: '密码必须是6-10位的非空字符串',
trigger: 'blur'
}
],
new_pwd: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{
pattern: /^\S{6,20}$/,
message: '密码必须是6-10位的非空字符串',
trigger: 'blur'
}
],
re_pwd:[
{ required: true, message: '请确定新密码', trigger: 'blur' },
{//第二个规则是判断是否和新密码一致
validator: (rule, value, callback) => {
if (value !== PassWord.value.new_pwd) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}
]
}
//修改用户密码
import useUserInfoStore from '@/stores/userinfo.js'
const userInfoStore = useUserInfoStore();
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
import { useRouter } from 'vue-router';
const router = useRouter();
import {updatePasswordService} from '@/api/user.js'
import { ElMessage } from 'element-plus';
const updatePassword = async ()=>{
let result =await updatePasswordService(PassWord.value);
ElMessage.success(result.message?result.message:'修改成功')
//删除pinia中存储的token,和suerInfo
userInfoStore.removeInfo();
tokenStore.removeToken();
//重新登录
router.push('/login')
}
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>重置密码</span>
</div>
</template>
<el-row>
<el-col :span="12">
<el-form :model="PassWord" :rules="rules" label-width="100px" size="large">
<el-form-item label="旧密码" prop="old_pwd">
<el-input v-model="PassWord.old_pwd" type="password"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="new_pwd">
<el-input v-model="PassWord.new_pwd" type="password"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="re_pwd">
<el-input v-model="PassWord.re_pwd" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="updatePassword">提交修改</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</el-card>
</template>