黑马 springboot3+vue3(大事件)笔记分享

文章目录

一、基础篇

1、springboot概述

Spring Boot是Spring提供的一个子项目,用于快速构建Spring应用程序。

其中由SpringFramework提供核心功能,是其他 Spring 全家桶(SpringMVC、SpringBoot、SpringCloud(用于服务治理)、SpringData(用于数据获取)、SpringSecurity(用于认证授权)SpringAMQP(用于消息传递)等)的基础和核心。其余子项目由SpringFramework整合起来构成一个应用程序

传统方式构建spring应用程序的缺点

在这里插入图片描述
使用传统构建方式的时候这些依赖需要一个一个手动导入,它们之间关联的jar包还有可能会发生jar包冲突问题,问题还需要我们手动解决。除依赖外,还需要写很多配置文件。其中applicationContext.xml文件是Spring应用程序的核心配置文件,需要在配置文件里面配置大量的bean对象,因为使用Spring这些beam对象时需要先去声明再使用。

SpringBoot特性

起步依赖、自动配置主要用于简化Spring应用程序的构建。

起步依赖
本质上就是一个Maven坐标,整合了完成一个功能需要的所有坐标。
以下举个web引入依赖的例子。
在这里插入图片描述
之前需要手动引入的依赖
在这里插入图片描述

springBoot提供的起步依赖,在这个坐标里面就把所有web开发需要的坐标都整合起来到spring-boot-starter-web中。引入以上坐标就不需要再引入其他。

解决了配置繁琐的问题。

自动配置
遵循约定大约配置的原则,在boot程序启动后,一些bean对象会自动注入到ioc容器,不需要手动声明,简化开发

如Spring整合Mybatis
在这里插入图片描述

之前除了引入Mybatis的依赖还需要声明两个bean对象SqlSessionFactoryBean和MapperScannerConfigurer。这样Spring整合Mybatis才算完成。
在这里插入图片描述

使用springBoot去构建spring应用程序需要引入Mybatis的起步依赖(mybatis-spring-boot-starter)他的内部就自动的声明好了两个bean对象SqlSessionFactoryBean和MapperScannerConfigurer。

其他特性
内嵌的Tomcat、Jetty(无需部署WAR文件) 外部化配置 不需要XML配置(properties/yml)等

2、SpringBoot入门程序

需求:使用 SpringBoot 开发一个web应用,浏览器发起请求 /hello后,给浏览器返回字符串 “hello world ~"。
在这里插入图片描述

①. 创建Maven工程

在这里插入图片描述
jdk需要使用17及以上版本
在这里插入图片描述

②. 导入spring-boot-stater-web起步依赖(在pom.xml中)

在这里插入图片描述
其他内容
在这里插入图片描述

③. 编写Controller

在这里插入图片描述

④. 提供启动类(SpringbootQuickstartApplication)

在这里插入图片描述
解释:@SpringBootApplication注解表示当前一个java类是boot程序的入口,是启动类。

SpringApplication.run(SpringbootQuickstartApplication.class, args);为main方法里面的固定的代码内部传入两个固定参数一个是当前类的字节码文件(SpringbootQuickstartApplication.class),另一个是main方法的数组参数(args)。

当运行main方法时springBoot工程就会启动,同时内置的tomcat也会启动。同时会把当前的control等资源部署好,浏览器就能访问了。
在这里插入图片描述
运行后再浏览器输入 localhost:8080/hello
在这里插入图片描述

3、手动创建SpringBoot工程

1、

在这里插入图片描述
点击:+号 再点击:new module

在这里插入图片描述
再接着点击Create,apply,OK工程就开始创建了

2、

打开pom.xml文件添加父工程version与自己需求修改。

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

如果需要完成web工程接着引入web依赖,其余依赖根据自己需求。接着点击(M)图标刷新一下。

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

3、

在这里插入图片描述
在这里插入图片描述
可修改为XXXApplication 如SpringBootCreateManualApplication。

接着再修改代码
在这里插入图片描述
接着在main包下生成一个包rescources,再在这个文件下生成一个文件(New File)application.properties
在这里插入图片描述
工程就创建完成。

4、SpringBoot配置文件

properties配置文件的使用

具体使用可参考以下链接 http://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties
在这里插入图片描述
可通过修改properties的内容修改Tomcat端口号或者是虚拟目录
在这里插入图片描述

server.port=9090
server.servlet.context-path=/start

在浏览器中的访问也要随之更改
在这里插入图片描述

yml配置文件的使用

在这里插入图片描述
注意缩进不可更改
在这里插入图片描述通常更多使用yml文件。

yml配置信息书写与获取

举发送邮件的例子,以下是不配置yml文件时完成发送的内容

MailUtil类

在这里插入图片描述EmailService接口

在这里插入图片描述实现类EmailServiceImpl在这里插入图片描述
重写了send方法,在方法内部调用了工具类的sendMail方法。

由依赖注入的emailProperties,它对应的类EmailPerperties提供了四个成员变量
在这里插入图片描述
在EmailController中调用了emailProperties.send方法发送邮件
在这里插入图片描述
配置yml文件

值前边必须有空格,作为分隔符 使用空格作为缩进表示层级关系,相同的层级左侧对齐
在这里插入图片描述
配置好了后EmailProperties文件中相关的信息就不需要了

可通过@Value(“${键名}”)注解获取配置文件中的信息
在这里插入图片描述
可通过@ConfigurationProperties(prefix=“前缀”)注解获取配置文件中的信息,同时实体类的成员变量名与配置文件中的键名保持一致

5、整合Mybatis

注入sql文件代码并运行

create database if not exists mybatis;

use mybatis;

create table user(
    id int unsigned primary key auto_increment comment 'ID',
    name varchar(100) comment '姓名',
    age tinyint unsigned comment '年龄',
    gender tinyint unsigned comment '性别, 1:男, 2:女',
    phone varchar(11) comment '手机号'
) comment '用户表';

insert into user(id, name, age, gender, phone) VALUES (null,'白眉鹰王',55,'1','18800000000');
insert into user(id, name, age, gender, phone) VALUES (null,'金毛狮王',45,'1','18800000001');
insert into user(id, name, age, gender, phone) VALUES (null,'青翼蝠王',38,'1','18800000002');
insert into user(id, name, age, gender, phone) VALUES (null,'紫衫龙王',42,'2','18800000003');
insert into user(id, name, age, gender, phone) VALUES (null,'光明左使',37,'1','18800000004');
insert into user(id, name, age, gender, phone) VALUES (null,'光明右使',48,'1','18800000005');

如下建立各文件
在这里插入图片描述
mybatis起步依赖(xml文件)

<dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>

mysql驱动依赖

  <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
    </dependency>

引入yml文件需要根据自己实际数据库名称和密码进行更改

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456
  data:
    redis:
      host: localhost
      port: 6379
mybatis:
  configuration:
    map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换

在pojo包中建立User实体类

public class User {
    
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;

    public User() {
    }

    public User(Integer id, String name, Short age, Short gender, String phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.phone = phone;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Short getAge() {
        return age;
    }

    public void setAge(Short age) {
        this.age = age;
    }

    public Short getGender() {
        return gender;
    }

    public void setGender(Short gender) {
        this.gender = gender;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", phone='" + phone + '\'' +
                '}';
    }
}

建立UserMapper接口

@Mapper
public interface UserMapper {
    //根据id查询用户
    @Select("select * from user where id=#{id}")
    pubic User findByid(String id);
}

建立UserService接口

public interface UserService {
//    根据id查询用户
    public User findByid(String id);
}

建立接口的实现类UserServiceImpl

@Service
public class UserServiceImpl implements UserService {
        @Autowired
            private UserMapper userMapper;
         @Override
            public User findByUserId(String id) {
            return userMapper.findById(id);
    }
}

建立UserController类

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @RestController("/findById)
    public User findById(Integer id){
    return userService.findById(id);
    }
}

运行后在浏览器中访问为 则成功
在这里插入图片描述

6、Bean管理

1)Bean扫描

标签:<context:component-scan base-package=“com.itheima”/>

注解:@ComponentScan(basePackages = “com.itheima”)
在这里插入图片描述
@SpringBootApplication是一个组合注解,所以不需要去手动添加,他也能够自动扫描到controller service等等
在这里插入图片描述

2)Bean注册

在这里插入图片描述
注意:如果要注册的bean对象来自于第三方(不是自定义的),是无法用 @Component 及衍生注解声明bean的

可以注解第三方注入到ioc容器的方法是

@Bean
在这里插入图片描述
如果要注册第三方bean,建议在配置类中集中注册
在这里插入图片描述
对象默认的名字是:方法名

如果方法的内部需要使用到ioc容器中已经存在的bean对象,那么只需要在方法上声明即可,Spring会自动注入

@Import

导入 配置类
在这里插入图片描述
导入 ImportSelector 接口实现类在这里插入图片描述

@EnableXXXX注解,封装@Import注解

启动类

在这里插入图片描述

改为

在这里插入图片描述

注解类
在这里插入图片描述

3)注册条件

配置文件yml中
在这里插入图片描述在这里插入图片描述
同时SpringBoot提供了设置注册生效条件的注解 @Conditional
在这里插入图片描述
其中常用的注解
在这里插入图片描述
@ConditionalOnProperty //如果配置文件中配置了指定信息,则注入,否则不注入

@ConditionalOnMissingBean //如果ioc容器中存在相同类型的bean,则不会成功

@ConditionalOnClass // 例如:如果当前环境中有DispatcherServlet类,则注入当前有的bean(如:Province),否则不注入

7、自动配置原理

遵循约定大约配置的原则,在boot程序启动后,起步依赖中的一些bean对象会自动注入到ioc容器
在这里插入图片描述
提供一个自动配置类,把类名写到指定的配置文件里面
在这里插入图片描述
①:jar包提供CommonConfig类

②:自动配置类添加两个注解@AutoConfiguration(标识此类为自动配置类)和@Import(把CommonConfig类导入到jar包中)

③:提供一个.imports配置文件

④:把自动配置类的全类名配置到.imports配置文件里面
在这里插入图片描述

8、自定义starter

在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot 的 starter。

起步依赖由两个工程组成在这里插入图片描述
例:自定义mybatis的starter
-创建 dmybatis-spring-boot-autoconfigure 模块,提供自动配置功能,并自定义配置文件 META-INF/spring/xxx.imports
-创建 dmybatis-spring-boot-starter 模块,在starter中引入自动配置模块

依赖管理功能
在这里插入图片描述
pom.xml引入以下

<dependency>
    <groupId>com.itheima</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.13</version> 
</dependency>
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId> mybatis-spring</artifactId>
	<version>3.0.0</version> 
</dependency>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>17</source>
                <target>17</target>
            </configuration>
        </plugin>
    </plugins>
</build>

自动配置功能
在这里插入图片描述
pom.xml引入以下

<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.13</version> 
</dependency>
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId> mybatis-spring</artifactId>
	<version>3.0.0</version> 
</dependency>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>17</source>
                <target>17</target>
            </configuration>
        </plugin>
    </plugins>
</build>

建立config包,在包下建立MyBatisAutoConfig自动配置类引入以下

public class MyBatisAutoConfig {
//SqlSessionFactoryBean
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    return sqlSessionFactoryBean;
}
//MapperScannerConfigure
@Bean
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;
}
}

