文章目录
一、基础篇
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、校验时指定要校验的分组
{
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>
<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、环境准备
- 选择安装目录
选择安装到一个,没有中文,没有空格的目录下(新建一个文件夹NodeJS)
- 验证NodeJS环境变量
NodeJS 安装完毕后,会自动配置好环境变量,我们验证一下是否安装成功,通过: node -v
- 配置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>
注意:
-
由于这个请求时el-upload自动发送的异步请求,并没有使用咱们的request.js请求工具,所以在请求的路ing上,需要加上/api, 这个时候请求代理才能拦截到这个请求,转发到后台服务器上
-
要携带请求头,还需要导入pinia状态才可以使用
import { useTokenStore } from '@/stores/token.js'
const tokenStore = useTokenStore();
- 在成功的回调函数中,可以拿到服务器响应的数据,其中有一个属性为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
}