建立如下文件
在这里插入图片描述在这里插入图片描述
并在该文件配置以下全类名(需与自身名字相匹配)
在这里插入图片描述

在其他mybatis项目引入以下自己做好的依赖,即可成功运行

<dependency>
    <groupId>com.itheima</groupId>
    <artifactId>dmybatis-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

二、实战篇(后端)

在这里插入图片描述
在这里插入图片描述
需求:
在这里插入图片描述

1、开发模式&开发环境

开发模式

在这里插入图片描述

环境搭建

-执行资料中的big_event.sql脚本,准备数据库表

-- 创建数据库
create database big_event;

-- 使用数据库
use big_event;

-- 用户表
create table user (
                      id int unsigned primary key auto_increment comment 'ID',
                      username varchar(20) not null unique comment '用户名',
                      password varchar(32)  comment '密码',
                      nickname varchar(10)  default '' comment '昵称',
                      email varchar(128) default '' comment '邮箱',
                      user_pic varchar(128) default '' comment '头像',
                      create_time datetime not null comment '创建时间',
                      update_time datetime not null comment '修改时间'
) comment '用户表';

-- 分类表
create table category(
                         id int unsigned primary key auto_increment comment 'ID',
                         category_name varchar(32) not null comment '分类名称',
                         category_alias varchar(32) not null comment '分类别名',
                         create_user int unsigned not null comment '创建人ID',
                         create_time datetime not null comment '创建时间',
                         update_time datetime not null comment '修改时间',
                         constraint fk_category_user foreign key (create_user) references user(id) -- 外键约束
);

-- 文章表
create table article(
                        id int unsigned primary key auto_increment comment 'ID',
                        title varchar(30) not null comment '文章标题',
                        content varchar(10000) not null comment '文章内容',
                        cover_img varchar(128) not null  comment '文章封面',
                        state varchar(3) default '草稿' comment '文章状态: 只能是[已发布] 或者 [草稿]',
                        category_id int unsigned comment '文章分类ID',
                        create_user int unsigned not null comment '创建人ID',
                        create_time datetime not null comment '创建时间',
                        update_time datetime not null comment '修改时间',
                        constraint fk_article_category foreign key (category_id) references category(id),-- 外键约束
                        constraint fk_article_user foreign key (create_user) references user(id) -- 外键约束
)

-创建springboot工程,引入对应的依赖(web、mybatis、mysql驱动)
在这里插入图片描述
创建resources目录以及application.yml文件

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.3</version>
  </parent>
  <!--    web依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
<!--    mybitis依赖-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>3.0.0</version>
    </dependency>
<!--    mysql驱动依赖-->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
    </dependency>

-配置文件application.yml中引入mybatis的配置信息

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456

-创建包结构,并准备实体类
建立以下目录
在这里插入图片描述
pojo包下Article类

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;//更新时间
}

pojo包下Category类

public class Category {
    private Integer id;//主键ID
    private String categoryName;//分类名称
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

pojo包下User类

//lombok  在编译阶段,为实体类自动生成setter  getter toString
// pom文件中引入依赖   在实体类上添加注解
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;//更新时间
}

启动类(app)改为BigEventApplication,修改为以下内容

@SpringBootApplication
public class BigEventApplication
{
    public static void main( String[] args )
    {
       SpringApplication.run(BigEventApplication.class,args);
    }
}

2、用户

1、注册

//lombok 在编译阶段,为实体类自动生成setter getter toString
// pom文件中引入依赖 在实体类上添加注解

<!--      lombok依赖-->
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
      </dependency>

Result类

//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data
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);
    }
}

实体类上(Article,User,Category)添加注解
@Data

在这里插入图片描述
开发流程
在这里插入图片描述
需求
在这里插入图片描述
接口文档
点击跳转

思路分析
在这里插入图片描述

开发
controller包下建立UserController类

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping("/register")
    public Result register(String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//            查询用户
            User u = userService.findByUserName(username);
            if(u==null){
//            没有占用
                // 注册
                userService.register(username,password);
                return Result.success();
            }else{
//            占用
                return Result.error("用户名已被占用");
            }

        }

mapper包下建立UserMapper接口

@Mapper
public interface UserMapper {
    //根据用户名查询用户
    @Select("select * from user where username=#{username}")
    User findByUserName(String username);

    //添加
    @Insert("insert into user(username,password,create_time,update_time)" +
            " values(#{username},#{password},now(),now())")
    void add(String username, String password);
    }

service包下建立UserService接口

public interface UserService {
//    根据用户名查询用户
    User findByUserName(String username);
    //注册
    void register(String username, String password);
    }

service包下建立impl包再建立UserViceImpl类

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User findByUserName(String username) {
        User u = userMapper.findByUserName(username);
        return u;
    }
    @Override
    public void register(String username, String password) {
//        加密
        String md5String =Md5Util.getMD5String(password);
//        添加
        userMapper.add(username,md5String);
    }
    }

添加Md5Util类(一种加密方式)到utils工具包下

public class Md5Util {
    /**
     * 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
     */
    protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    protected static MessageDigest messagedigest = null;

    static {
        try {
            messagedigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException nsaex) {
            System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
            nsaex.printStackTrace();
        }
    }

    /**
     * 生成字符串的md5校验值
     *
     * @param s
     * @return
     */
    public static String getMD5String(String s) {
        return getMD5String(s.getBytes());
    }

    /**
     * 判断字符串的md5校验码是否与一个已知的md5码相匹配
     *
     * @param password  要校验的字符串
     * @param md5PwdStr 已知的md5校验码
     * @return
     */
    public static boolean checkPassword(String password, String md5PwdStr) {
        String s = getMD5String(password);
        return s.equals(md5PwdStr);
    }


    public static String getMD5String(byte[] bytes) {
        messagedigest.update(bytes);
        return bufferToHex(messagedigest.digest());
    }

    private static String bufferToHex(byte bytes[]) {
        return bufferToHex(bytes, 0, bytes.length);
    }

    private static String bufferToHex(byte bytes[], int m, int n) {
        StringBuffer stringbuffer = new StringBuffer(2 * n);
        int k = m + n;
        for (int l = m; l < k; l++) {
            appendHexPair(bytes[l], stringbuffer);
        }
        return stringbuffer.toString();
    }

    private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
        char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
        // 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
        char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
        stringbuffer.append(c0);
        stringbuffer.append(c1);
    }

}

测试
应用商店下载postman工具,点击跳转使用方式
在这里插入图片描述
在这里插入图片描述
点击import
在这里插入图片描述
点击跳转下载测试用例
在这里插入图片描述
启动idea,点击send
在这里插入图片描述
在这里插入图片描述

2、参数校验

在这里插入图片描述
第一种方法:在UserController类中register方法增加if-else判断用户名以及密码满足接口文档中的要求。
在这里插入图片描述
第二种方法:使用Spring Validation, 对注册接口的参数进行合法性校验
-引入Spring Validation 起步依赖
在这里插入图片描述
-在参数前面添加@Pattern注解
在这里插入图片描述
-在Controller类上添加@Validated注解

参数校验异常处理
在exception包下建立GlobalExceptionHandler类
在这里插入图片描述
全局异常处理器

3、登录

UserController类中添加登录方法

 @PostMapping("Login")
public Result<String> login(@Pattern(regexp = "^\setminus \\setminus5,16)" String username, @Pattern(regexp = "^ {s {5}, 16}") String password){
    //根据用户名查询用户
    User LoginUser = userService.findByUserName(username);
    //判断该用户是否存在
    if (LoginUser==null){
        return Result.error("用户名错误");
    }
    //判断密码是否正确 LoginUser对象中的password是密文
    if (Md5Util.getMD5String(password).equals(loginUser.getPassword())){
        //登录成功
        return Result.success(data: "jwt token令牌..");
    }
    return Result.error("密码错误");
}

登录认证
controller包下创建ArtcleController类
在这里插入图片描述

JWT令牌

在这里插入图片描述
引入依赖

<!--      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>
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <scope>test</scope>
      </dependency>

在test内创建JwtTest
生成jwt

public class JwtTest {

    @Test
    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))//添加过期时间
                .sign(Algorithm.HMAC256("itheima"));//指定算法,配置秘钥

        System.out.println(token);

    }
}

验证jwt

@Test
    public void testParse() {
        //定义字符串,模拟用户传递过来的token
        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE2OTQzMjUzMzB9.dFmeOG04w6EfnCue4CFS-x-XMRv145EfsY8wnchbxL4";
        //上一个jwt运行生成的token

        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();

        DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
        Map<String, Claim> claims = decodedJWT.getClaims();
        System.out.println(claims.get("user"));

        //如果篡改了头部和载荷部分的数据,那么验证失败
        //如果秘钥改了,验证失败
        //token过期
    }
登录认证

添加到工具类utils

public class JwtUtil {

    private static final String KEY = "itheima";
	
	//接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 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();
    }

}

更改UserController接口

@PostMapping("/login")
        public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {

//        根据用户名查询用户
            User loginUser = userService.findByUserName(username);
//            判断用户是否存在
            if(loginUser==null){
                return Result.error("用户名错误");
            }
//            判断密码是否正确 loginUser对象中的password是密文
            if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//                登录成功
                Map<String,Object> claims = new HashMap<>();
                claims.put("id",loginUser.getId());
                claims.put("username",loginUser.getUsername());
                String token = JwtUtil.genToken(claims);
//                把token存储到redis中
                ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
                operations.set(token,token,1, TimeUnit.HOURS);
                return Result.success(token);
            }
            return Result.error("密码错误");
    }

更改ArticleController

@RestController
@RequestMapping("/article")
public class ArticleController {
    @GetMapping("/list")
    public Result<String> list(@RequestHeader(name = "Authorization") String token, HttpServletResponse response){
        验证token
        try {
            Map<String, Object> claims = JwtUtil.parseToken(token);
            return Result.success("所有文掌数据");
        } catch (Exception e) {
            /*http响应状态码为401*/
            response.setStatus(401);
           return Result.error("未登录");
        }
        return Result.success("所有文掌数据");
    }

在这里插入图片描述
postman登录,获取token
在这里插入图片描述
添加到Authorition中(时间过长需要重新登录获取新token添加)

设立拦截器
interceptors包下建立LoginInterceptor

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    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失效
                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;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清空ThreadLocal中的数据
        ThreadLocalUtil.remove();
    }
}

config包下建立WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        登录接口和注册接口不拦截
        registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
    }
}

4、获取用户详细信息

ThreadLocal:提供线程局部变量,用来存取数据: set()/get()
使用ThreadLocal存储的数据, 线程安全。
在这里插入图片描述
建一个测试类,可测试threadlocal方法

 @Test
    public void testThreadLocalSetAndGet() {
//        提供一个TheadLocal
        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());
        },"蓝色").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());
        },"绿色").start();
    }
}

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();
    }
}

UserController类

@GetMapping("/userInfo")
    public Result<User> userInfo(/*@RequestHeader(name = "Authorization")String token*/){
//        根据用户名查询用户
        /*Map<String, Object> map = JwtUtil.parseToken((token));
        String username = (String) map.get("username");*/  
        //或者调用ThreadLocal方法
        Map<String,Object> map = ThreadLocalUtil.get();
        String username =(String) map.get("username");
        
        User user = userService.findByUserName((username));
        return  Result.success(user);
    }

User类添加
在这里插入图片描述
postman(添加Authorization)测试获取用户详细信息。
或者添加全局请求头
在这里插入图片描述

5、更新用户基本信息

在这里插入图片描述
UserController

@PutMapping("/update")
    public Result update(@RequestBody @Validated User user){
        userService.update(user);
        return Result.success();
    }

UserService

//更新
    void update(User user);

UserServiceImpl

@Override
    public void update(User user) {
        user.setUpdateTime(LocalDateTime.now());
        userMapper.update(user);
    }

UserMapper

@Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}")
    void update(User user);

参数校验
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6、更新用户头像

UserController

@PatchMapping("updateAvatar")
    public Result updateAvatar(@RequestParam @URL String avatarUrl){
        userService.updateAvatar(avatarUrl);
        return Result.success();
    }

UserService

//更新头像
    void updateAvatar(String avatarUrl);

UserServiceImpl

@Override
    public void updateAvatar(String avatarUrl) {
        Map<String,Object> map = ThreadLocalUtil.get();
        Integer id =(Integer) map.get("id");
        userMapper.updateAvatar(avatarUrl,id);
    }

UserMapper

@Update("update user set user_pic=#{avatarUrl},update_time=now() where id=#{id}")
    void updateAvatar(String avatarUrl,Integer id);

7、更新用户密码

UserController

@PatchMapping ("updatePwd")
    public Result updatePwd(@RequestBody Map< String,String> params,@RequestHeader("Authorization") String token){
//        1.校验参数
        String oldPwd = params.get("old_pwd");
        String newPwd = params.get("new_pwd");
        String rePwd = params.get("re_pwd");

        if(!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(rePwd)){
            return Result.error("缺少必要的参数");
        }
//        原密码是否正确
//        调用userService根据用户名拿到原密码,再和old_pwd对比
        Map<String,Object> map = ThreadLocalUtil.get();
        String username =(String) map.get("username");
        User loginUser = userService.findByUserName(username);
        if (!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
            return Result.error("原密码填写不正确");
        }

//        newPwd和rePwd是否一样
        if (!rePwd.equals(newPwd)){
            return Result.error("两次填写的新密码不一致");
        }
//        2.调用service完成密码更新
        userService.updatePwd(newPwd);
        //删除redis中对应的token
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        operations.getOperations().delete(token);
        return Result.success();
    }

UserService

//更新密码
    void updatePwd(String newPwd);

UserServiceImpl

@Override
    public void updatePwd(String newPwd) {
        Map<String,Object> map = ThreadLocalUtil.get();
        Integer id =(Integer) map.get("id");
        userMapper.updatePwd(Md5Util.getMD5String(newPwd),id);
    }

UserMapper

 @Update("update user set password=#{md5String},update_time=now() where id=#{id}")
    void updatePwd(String md5String);

3、文章分类

1、新增文章分类

在这里插入图片描述
CategoryController

@RestController
@RequestMapping("/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    @PostMapping
    private Result add(@RequestBody @Validated(ControllerAdvice.class) Category category){
        categoryService.add(category);
        return Result.success();
    }
   } 

CategoryService

//    新增分类
    void add(Category category);

CategoryServiceImpl

@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
    private ArticleMapper articleMapper;

    @Override
    public void add(Article article) {
//        补充属性值
        article.setCreateTime(LocalDateTime.now());
        article.setUpdateTime(LocalDateTime.now());

        Map<String,Object> map = ThreadLocalUtil.get();
        Integer userId = (Integer) map.get("id");
        article.setCreateUser(userId);

        articleMapper.add(article);
    }
    }

CategoryMapper

//    新增
    @Insert("insert into category(category_name,category_alias,create_user,create_time,update_time) " +
        "values(#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})")
void add(Category category);

Category添加注解

@Data
public class Category {
    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty/*(groups = {Add.class,Update.class})*/
    private String categoryName;//分类名称
    @NotEmpty/*(groups = {Add.class,Update.class})*/
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;//创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;//更新时间


//    如果说某个校验没有指定分组,默认属于Default分组
//    分组之间可以继承,A extent B 那么A中拥有所有的校验项
    public interface Add extends Default {

    }
    public interface Update extends Default {

    }
}

分组校验: 把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项
1、定义分组
2、定义校验项时指定归属的分组
3、校验时指定要校验的分组
在这里插入图片描述
在这里插入图片描述

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/96f626d6787e42689d0f629cbc6070ca.png在这里插入图片描述

注意:定义校验项时如果没有指定分组,则属于Default分组,分组可以继承

2、文章分类列表

CategoryController

@GetMapping
    public Result<List<Category>> list(){
        List<Category> cs = categoryService.list();
        return Result.success(cs);
    }

CategoryService

//列表查询
    List<Category> list();

CategoryMapper

//查询所有
    @Select("select * from category where create_user = #{userId}")
    List<Category> list(Integer userId);

CategoryServiceImpl

@Override
    public List<Category> list() {
        Map<String,Object> map = ThreadLocalUtil.get();
        Integer userId = (Integer) map.get("id");
        return categoryMapper.list(userId);
    }

3、获取文章分类详情

CategoryController

@GetMapping("/detail")
    public Result<Category>detail(Integer id){
        Category c = categoryService.findById(id);
        return Result.success(c);
    }

CategoryService

//根据id查询信息
    Category findById(Integer id);

CategoryMapper

//根据id查询
    @Select("select * from category where id=#{id}")
    Category findById(Integer id);

CategoryServiceImpl

 @Override
    public Category findById(Integer id) {
        Category c = categoryMapper.findById(id);
        return c;
    }

4、更新文章分类

CategoryController

@PutMapping
    public Result update(@RequestBody @Validated(Category.Update.class) Category category){
        categoryService.update(category);
        return Result.success();
    }

CategoryService

//更新分类
    void update(Category category);

CategoryMapper

//更新
    @Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id=#{id}")
    void update(Category category);

CategoryServiceImpl

@Override
public void update(Category category) {
    category.setUpdateTime(LocalDateTime.now());
    categoryMapper.update(category);
}

5、删除文章分类

CategoryController

@DeleteMapping
    public Result<Category> delete(Integer id){
        categoryService.deleteById(id);
        return Result.success();
    }

CategoryService

//根据id删除信息
    void deleteById(Integer id);

CategoryMapper

//    根据id删除
    @Delete("delete from category where id=#{id}")
    void deleteById(Integer id);

CategoryServiceImpl

@Override
public void deleteById(Integer id) {
    categoryMapper.deleteById(id);
}

4、文章分类

1、新增文章

在这里插入图片描述
Article,同时对参数进行校验

@Data
public class Article {
    private Integer id;//主键ID
    @NotEmpty
    @Pattern(regexp = "^\\S{1,10}$")
    private String title;//文章标题
    @NotEmpty
    private String content;//文章内容
    @NotEmpty
    @URL
    private String coverImg;//封面图像
    @State
    private String state;//发布状态 已发布|草稿
    @NotNull
    private Integer categoryId;//文章分类id
    private Integer createUser;//创建人ID
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

自定义校验: 已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验(自定义校验注解)
1、自定义注解State
2、自定义校验数据的类StateValidation实现ConstraintValidator接口
3. 在需要校验的地方使用自定义注解
在这里插入图片描述
在这里插入图片描述
建立anno包再建立State注解

@Documented//元注解
@Target({ FIELD})//元注解
@Retention(RUNTIME)//元注解
@Constraint(validatedBy = { StateValidation.class})//指定提供校验规则的类
public @interface State {
    //提供校验失败后的提示信息
    String message() default "state参数的值只能是已发布或者草稿";
    //指定分组
    Class<?>[] groups() default { };
    //负载  获取到State注解的附加信息
    Class<? extends Payload>[] payload() default { };
}

建立validation包再建立StateValidation

public class StateValidation implements ConstraintValidator<State,String> {
    /**
     *
     * @param value 将来要校验的数据
     * @param context context in which the constraint is evaluated
     *
     * @return 如果返回false,则校验不通过,如果返回true,则校验通过
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        //提供校验规则
        if (value == null){
            return false;
        }
        if (value.equals("已发布") || value.equals("草稿")){
            return true;
        }
        return false;
    }
}

ArticleController

@PostMapping
    public  Result add(@RequestBody @Validated Article article){
        articleService.add(article);
        return Result.success();
    }

ArticleService
public interface ArticleService {

//    新增文章
    void add(Article article);

ArticleMapper

//新增
@Insert("insert into article(title,content,cover_img,state,category_id,create_user,create_time,update_time) " +
“values(#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})”)
void add(Article article);

ArticleServiceImpl

@Service
public class ArticleServiceImpl implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    @Override
    public void add(Article article) {
//        补充属性值
        article.setCreateTime(LocalDateTime.now());
        article.setUpdateTime(LocalDateTime.now());

        Map<String,Object> map = ThreadLocalUtil.get();
        Integer userId = (Integer) map.get("id");
        article.setCreateUser(userId);

        articleMapper.add(article);
    }

2、文章列表(条件分页)

在这里插入图片描述
在pojo包下添加PageBean

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean <T>{
    private Long total;//总条数
    private List<T> items;//当前页数据集合
}

ArticleController

@GetMapping
    public Result<PageBean<Article>> list(
            Integer pageNum,
            Integer pageSize,
            @RequestParam(required = false) Integer categoryId,
            @RequestParam(required = false) String state
    ){
            PageBean<Article> pb = articleService.list(pageNum,pageSize,categoryId,state);
            return Result.success(pb);
    }

ArticleService

//条件分页列表查询
    PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state);

ArticleMapper同时在resources目录下建立ArticleMapper.xml文件

List<Article> list(Integer userId, Integer categoryId, String state);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.ArticleMapper">

<!--    动态aql-->
    <select id="list" resultType="com.itheima.pojo.Article">
        select * from article
        <where>
            <if test="categoryId!=null">
                category_id=#{categoryId}
            </if>
        <if test="state!=null">
            and state=#{state}
        </if>
        and create_user=#{userId}
        </where>
    </select>
    
</mapper>

ArticleServiceImpl

@Override
    public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
//        1.创建pageBean对象
        PageBean<Article> pb = new PageBean<>();
//        开启分页查询 PageHelper
        PageHelper.startPage(pageNum,pageSize);
//        调用mapper
        Map<String,Object> map = ThreadLocalUtil.get();
        Integer userId = (Integer) map.get("id");
        List<Article> as = articleMapper.list(userId,categoryId,state);
//        Page中提供了方法,可以获取PageHelper分页查询后,得到的总记录条数和当前页数据
        Page<Article> p = (Page<Article>) as;

//        把数据填充到PageBean中
        pb.setTotal(p.getTotal());
        pb.setItems(p.getResult());
        return pb;
    }

3、获取文章详情

ArticleController

@GetMapping("/detail")
    public Result<Article>detail(Integer id){
        Article c = articleService.findById(id);
        return Result.success(c);
    }

ArticleService

//根据id查询
    Article findById(Integer id);

ArticleMapper

//根据id查询
    @Select("select * from article where id=#{id}")
    Article findById(Integer id);

ArticleServiceImpl

@Override
    public Article findById(Integer id) {
        Article c = articleMapper.findById(id);
        return c;
    }

4、更新文章

ArticleController

@PutMapping
    public Result update(@RequestBody @Validated Article article){
        articleService.update(article);
        return Result.success();
    }

ArticleService

//更新文章
    void update(Article article);

ArticleMapper

//更新update
    @Update("update article set title=#{title},content=#{content},cover_img=#{coverImg},state=#{state},category_id=#{categoryId} where id=#{id}")
    void update(Article article);

ArticleServiceImpl

@Override
    public void update(Article article) {
            article.setUpdateTime(LocalDateTime.now());
            articleMapper.update(article);
        }

5、删除文章

ArticleController

@DeleteMapping
    public Result<Category> delete(Integer id){
        articleService.deleteById(id);
        return Result.success();
    }

ArticleService

//根据id删除
    void deleteById(Integer id);

ArticleMapper

//根据id删除
    @Delete("delete from article where id=#{id}")
    void deleteById(Integer id);

ArticleServiceImpl

@Override
    public void deleteById(Integer id) {
        articleMapper.deleteById(id);
    }

5、文件上传

在这里插入图片描述
在controller文件下建立FileUploadController

@RestController
public class FileUploadController {
    @PostMapping
    public Result<String> upload(MultipartFile file) throws Exception {
//        把文件内容存储到本地磁盘上
        String originalFilename = file.getOriginalFilename();
//        保证文件的名字是唯一的,从而防止文件覆盖
        String fileName = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
        //file.transferTo(new File("E:\\Desktop\\files\\"+originalFilename));
        String url = AliOssUtil.uploadFile(fileName,file.getInputStream());
        return Result.success(url);
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

<!--      阿里云oss依赖坐标-->
      <dependency>
          <groupId>com.aliyun.oss</groupId>
          <artifactId>aliyun-sdk-oss</artifactId>
          <version>3.17.4</version>
      </dependency>
      <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>

utils下建立个AilOssUtil
ACCESS_KEY = “” ACCESS_KEY_SECRET = “”
需要与自己在阿里云建立的秘钥相匹配

public class AliOssUtil {
    // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
    private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
    // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
//        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
    private static final String ACCESS_KEY = "LTg75tLhqq37";
    private static final String ACCESS_KEY_SECRET = "UIN5YcokLX2jJ7y8y41";
    // 填写Bucket名称,例如examplebucket。
    private static final String BUCKET_NAME = "big-event0025";
    public static String uploadFile(String objectName, InputStream in) throws Exception {

        // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
        String region = "cn-beijing";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClient(ENDPOINT, ACCESS_KEY, ACCESS_KEY_SECRET);
        String url = "";
        try {
            // 填写字符串。
            String content = "Hello OSS,你好世界";

            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, objectName, in);


            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传字符串。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
//            url组成:https://bucket名称。区域节点.objrctName
            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;
    }
}

6、登录优化-redis

在这里插入图片描述

1、SpringBoot集成redis

1、导入spring-boot-starter-data-redis起步依赖

<!--      redis坐标-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>

2、在yml配置文件中, 配置redis连接信息
在这里插入图片描述

3、调用API(StringRedisTemplate)完成字符串的存取操作
在这里插入图片描述

@SpringBootTest//如果在测试类上添加了这个注解,那么将来单元测试方法执行之前,会先初始化Spring容器
public class RedisTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void testSet(){
//      往Redis中存储一个键值对 StringRedisTemplate
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        operations.set("username", "zhangsan");
        operations.set("id","1",15, TimeUnit.SECONDS);
    }

    @Test
    public void testGet(){
//        从redis中获取一个键值对
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        System.out.println(operations.get("username"));
    }
}

2、令牌主动失效

-登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中
-LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
-当用户修改密码成功后,删除redis中存储的旧令牌

7、SpringBoot部署

在这里插入图片描述

<build>
        <plugins>
            <!--打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.1.3</version>
            </plugin>
        </plugins>
    </build>

双击
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
打开方式(本地磁盘)
在这里插入图片描述
运行在某服务器
在文件目录下进入命令行提示符窗口 CMD
在这里插入图片描述
在这里插入图片描述

1、属性配置方式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
配置优先级
1、项目中resources目录下的application.yml
2、Jar包所在目录下的application.yml
3、操作系统环境变量
4、命令行参数

2、多环境开发Pofiles

SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定环境下指定部分配置生效
在这里插入图片描述

在这里插入图片描述
如果特定环境中的配置和通用信息冲突了,特定环境中的配置生效
在这里插入图片描述

3、多环境开发-分组

在这里插入图片描述
-按照配置的类别,把配置信息配置到不同的配置文件中
application-分类名.yml

-在application.yml中定义分组
spring.profiles.group

-在application.yml中激活分组
spring.profiles.active

8、新特性-原生镜像

项目部署-原生镜像
在这里插入图片描述
在这里插入图片描述
1、安装GraalVM JDK
点击跳转
在这里插入图片描述

2、安装MSVC环境
点击跳转
在这里插入图片描述

3、打包springboo3项目,并完成测试

<plugin>
	<groupId>org.graalvm.buildtools</groupId>
	<artifactId>native-maven-plugin</artifactId>
</plugin>

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、实战篇(前端)

在这里插入图片描述
JavaScript-导入导出
在这里插入图片描述
在这里插入图片描述

JS提供的导入导出机制,可以实现按需导入。
在这里插入图片描述
或者
在这里插入图片描述

在这里插入图片描述
导入和导出的时候, 可以使用 as 重命名:如complexMessage as cm
默认导出
在这里插入图片描述
在这里插入图片描述

1、Vue

Vue 是一款用于构建用户界面的渐进式的JavaScript框架。 (官方:https://cn.vuejs.org/
在这里插入图片描述
在这里插入图片描述
学习路径
在这里插入图片描述

1、快速入门

https://cn.vuejs.org
–准备
1、准备html页面,并引入Vue模块(官方提供)
2、创建Vue程序的应用实例
3、准备元素(div),被Vue控制
–构建用户界面
1、准备数据
2、通过插值表达式渲染页面

<!DOCTYPE html>
<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"><!-- 准备元素(div),被Vue控制 -->
        <h1>{{msg}}</h1><!-- 通过插值表达式渲染页面 -->
    </div>

    <div >
        <h1>{{msg}}</h1>
    </div>
    <!-- 引入vue模块 -->
    <script type="module">
        import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';//引入Vue模块
        /* 创建vue的应用实例 */
        createApp({//创建Vue程序的应用实例
            data(){//准备数据
                return {
                    //定义数据
                    msg: 'hello vue3'
                }
            }

        }).mount("#app");
    </script>
</body>
</html>

2、常用指令

指令:HTML标签上带有 v-前缀的特殊属性,不同的指令具有不同的含义,可以实现不同的功能。
常用指令:
在这里插入图片描述

v-for

在这里插入图片描述
作用:列表渲染,遍历容器的元素或者对象的属性
语法: v-for = “(item,index) in items”
–参数说明:
items 为遍历的数组
item 为遍历出来的元素
index 为索引/下标,从0开始 ;可以省略,省略index语法: v-for = “item in items”
注意:遍历的数组,必须在data中定义; 要想让哪个标签循环展示多次,就在哪个标签上使用 v-for 指令。

<!DOCTYPE html>
<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">
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
            <!-- 哪个元素要出现多次,v-for指令就添加到哪个元素上 -->
            <tr v-for="(article,index) in articleList">
                <td>{{article.title}}</td>
                <td>{{article.category}}</td>
                <td>{{article.time}}</td>
                <td>{{article.state}}</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <!-- <tr>
                <td>标题2</td>
                <td>分类2</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <tr>
                <td>标题3</td>
                <td>分类3</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr> -->
        </table>
    </div>

    <script type="module">
        //导入vue模块
        import { createApp} from 
                'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        //创建应用实例
        createApp({
            data() {
                return {
                  //定义数据
                    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:"已发布"
                            }]  
                }
            }
        }).mount("#app")//控制页面元素
        
    </script>
</body>
</html>
v-bind

作用:动态为HTML标签绑定属性值,如设置href,src,style样式等。
语法:v-bind:属性名=“属性值”
简化::属性名=“属性值”
注意:v-bind所绑定的数据,必须在data中定义 。

<!DOCTYPE html>
<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">
        <!-- <a v-bind:href="url">黑马官网</a> -->
        <a :href="url">黑马官网</a>
    </div>

    <script type="module">
        //引入vue模块
        import { createApp} from 
                'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        //创建vue应用实例
        createApp({
            data() {
                return {
                    url: 'https://www.itheima.com'
                }
            }
        }).mount("#app")//控制html元素
    </script>
</body>
</html>
v-if & v-show

作用:这两类指令,都是用来控制元素的显示与隐藏的
v-if
语法:v-if=“表达式”,表达式值为 true,显示;false,隐藏
其它:可以配合 v-else-if / v-else 进行链式调用条件判断
原理:基于条件判断,来控制创建或移除元素节点(条件渲染)
场景:要么显示,要么不显示,不频繁切换的场景

v-show
语法:v-show=“表达式”,表达式值为 true,显示;false,隐藏
原理:基于CSS样式display来控制显示与隐藏
场景:频繁切换显示隐藏的场景

<!DOCTYPE html>
<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">

        手链价格为:  <span v-if="customer.level>=0 && customer.level<=1">9.9</span>  
                    <span v-else-if="customer.level>=2 && customer.level<=4">19.9</span> 
                    <span v-else>29.9</span>

        <br/>
        手链价格为:  <span v-show="customer.level>=0 && customer.level<=1">9.9</span>  
                    <span v-show="customer.level>=2 && customer.level<=4">19.9</span> 
                    <span v-show="customer.level>=5">29.9</span>

    </div>

    <script type="module">
        //导入vue模块
        import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

        //创建vue应用实例
        createApp({
            data() {
                return {
                    customer:{
                        name:'张三',
                        level:2
                    }
                }
            }
        }).mount("#app")//控制html元素
    </script>
</body>

</html>
v-on

作用:为html标签绑定事件
语法:
v-on:事件名=“函数名”
简写为 @事件名=“函数名”
createApp({ data(){需要用到的数据}, methods:{需要用到的方法} })

<!DOCTYPE html>
<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">
        <button v-on:click="money">点我有惊喜</button> &nbsp;
        <button @click="love">再点更惊喜</button>
    </div>

    <script type="module">
        //导入vue模块
        import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

        //创建vue应用实例
        createApp({
            data() {
                return {
                    //定义数据
                }
            },
            methods:{
                money: function(){
                    alert('送你钱100')
                },
                love: function(){
                    alert('爱你一万年')
                }
            }
        }).mount("#app");//控制html元素

    </script>
</body>
</html>
v-model

作用:在表单元素上使用,双向数据绑定。可以方便的 获取 或 设置 表单项数据
语法:v-model=“变量名”
在这里插入图片描述
在这里插入图片描述
注意:v-model 中绑定的变量,必须在data中定义。

<!DOCTYPE html>
<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">

        文章分类: <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 v-on:click="clear">重置</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">
                <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>
    <script type="module">
        //导入vue模块
        import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        //创建vue应用实例
        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: "已发布"
                    }]
                }
            }
            ,
            methods:{
                clear:function(){
                    //清空category以及state的数据
                    //在methods对应的方法里面,使用this就代表的是vue实例,可以使用this获取到vue实例中准备的数据
                    this.searchConditions.category='';
                    this.searchConditions.state='';
                }
            }
            ,
            mounted:function(){
                console.log('Vue挂载完毕,发送请求获取数据')
            }
        }).mount("#app")//控制html元素
    </script>
</body>

</html>

3、生命周期

生命周期:指一个对象从创建到销毁的整个过程。
生命周期的八个阶段:每个阶段会自动执行一个生命周期方法(钩子), 让开发者有机会在特定的阶段执行自己的代码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Axios

介绍:Axios 对原生的Ajax进行了封装,简化书写,快速开发。
官网:https://www.axios-http.cn/

Axios使用步骤
引入Axios的js文件(参照官网)
使用Axios发送请求,并获取相应结果
在这里插入图片描述

Axios-请求方式别名
为了方便起见,Axios已经为所有支持的请求方法提供了别名
格式:axios.请求方式(url [, data [, config]])
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!-- 引入axios的js文件 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
        /* 发送请求 */
        /* axios({
            method:'get',
            url:'http://localhost:8080/article/getAll'
        }).then(result=>{
            //成功的回调
            //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
            console.log(result.data);
        }).catch(err=>{
            //失败的回调
            console.log(err);
        }); */
        let article = {
            title: '明天会更好',
            category: '生活',
            time: '2000-01-01',
            state: '草稿'
        }
        /*  axios({
             method:'post',
             url:'http://localhost:8080/article/add',
             data:article
         }).then(result=>{
             //成功的回调
             //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
             console.log(result.data);
         }).catch(err=>{
             //失败的回调
             console.log(err);
         }); */

        //别名的方式发送请求
        /* axios.get('http://localhost:8080/article/getAll').then(result => {
            //成功的回调
            //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
            console.log(result.data);
        }).catch(err => {
            //失败的回调
            console.log(err);
        }); */
        axios.post('http://localhost:8080/article/add', article).then(result => {
            //成功的回调
            //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
            console.log(result.data);
        }).catch(err => {
            //失败的回调
            console.log(err);
        });
    </script>
</body>

</html>
案例

使用表格展示所有文章的数据, 并完成条件搜索功能
在这里插入图片描述
钩子函数mounted中, 获取所有的文章数据
使用v-for指令,把数据渲染到表格上展示
使用v-model指令完成表单数据的双向绑定
使用v-on指令为搜索按钮绑定单击事件

<!DOCTYPE html>
<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">

        文章分类: <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">
                <td>{{article.title}}</td>
                <td>{{article.category}}</td>
                <td>{{article.time}}</td>
                <td>{{article.state}}</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <!-- <tr>
                <td>标题2</td>
                <td>分类2</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <tr>
                <td>标题3</td>
                <td>分类3</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr> -->
        </table>
    </div>
    <!-- 导入axios的js文件 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script type="module">
        //导入vue模块
        import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
        //创建vue应用实例
        createApp({
            data(){
                return {
                    articleList:[],
                    searchConditions:{
                        category:'',
                        state:''
                    }
                }
            },
            methods:{
                //声明方法
                search:function(){
                    //发送请求,完成搜索,携带搜索条件
                    axios.get('http://localhost:8080/article/search?category='+this.searchConditions.category+'&state='+this.searchConditions.state)
                    .then(result=>{
                        //成功回调 result.data
                        //把得到的数据赋值给articleList
                        this.articleList=result.data
                    }).catch(err=>{
                        console.log(err);
                    });
                }
            },
            //钩子函数mounted中,获取所有文章数据
            mounted:function(){
                //发送异步请求  axios
                axios.get('http://localhost:8080/article/getAll').then(result=>{
                    //成功回调
                    //console.log(result.data);
                    this.articleList=result.data;
                }).catch(err=>{
                    //失败回调
                    console.log(err);
                });
            }
        }).mount('#app');//控制html元素
    </script>
</body>

</html>

2、整站使用Vue(工程化)

1、环境准备

在这里插入图片描述

在这里插入图片描述

  1. 选择安装目录

选择安装到一个,没有中文,没有空格的目录下(新建一个文件夹NodeJS)


在这里插入图片描述

  1. 验证NodeJS环境变量

NodeJS 安装完毕后,会自动配置好环境变量,我们验证一下是否安装成功,通过: node -v

在这里插入图片描述

  1. 配置npm的全局安装路径

在这里插入图片描述

使用管理员身份运行命令行,在命令行中,执行如下指令:

npm config set prefix "D:\develop\NodeJS"

注意:D:\develop\NodeJS 这个目录是NodeJS的安装目录

5.更换安装包的源

设置

npm config set registry http://registry.npm.taobao.org/

检查

npm config get registry

2、Vue项目创建和启动

创建一个工程化的Vue项目,执行命令:npm init vue@latest
在这里插入图片描述
在这里插入图片描述
进入项目目录,执行命令安装当前项目的依赖:npm install
在这里插入图片描述
Vue项目-目录结构
在这里插入图片描述
Vue项目-启动
执行命令:npm run dev ,就可以启动vue项目了。
在这里插入图片描述
或者
在这里插入图片描述
访问项目:打开浏览器,在浏览器地址栏访问 http://127.0.0.1:5173 就可以访问到vue项目。
在这里插入图片描述

3、Vue项目开发流程

Vue项目-目录结构
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
.vue是Vue项目中的组件文件,在Vue项目中也称为单文件组件(SFC,Single-File Components)。Vue 的单文件组件会将一个组件的逻辑 (JS),模板 (HTML) 和样式 (CSS) 封装在同一个文件里(.vue) 。

<!-- <script>
  //写数据
  export default{
    data(){
      return {
        msg:'上海'
      }
    }
  }
</script> -->
<script setup>
  import {ref} from 'vue';
  //调用ref函数,定义响应式数据
  const msg = ref('西安');

  //导入 Api.vue文件
  import ApiVue from './Api.vue'
  //导入Article.vue文件
  import ArticleVue from './Article.vue'
</script>

<template>
  <!-- html -->
  <!-- <h1>北京</h1> -->
  <!-- <h1>{{ msg }}</h1>
  <br>
  <ApiVue/> -->

  <ArticleVue/>
</template>

<style scoped>
  /* 样式 */
  h1{
    color: red;
  }
</style>

4、API风格

Vue的组件有两种不同的风格:组合式API 和 选项式API
在这里插入图片描述
setup:是一个标识,告诉Vue需要进行一些处理,让我们可以更简洁的使用组合式API。
ref():接收一个内部值,返回一个响应式的ref对象,此对象只有一个指向内部值的属性 value。
onMounted():在组合式API中的钩子方法,注册一个回调函数,在组件挂载完成后执行。

<script setup>
    import {ref,onMounted} from 'vue'
    //声明响应式数据 ref  响应式对象有一个内部的属性value
    const count = ref(0); //在组合式api中,一般需要把数据定义为响应式数据
    //const count=0;

    //声明函数
    function increment(){
        count.value++;
    }

    //声明钩子函数 onMounted
    onMounted(()=>{
        console.log('vue 已经挂载完毕了...');
    });
</script>

<template>
    <!-- 写html元素 -->
    <button @click="increment">count: {{ count }}</button>
</template>

在这里插入图片描述
选项式API,可以用包含多个选项的对象来描述组件的逻辑,如:data,methods,mounted等。

5、案例

使用表格展示所有文章的数据, 并完成条件搜索功能
在这里插入图片描述
钩子函数mounted中, 获取所有的文章数据
使用v-for指令,把数据渲染到表格上展示
使用v-model指令完成表单数据的双向绑定
使用v-on指令为搜索按钮绑定单击事件
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在请求或响应被 then 或 catch 处理前拦截它们
在这里插入图片描述
Artcle.vue

<script setup>
    import {articleGetAllService,articleSearchService} from '@/api/article.js';
    import {ref} from 'vue';
    
    //定义响应式数据  ref
    const articleList = ref([]);

    //获取所有文章数据
    //同步获取articleGetAllService的返回结果  async await
    const getAllArticle=async function(){
        let data = await articleGetAllService();
        articleList.value=data;
    }

    getAllArticle();
   
    

    //定义响应式数据 searchConditions
    const searchConditions = ref({
        category:'',
        state:''
    })

    //声明search函数
    const search = async function(){
        //文章搜索
        let data = await articleSearchService({...searchConditions.value});
        articleList.value = data;
    }

</script>

<template>
    <!-- html元素 -->
    <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">
                <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>

artcle.js

/* //导入axios  npm install axios
import axios from 'axios';
//定义一个变量,记录公共的前缀  ,  baseURL
const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL}) */

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 });
}

request.js

//定制请求的实例

//导入axios  npm install axios
import axios from 'axios';
//定义一个变量,记录公共的前缀  ,  baseURL
const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL})


//添加响应拦截器
instance.interceptors.response.use(
    result=>{
        return result.data;
    },
    err=>{
        alert('服务异常');
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;

3、Element Plus

Element:是饿了么团队研发的,基于 Vue 3,面向设计师和开发者的组件库。
组件:组成网页的部件,例如 超链接、按钮、图片、表格、表单、分页条等等。
官网:https://element-plus.org/zh-CN/#/zh-CN

1、快速入门

准备工作:
创建一个工程化的vue项目
参照官方文档,安装Element Plus组件库(在当前工程的目录下):npm install element-plus --save
在这里插入图片描述

main.js中引入Element Plus组件库(参照官方文档)

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/locale/zh-cn.js'

const app = createApp(App)//创建应用实例

app.use(ElementPlus,{locale})//使用element-plus
app.mount('#app')//控制html元素

制作组件:
访问Element官方文档,复制组件代码,调整
Button.vue

<script lang="ts" setup>
import {
    Check,
    Delete,
    Edit,
    Message,
    Search,
    Star,
} from '@element-plus/icons-vue'
</script>

<template>
    <el-row class="mb-4">
        <el-button>Default</el-button>
        <el-button type="primary" disabled="true">编辑</el-button>
        <el-button type="success" loading="true">查看</el-button>
        
    </el-row>

    <el-row class="mb-4">
        
        <el-button type="info" plain>Info</el-button>
        <el-button type="warning" plain>Warning</el-button>
        <el-button type="danger" plain>Danger</el-button>
    </el-row>

</template>

App.vue

<script setup>
  import ButtonVue from './Button.vue'
  import ArticleVue from './Article.vue'
</script>

<template>
  <!-- <ButtonVue/> -->
  <ArticleVue/> 
</template>

2、常用组件

在这里插入图片描述
Article.vue

<script lang="ts" setup>
import { reactive } from 'vue'

const formInline = reactive({
    user: '',
    region: '',
    date: '',
})

const onSubmit = () => {
    console.log('submit!')
}

import { ref } from 'vue'

const currentPage4 = ref(2)
const pageSize4 = ref(5)
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const total = ref(20)

const handleSizeChange = (val: number) => {
    console.log(`${val} items per page`)
}
const handleCurrentChange = (val: number) => {
    console.log(`current page: ${val}`)
}

import {
    Delete,
    Edit,
} from '@element-plus/icons-vue'

const tableData = [
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    }

]
</script>

<template>
    <el-card class="box-card">

        <div class="card-header">
            <span>文章管理</span>
            <el-button type="primary">发布文章</el-button>
        </div>

        <div style="margin-top: 20px;">
            <hr>
        </div>

        <el-form :inline="true" :model="formInline" class="demo-form-inline">

            <el-form-item label="文章分类:">
                <el-select v-model="formInline.region" placeholder="请选择" clearable>
                    <el-option label="时事" value="时事" />
                    <el-option label="篮球" value="篮球" />
                </el-select>
            </el-form-item>

            <el-form-item label="发布状态:">
                <el-select v-model="formInline.region" placeholder="请选择" clearable>
                    <el-option label="已发布" value="已发布" />
                    <el-option label="草稿" value="草稿" />
                </el-select>
            </el-form-item>

            <el-form-item>
                <el-button type="primary" @click="onSubmit">搜索</el-button>
            </el-form-item>
            <el-form-item>
                <el-button type="default" @click="onSubmit">重置</el-button>
            </el-form-item>
        </el-form>
        <el-table :data="tableData" style="width: 100%">
            <el-table-column prop="title" label="文章标题" />
            <el-table-column prop="category" label="分类" />
            <el-table-column prop="time" label="发表时间" />
            <el-table-column prop="state" label="状态" />
            <el-table-column label="操作" width="180">
                <el-row>
                    <el-button type="primary" :icon="Edit" circle />
                    <el-button type="danger" :icon="Delete" circle />
                </el-row>
            </el-table-column>
        </el-table>


        <el-pagination class="el-p" v-model:current-page="currentPage4" v-model:page-size="pageSize4"
            :page-sizes="[5, 10, 15, 20]" :small="small" :disabled="disabled" :background="background"
            layout="jumper,total, sizes, prev, pager, next" :total="total" @size-change="handleSizeChange"
            @current-change="handleCurrentChange" />
    </el-card>
</template>

<style scoped>
.el-p {
    margin-top: 20px;
    display: flex;
    justify-content: flex-end;
}

.card-header {
    display: flex;
    justify-content: space-between;
}
</style>

4、大事件

需求在这里插入图片描述

1. 环境准备

1.创建Vue工程
npm init vue@latest
2. 安装依赖
Element-Plus
npm install element-plus --save
Axios
npm install axios
Sass
npm install sass -D
3. 目录调整
4. 删除components下面自动生成的内容
新建目录api、utils、views
将资料中的静态资源拷贝到assets目录下
删除App.uve中自动生成的内容

在这里插入图片描述
在这里插入图片描述
main.js

import './assets/main.scss'

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
import App from './App.vue'
import {createPinia} from 'pinia'
import { createPersistedState } from 'pinia-persistedstate-plugin'
import locale from 'element-plus/dist/locale/zh-cn.js'

const app = createApp(App);
const pinia = createPinia();
const persist = createPersistedState();
pinia.use(persist)
app.use(pinia)
app.use(router)
app.use(ElementPlus,{locale});
app.mount('#app')

2. 功能开发

注册登录

在这里插入图片描述
搭建页面
数据绑定:参考接口文档给属性起名
表单校验:
el-form标签上通过rules属性,绑定校验规则
el-form-item标签上通过prop属性,指定校验项

Login.vue

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)
//定义数据模型
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 rules = {
    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' }
    ]
}

//调用后台接口,完成注册
import { userRegisterService, userLoginService} from '@/api/user.js'
const register = async () => {
    //registerData是一个响应式对象,如果要获取值,需要.value
    let result = await userRegisterService(registerData.value);
    /* if (result.code === 0) {
        //成功了
        alert(result.msg ? result.msg : '注册成功');
    }else{
        //失败了
        alert('注册失败')
    } */
    //alert(result.msg ? result.msg : '注册成功');
    ElMessage.success(result.msg ? result.msg : '注册成功')
}

//绑定数据,复用注册表单的数据模型
//表单数据校验
//登录函数
import {useTokenStore} from '@/stores/token.js'
import {useRouter} from 'vue-router'
const router = useRouter()
const tokenStore = useTokenStore();
const login =async ()=>{
    //调用接口,完成登录
   let result =  await userLoginService(registerData.value);
   /* if(result.code===0){
    alert(result.msg? result.msg : '登录成功')
   }else{
    alert('登录失败')
   } */
   //alert(result.msg? result.msg : '登录成功')
   ElMessage.success(result.msg ? result.msg : '登录成功')
   //把得到的token存储到pinia中
   tokenStore.setToken(result.data)
   //跳转到首页 路由完成跳转
   router.push('/')
}

//定义函数,清空数据模型的数据
const clearRegisterData = ()=>{
    registerData.value={
        username:'',
        password:'',
        rePassword:''
    }
}
</script>

<template>
    <el-row class="login-page">
        <el-col :span="12" class="bg"></el-col>
        <el-col :span="6" :offset="3" class="form">
            <!-- 注册表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :model="registerData" :rules="rules">
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码"
                        v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item 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 @click="register">
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterData()">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else :model="registerData" :rules="rules">
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username">
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item class="flex">
                    <div class="flex">
                        <el-checkbox>记住我</el-checkbox>
                        <el-link type="primary" :underline="false">忘记密码?</el-link>
                    </div>
                </el-form-item>
                <!-- 登录按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true;clearRegisterData()">
                        注册 →
                    </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>

api/user.js

//导入request.js请求工具
import request from '@/utils/request.js'

//提供调用注册接口的函数
export const userRegisterService = (registerData)=>{
    //借助于UrlSearchParams完成传递
    const params = new URLSearchParams()
    for(let key in registerData){
        params.append(key,registerData[key]);
    }
    return request.post('/user/register',params);
}

//提供调用登录接口的函数
export const userLoginService = (loginData)=>{
    const params = new URLSearchParams();
    for(let key in loginData){
        params.append(key,loginData[key])
    }
    return request.post('/user/login',params)
}


//获取用户详细信息
export const userInfoService = ()=>{
    return request.get('/user/userInfo')
}

//修改个人信息
export const userInfoUpdateService = (userInfoData)=>{
   return request.put('/user/update',userInfoData)
}

//修改头像
export const userAvatarUpdateService = (avatarUrl)=>{
    const params = new URLSearchParams();
    params.append('avatarUrl',avatarUrl)
    return request.patch('/user/updateAvatar',params)
}

跨域
由于浏览器的同源策略限制,向不同源(不同协议、不同域名、不同端口)发送ajax请求会失败
在这里插入图片描述
优化axios响应拦截器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

request.js

//定制请求的实例

//导入axios  npm install axios
import axios from 'axios';

import { ElMessage } from 'element-plus'
//定义一个变量,记录公共的前缀  ,  baseURL
//const baseURL = 'http://localhost:8080';
const baseURL = '/api';
const instance = axios.create({ baseURL })

import {useTokenStore} from '@/stores/token.js'
//添加请求拦截器
instance.interceptors.request.use(
    (config)=>{
        //请求前的回调
        //添加token
        const tokenStore = useTokenStore();
        //判断有没有token
        if(tokenStore.token){
            config.headers.Authorization = tokenStore.token
        }
        return config;
    },
    (err)=>{
        //请求错误的回调
        Promise.reject(err)
    }
)

/* import {useRouter} from 'vue-router'
const router = useRouter(); */

import router from '@/router'
//添加响应拦截器
instance.interceptors.response.use(
    result => {
        //判断业务状态码
        if(result.data.code===0){
            return result.data;
        }

        //操作失败
        //alert(result.data.msg?result.data.msg:'服务异常')
        ElMessage.error(result.data.msg?result.data.msg:'服务异常')
        //异步操作的状态转换为失败
        return Promise.reject(result.data)
        
    },
    err => {
        //判断响应状态码,如果为401,则证明未登录,提示请登录,并跳转到登录页面
        if(err.response.status===401){
            ElMessage.error('请先登录')
            router.push('/login')
        }else{
            ElMessage.error('服务异常')
        }
       
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;

vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'

// 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替换为''
      }
    }
  }
})
主页面布局

在这里插入图片描述
Layout.vue

<script setup>
import {
    Management,
    Promotion,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'

import {userInfoService} from '@/api/user.js'
import useUserInfoStore from '@/stores/userInfo.js'
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
const userInfoStore = useUserInfoStore();
//调用函数,获取用户详细信息
const getUserInfo = async()=>{
    //调用接口
    let result = await userInfoService();
    //数据存储到pinia中
    userInfoStore.setInfo(result.data);
}

getUserInfo();
//条目被点击后,调用的函数
import {useRouter} from 'vue-router'
const router = useRouter();
import {ElMessage,ElMessageBox} from 'element-plus'
const handleCommand = (command)=>{
    //判断指令
    if(command === 'logout'){
        //退出登录
        ElMessageBox.confirm(
        '您确认要退出吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async () => {
            //退出登录
            //1.清空pinia中存储的token以及个人信息
            tokenStore.removeToken()
            userInfoStore.removeInfo()

            //2.跳转到登录页面
            router.push('/login')
            ElMessage({
                type: 'success',
                message: '退出登录成功',
            })
            
        })
        .catch(() => {
            ElMessage({
                type: 'info',
                message: '用户取消了退出登录',
            })
        })
    }else{
        //路由
        router.push('/user/'+command)
    }
}
</script>

<template>
    <!-- element-plus中的容器 -->
    <el-container class="layout-container">
        <!-- 左侧菜单 -->
        <el-aside width="200px">
            <div class="el-aside__logo"></div>
            <!-- element-plus的菜单标签 -->
            <el-menu active-text-color="#ffd04b" background-color="#232323"  text-color="#fff"
                router>
                <el-menu-item index="/article/category">
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>文章分类</span>
                </el-menu-item>
                <el-menu-item index="/article/manage">
                    <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 index="/user/info">
                        <el-icon>
                            <User />
                        </el-icon>
                        <span>基本资料</span>
                    </el-menu-item>
                    <el-menu-item index="/user/avatar">
                        <el-icon>
                            <Crop />
                        </el-icon>
                        <span>更换头像</span>
                    </el-menu-item>
                    <el-menu-item index="/user/resetPassword">
                        <el-icon>
                            <EditPen />
                        </el-icon>
                        <span>重置密码</span>
                    </el-menu-item>
                </el-sub-menu>
            </el-menu>
        </el-aside>
        <!-- 右侧主区域 -->
        <el-container>
            <!-- 头部区域 -->
            <el-header>
                <div>黑马程序员:<strong>{{ userInfoStore.info.nickname }}</strong></div>
                <!-- 下拉菜单 -->
                <!-- command: 条目被点击后会触发,在事件函数上可以声明一个参数,接收条目对应的指令 -->
                <el-dropdown placement="bottom-end" @command="handleCommand">
                    <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="resetPassword" :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> -->
                <router-view></router-view>
            </el-main>
            <!-- 底部区域 -->
            <el-footer>大事件 ©2023 Created by 黑马程序员</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 Router是Vue.js的官方路由
Vue Router
安装vue-router npm install vue-router@4
在src/router/index.js中创建路由器,并导出
在vue应用实例中使用vue-router
声明router-view标签,展示组件内容
在这里插入图片描述
App.vue
在这里插入图片描述
index.js
在这里插入图片描述

子路由

在这里插入图片描述
复制资料中提供好的五个组件
配置子路由
声明router-view标签
为菜单项 el-menu-item 设置index属性,设置点击后的路由路径
在这里插入图片描述

index.js

import { createRouter, createWebHistory } from 'vue-router'

//导入组件
import LoginVue from '@/views/Login.vue'
import LayoutVue from '@/views/Layout.vue'

import ArticleCategoryVue from '@/views/article/ArticleCategory.vue'
import ArticleManageVue from '@/views/article/ArticleManage.vue'
import UserAvatarVue from '@/views/user/UserAvatar.vue'
import UserInfoVue from '@/views/user/UserInfo.vue'
import UserResetPasswordVue from '@/views/user/UserResetPassword.vue'

//定义路由关系
const routes = [
    { path: '/login', component: LoginVue },
    {
        path: '/', component: LayoutVue,redirect:'/article/manage', children: [
            { path: '/article/category', component: ArticleCategoryVue },
            { path: '/article/manage', component: ArticleManageVue },
            { path: '/user/info', component: UserInfoVue },
            { path: '/user/avatar', component: UserAvatarVue },
            { path: '/user/resetPassword', component: UserResetPasswordVue }
        ]
    }
]

//创建路由器
const router = createRouter({
    history: createWebHistory(),
    routes: routes
})

//导出路由
export default router
文章分类

在这里插入图片描述
api/article.js

import 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}})
    return request.get('/category')
}

//文章分类添加
export const articleCategoryAddService = (categoryData)=>{
    return request.post('/category',categoryData)
}

//文章分类修改
export const articleCategoryUpdateService = (categoryData)=>{
   return  request.put('/category',categoryData)
}

//文章分类删除
export const articleCategoryDeleteService = (id)=>{
    return request.delete('/category?id='+id)
}

//文章列表查询
export const articleListService = (params)=>{
   return  request.get('/article',{params:params})
}

//文章添加
export const articleAddService = (articleData)=>{
    return request.post('/article',articleData);

}

AticleCategory.vue

<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": 4,
        "categoryName": "娱乐",
        "categoryAlias": "yl",
        "createTime": "2023-09-02 12:08:16",
        "updateTime": "2023-09-02 12:08:16"
    },
    {
        "id": 5,
        "categoryName": "军事",
        "categoryAlias": "js",
        "createTime": "2023-09-02 12:08:33",
        "updateTime": "2023-09-02 12:08:33"
    }
])
//声明一个异步的函数
import { articleCategoryListService, articleCategoryAddService, articleCategoryUpdateService,articleCategoryDeleteService } from '@/api/article.js'
const articleCategoryList = async () => {
    let result = await articleCategoryListService();
    categorys.value = result.data;

}
articleCategoryList();
//控制添加分类弹窗
const dialogVisible = ref(false)

//添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}


//调用接口,添加表单
import { ElMessage } from 'element-plus'
const addCategory = async () => {
    //调用接口
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.msg ? result.msg : '添加成功')

    //调用获取所有文章分类的函数
    articleCategoryList();
    dialogVisible.value = false;
}

//定义变量,控制标题的展示
const title = ref('')

//展示编辑弹窗
const showDialog = (row) => {
    dialogVisible.value = true; title.value = '编辑分类'
    //数据拷贝
    categoryModel.value.categoryName = row.categoryName;
    categoryModel.value.categoryAlias = row.categoryAlias;
    //扩展id属性,将来需要传递给后台,完成分类的修改
    categoryModel.value.id = row.id
}

//编辑分类
const updateCategory = async () => {
    //调用接口
    let result = await articleCategoryUpdateService(categoryModel.value);

    ElMessage.success(result.msg ? result.msg : '修改成功')

    //调用获取所有分类的函数
    articleCategoryList();

    //隐藏弹窗
    dialogVisible.value = false;
}

//清空模型的数据
const clearData = () => {
    categoryModel.value.categoryName = '';
    categoryModel.value.categoryAlias = '';
}

//删除分类
import {ElMessageBox} from 'element-plus'
const deleteCategory = (row) => {
    //提示用户  确认框

    ElMessageBox.confirm(
        '你确认要删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async () => {
            //调用接口
            let result = await articleCategoryDeleteService(row.id);
            ElMessage({
                type: 'success',
                message: '删除成功',
            })
            //刷新列表
            articleCategoryList();
        })
        .catch(() => {
            ElMessage({
                type: 'info',
                message: '用户取消了删除',
            })
        })
}
</script>
<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>文章分类</span>
                <div class="extra">
                    <el-button type="primary" @click="dialogVisible = true; title = '添加分类'; clearData()">添加分类</el-button>
                </div>
            </div>
        </template>
        <el-table :data="categorys" style="width: 100%">
            <el-table-column label="序号" width="100" type="index"> </el-table-column>
            <el-table-column label="分类名称" prop="categoryName"></el-table-column>
            <el-table-column label="分类别名" prop="categoryAlias"></el-table-column>
            <el-table-column label="操作" width="100">
                <template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="showDialog(row)"></el-button>
                    <el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>

        <!-- 添加分类弹窗 -->
        <el-dialog v-model="dialogVisible" :title="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" @click="title == '添加分类' ? addCategory() : updateCategory()"> 确认 </el-button>
                </span>
            </template>
        </el-dialog>
    </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>
Pinia状态管理库

Pinia是Vue的专属状态管理库,它允许你跨组件或页面共享状态
在这里插入图片描述
安装pinia npm install pinia
在vue应用实例中使用pinia
在src/stores/token.js中定义store
在组件中使用store
在这里插入图片描述
在这里插入图片描述

token.js
在这里插入图片描述
Axios请求拦截器
在这里插入图片描述
在这里插入图片描述
Pinia持久化插件-persist
Pinia默认是内存存储,当刷新浏览器的时候会丢失数据。
Persist插件可以将pinia中的数据持久化的存储

安装persist npm install pinia-persistedstate-plugin
在pinia中使用persist
定义状态Store时指定持久化配置参数

main.js
在这里插入图片描述
在这里插入图片描述
未登录统一处理
在这里插入图片描述

文章管理

添加文章分类
在这里插入图片描述
添加分类弹窗页面

<!-- 添加分类弹窗 -->
<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>

数据模型和校验规则

  //控制添加分类弹窗
    const dialogVisible = ref(false)
    
    //添加分类数据模型
    const categoryModel = ref({
        categoryName: '',
        categoryAlias: ''
    })
    //添加分类表单校验
    const rules = {
        categoryName: [
            { required: true, message: '请输入分类名称', trigger: 'blur' },
        ],
        categoryAlias: [
            { required: true, message: '请输入分类别名', trigger: 'blur' },
        ]
    }

添加分类按钮单击事件

 <el-button type="primary" @click="dialogVisible = true">添加分类</el-button>

接口调用

在article.js中提供添加分类的函数

//添加文章分类
export const articleCategoryAddService = (categoryModel) => {
    return request.post('/category', categoryModel)
}

在页面中调用接口

 //访问后台,添加文章分类
    const addCategory = async ()=>{
        let result = await articleCategoryAddService(categoryModel.value);
        ElMessage.success(result.message? result.message:'添加成功')
        //隐藏弹窗
        dialogVisible.value = false
        //再次访问后台接口,查询所有分类
        getAllCategory()
    }

    <el-button type="primary" @click="addCategory"> 确认 </el-button>

修改文章分类
在这里插入图片描述
修改分类弹窗页面

修改分类弹窗和新增文章分类弹窗长的一样,所以可以服用添加分类的弹窗

弹窗标题显示定义标题

//弹窗标题
const title=ref('')

在弹窗上绑定标题

 <el-dialog v-model="dialogVisible" :title="title" width="30%">

为添加分类按钮绑定事件

<el-button type="primary" @click="title='添加分类';dialogVisible = true">添加分类</el-button>

为修改分类按钮绑定事件

<el-button :icon="Edit" circle plain type="primary" @click="title='修改分类';dialogVisible=true"></el-button>

数据回显

当点击修改分类按钮时,需要把当前这一条数据的详细信息显示到修改分类的弹窗上,这个叫回显

通过插槽的方式得到被点击按钮所在行的数据

<template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="updateCategoryEcho(row)"></el-button>
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>

回显函数

 //修改分类回显
    const updateCategoryEcho = (row) => {
        title.value = '修改分类'
        dialogVisible.value = true
        //将row中的数据赋值给categoryModel
        categoryModel.value.categoryName=row.categoryName
        categoryModel.value.categoryAlias=row.categoryAlias
        //修改的时候必须传递分类的id,所以扩展一个id属性
        categoryModel.value.id=row.id
    }

接口调用

article.js中提供修改分类的函数

   //修改分类
    export const articleCategoryUpdateService = (categoryModel)=>{
        return request.put('/category',categoryModel)
    }

修改确定按钮的绑定事件

   <span class="dialog-footer">
                    <el-button @click="dialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="title==='添加分类'? addCategory():updateCategory()"> 确认 </el-button>
                </span>

调用接口完成修改的函数

//修改分类
const updateCategory=async ()=>{
    let result = await articleCategoryUpdateService(categoryModel.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //隐藏弹窗
    dialogVisible.value=false
    //再次访问后台接口,查询所有分类
    getAllCategory()
}

由于现在修改和新增共用了一个数据模型,所以在点击添加分类后,有时候会显示数据,此时可以将categoryModel中的数据清空

  //清空模型数据
    const clearCategoryModel = ()=>{
        categoryModel.value.categoryName='',
        categoryModel.value.categoryAlias=''
    }

修改添加按钮的点击事件

<el-button type="primary" @click="title = '添加分类'; dialogVisible = true;clearCategoryModel()">添加分类</el-button>

删除文章分类
在这里插入图片描述
确认框

//删除分类  给删除按钮绑定事件
const deleteCategory = (row) => {
    ElMessageBox.confirm(
        '你确认删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(() => {
            //用户点击了确认
            ElMessage({
                type: 'success',
                message: '删除成功',
            })
        })
        .catch(() => {
            //用户点击了取消
            ElMessage({
                type: 'info',
                message: '取消删除',
            })
        })
}

接口调用

article.js中提供删除分类的函数

   //删除分类
    export const articleCategoryDeleteService = (id) => {
        return request.delete('/category?id='+id)
    }

当用户点击确认后,调用接口删除分类

//删除分类
const deleteCategory = (row) => {
    ElMessageBox.confirm(
        '你确认删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async () => {
            //用户点击了确认
            let result = await articleCategoryDeleteService(row.id)
            ElMessage.success(result.message?result.message:'删除成功')
            //再次调用getAllCategory,获取所有文章分类
            getAllCategory()
        })
        .catch(() => {
            //用户点击了取消
            ElMessage({
                type: 'info',
                message: '取消删除',
            })
        })
}

文章列表查询
在这里插入图片描述
文章列表页面组件

  <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": 4,
            "categoryName": "娱乐",
            "categoryAlias": "yl",
            "createTime": "2023-09-02 12:08:16",
            "updateTime": "2023-09-02 12:08:16"
        },
        {
            "id": 5,
            "categoryName": "军事",
            "categoryAlias": "js",
            "createTime": "2023-09-02 12:08:33",
            "updateTime": "2023-09-02 12:08:33"
        }
    ])
    
    //用户搜索时选中的分类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"
        },
        {
            "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"
        },
        {
            "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">
                        <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">
                        <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中完成

 import locale from 'element-plus/dist/locale/zh-cn.js'
    
    app.use(ElementPlus,{locale})

文章分类数据回显

ArticleMange.vue

//文章列表查询
import { articleCategoryListService } from '@/api/article.js'
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
    categorys.value = resultC.data
}
getArticleCategoryList();

文章列表接口调用

article.js中提供获取文章列表数据的函数

//文章列表查询
export const articleListService = (params) => {
    return request.get('/article', { params: params })
}

ArticleManage.vue中,调用接口获取数据

 //文章列表查询
    import { articleListService } from '@/api/article.js'
    const getArticles = async () => {
        let params = {
            pageNum: pageNum.value,
            pageSize: pageSize.value,
            categoryId: categoryId.value ? categoryId.value : null,
            state: state.value ? state.value : null
        }
        let result = await articleListService(params);
        //渲染列表数据
        articles.value = result.data.items
        //为列表中添加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
                }
            }
        }
        //渲染总条数
        total.value=result.data.total
    }
    getArticles()

当分页条的当前页和每页条数发生变化,重新发送请求获取数据

//当每页条数发生了变化,调用此函数
const onSizeChange = (size) => {
    pageSize.value = size
    getArticles()
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    getArticles()
}

搜索和重置

为搜索按钮绑定单击事件,调用getArticles函数即可

<el-button type="primary" @click="getArticles">搜索</el-button>

为重置按钮绑定单击事件,清除categoryId和state的之即可

 <el-button @click="categoryId='';state=''">重置</el-button>

添加文章
在这里插入图片描述
添加文章抽屉组件

import {Plus} from '@element-plus/icons-vue'
//控制抽屉是否显示
const visibleDrawer = ref(false)
//添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

<!-- 抽屉 -->
        <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>

/* 抽屉样式 */
.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: #8c939d;
            width: 178px;
            height: 178px;
            text-align: center;
        }
    }
}
.editor {
  width: 100%;
  :deep(.ql-editor) {
    min-height: 200px;
  }
}

为添加文章按钮添加单击事件,展示抽屉

<el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>

富文本编辑器

文章内容需要使用到富文本编辑器,这里咱们使用一个开源的富文本编辑器 Quill

官网地址: https://vueup.github.io/vue-quill/

安装:

npm install @vueup/vue-quill@latest --save

导入组件和样式:

  import { QuillEditor } from '@vueup/vue-quill'
    import '@vueup/vue-quill/dist/vue-quill.snow.css'

页面长使用quill组件:

<quill-editor
              theme="snow"
              v-model:content="articleModel.content"
              contentType="html"
              >
</quill-editor>

样式美化:

 .editor {
      width: 100%;
      :deep(.ql-editor) {
        min-height: 200px;
      }
    }

文章封面图片上传

将来当点击+图标,选择本地图片后,el-upload这个组件会自动发送请求,把图片上传到指定的服务器上,而不需要我们自己使用axios发送异步请求,所以需要给el-upload标签添加一些属性,控制请求的发送

auto-upload:是否自动上传

action: 服务器接口路径

name: 上传的文件字段名

headers: 设置上传的请求头

on-success: 上传成功的回调函数

  import {
        Plus
    } from '@element-plus/icons-vue'
    
    <el-form-item label="文章封面">
        <el-upload class="avatar-uploader" 
                   :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>

注意:

  1. 由于这个请求时el-upload自动发送的异步请求,并没有使用咱们的request.js请求工具,所以在请求的路ing上,需要加上/api, 这个时候请求代理才能拦截到这个请求,转发到后台服务器上

  2. 要携带请求头,还需要导入pinia状态才可以使用

  import { useTokenStore } from '@/stores/token.js'
  const tokenStore = useTokenStore();
  1. 在成功的回调函数中,可以拿到服务器响应的数据,其中有一个属性为data,对应的就是图片在阿里云oss上存储的访问地址,需要把它赋值给articleModel的coverImg属性,这样img标签就能显示这张图片了,因为img标签上通过src属性绑定了articleModel.coverImg
   //上传图片成功回调
      const uploadSuccess = (img) => {
          //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
          articleModel.value.coverImg=img.data
      }
  

添加文章接口调用

article.js中提供添加文章函数

//添加文章
export const articleAddService = (articleModel)=>{
    return request.post('/article',articleModel)
}

为已发布和草稿按钮绑定事件

<el-form-item>
    <el-button type="primary" @click="addArticle('已发布')">发布</el-button>
    <el-button type="info" @click="addArticle('草稿')">草稿</el-button>
</el-form-item>

ArticleManage.vue中提供addArticle函数完成添加文章接口的调用

 //添加文章
    const addArticle=async (state)=>{
        articleModel.value.state = state
        let result = await articleAddService(articleModel.value);
        ElMessage.success(result.message? result.message:'添加成功')
        //再次调用getArticles,获取文章
        getArticles()
        //隐藏抽屉
        visibleDrawer.value=false
    }

顶部导航栏信息显示
在这里插入图片描述
在Layout.vue中,页面加载完就发送请求,获取个人信息展示,并存储到pinia中,因为将来在个人中心中修改信息的时候还需要使用

user.js中提供获取个人信息的函数

//获取个人信息
export const userInfoGetService = ()=>{
    return request.get('/user/userInfo');
}

src/stores/user.js中,定义个人中心状态

 import { defineStore } from "pinia"
    import {ref} from 'vue'
    
    export const useUserInfoStore = defineStore('userInfo',()=>{
        //1.定义用户信息
        const info = ref({})
        //2.定义修改用户信息的方法
        const setInfo = (newInfo)=>{
            info.value = newInfo
        }
        //3.定义清空用户信息的方法
        const removeInfo = ()=>{
            info.value={}
        }
    
        return{info,setInfo,removeInfo}
    },{
        persist:true
    })

Layout.vue中获取个人信息,并存储到pinia中

   //导入接口函数
    import {userInfoGetService} from '@/api/user.js'
    //导入pinia
    import {useUserInfoStore} from '@/stores/user.js'
    const userInfoStore = useUserInfoStore();
    import {ref} from 'vue'
    
    //获取个人信息
    const getUserInf = async ()=>{
        let result = await userInfoGetService();
        //存储pinia
        userInfoStore.info =result.data;
    }
    getUserInf()

Layout.vue的顶部导航栏中,展示昵称和头像

<div>黑马程序员:<strong>{{ userInfoStore.info.nickname ? userInfoStore.info.nickname : userInfoStore.info.usrename }}</strong></div>



<el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" />

下拉菜单功能
在这里插入图片描述
el-dropdown中功能实现

在el-dropdown中有四个子条目,分别是:

  • 基本资料
  • 更换头像
  • 重置密码
  • 退出登录

其中其三个起到路由功能,跟左侧菜单中【个人中心】下面的二级菜单是同样的功能,退出登录需要删除本地pinia中存储的token以及userInfo

路由实现:

在el-dropdown-item标签上添加command属性,属性值和路由表中/user/xxx保持一致

<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="password" :icon="EditPen">重置密码</el-dropdown-item>
    <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>

在el-dropdown标签上绑定command事件,当有条目被点击后,会触发这个事件

<el-dropdown placement="bottom-end" @command="handleCommand">

提供handleCommand函数,参数为点击条目的command属性值

 //dropDown条目被点击后,回调的函数
    import {useRouter} from 'vue-router'
    const router = useRouter()
    const handleCommand = (command)=>{
        if(command==='logout'){
            //退出登录
            alert('退出登录')
        }else{
            //路由
            router.push('/user/'+command)
        }
    }

退出登录实现:

  import {ElMessage,ElMessageBox} from 'element-plus'
    import { useTokenStore } from '@/stores/token.js'
    const tokenStore = useTokenStore()
    const handleCommand = (command) => {
        if (command === 'logout') {
            //退出登录
            ElMessageBox.confirm(
                '你确认退出登录码?',
                '温馨提示',
                {
                    confirmButtonText: '确认',
                    cancelButtonText: '取消',
                    type: 'warning',
                }
            )
                .then(async () => {
                    //用户点击了确认
                    //清空pinia中的token和个人信息
                    userInfoStore.info={}
                    tokenStore.token=''
                    //跳转到登录页
                    router.push('/login')
                })
                .catch(() => {
                    //用户点击了取消
                    ElMessage({
                        type: 'info',
                        message: '取消退出',
                    })
                })
        } else {
            //路由
            router.push('/user/' + command)
        }
    }
个人中心

基本资料修改
在这里插入图片描述
基本资料页面组件

 <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中,只需要从pinia中获取个人信息,替换模板数据即可

   import { useUserInfoStore } from '@/stores/user.js';
    const userInfoStore = useUserInfoStore()
    const userInfo = ref({...userInfoStore.info})

接口调用

在src/api/user.js中提供修改基本资料的函数

//修改个人信息
export const userInfoUpdateService = (userInfo)=>{
    return request.put('/user/update',userInfo)
}

为修改按钮绑定单击事件

  <el-button type="primary" @click="updateUserInfo">提交修改</el-button>

提供updateUserInfo函数

  //修改用户信息
    import {userInfoUpdateService} from '@/api/user.js'
    import { ElMessage } from 'element-plus';
    const updateUserInfo = async ()=>{
        let result = await userInfoUpdateService(userInfo.value)
        ElMessage.success(result.message? result.message:'修改成功')
        //更新pinia中的数据
        userInfoStore.info.nickname=userInfo.value.nickname
        userInfoStore.info.email = userInfo.value.email
    }

用户头像修改
在这里插入图片描述
修改头像页面组件

<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中读取用户的头像数据

 //读取用户信息
    import {ref} from 'vue'
    import {useUserInfoStore} from '@/stores/user.js'
    const userInfoStore = useUserInfoStore()
    const imgUrl=ref(userInfoStore.info.userPic)

img标签上绑定图片地址

<img v-if="imgUrl" :src="imgUrl" class="avatar" />
<img v-else src="@/assets/avatar.jpg" width="278" />

头像上传

为el-upload指定属性值,分别有:

  • ​ action: 服务器接口路径
  • ​ headers: 设置请求头,需要携带token
  • ​ on-success: 上传成功的回调函数
  • ​ name: 上传图片的字段名称
<el-upload 
           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="@/assets/avatar.jpg" width="278" />
</el-upload>

提供上传成功的回调函数

  //读取token信息
    import {useTokenStore} from '@/stores/token.js'
    const tokenStore = useTokenStore()
    
    //图片上传成功的回调
    const uploadSuccess = (result)=>{
        //回显图片
        imgUrl.value = result.data
    }

外部触发图片选择

​ 需要获取到el-upload组件,然后再通过$el.querySelector(‘input’)获取到el-upload对应的元素,触发click事件

   //获取el-upload元素
    const uploadRef = ref()
    
    
    <el-button type="primary" :icon="Plus" size="large"  @click="uploadRef.$el.querySelector('input').click()">
        选择图片
    </el-button>

接口调用

在user.js中提供修改头像的函数

//修改头像
export const userAvatarUpdateService=(avatarUrl)=>{
    let params = new URLSearchParams();
    params.append('avatarUrl',avatarUrl)
    return request.patch('/user/updateAvatar',params)
}

为【上传头像】按钮绑定单击事件

<el-button type="success" :icon="Upload" size="large" @click="updateAvatar">
    上传头像
</el-button>

提供updateAvatar函数,完成头像更新

//调用接口,更新头像url
import {userAvatarUpdateService} from '@/api/user.js'
import {ElMessage} from 'element-plus'
const updateAvatar = async ()=>{
    let result = await userAvatarUpdateService(imgUrl.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //更新pinia中的数据
    userInfoStore.info.userPic=imgUrl.value
}
  
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值