以下内容都是本人在听课时整理的,不是黑马官方的教程
环境搭建
-
准备数据表
-- 创建数据库 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驱动)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.3</version> </parent> <groupId>org.jjq</groupId> <artifactId>big-event</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>big-event</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- web依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mybatis依赖--> <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> <version>8.0.33</version> </dependency> </dependencies> </project>
-
配置文件application.yml中引入mybatis的配置信息
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/big_event username: root password: jinjiaqi123
-
创建包结构并准备实体类
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;//更新时间
}
public class Category {
private Integer id;//主键ID
private String categoryName;//分类名称
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
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;//更新时间
}
开发流程
- 明确需求
- 阅读接口文档
- 思路分析
- 开发
- 测试
用户相关
1、注册
查看接口文档
编写Result实体类
package org.jjq.pojo;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
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);
}
}
编写Controller
package org.jjq.controller;
import org.jjq.pojo.Result;
import org.jjq.pojo.User;
import org.jjq.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/register")
public Result register(String username,String password){
//查询用户
User u = userService.findByUserName(username);
//判断是否存在用户
if (u==null){
//没有占用
//注册
userService.register(username,password);
return Result.success();
}
else {
//占用
return Result.error("用户名已被占用");
}
}
}
编写UserService
package org.jjq.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.jjq.pojo.User;
@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实现层
package org.jjq.service.Impl;
import org.jjq.mapper.UserMapper;
import org.jjq.pojo.User;
import org.jjq.service.UserService;
import org.jjq.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@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);
}
}
编写UserMapper
package org.jjq.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.jjq.pojo.User;
@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);
}
运行SpringBoot并在postman中进行测试
测试成功数据库中出现测试的数据就表示用户查询和注册功能已经完成了
参数校验
传统方法
@RequestMapping("/register")
public Result register(String username,String password){
if(username!=null&&username.length()>=5&&username.length()<=16&&
password!=null&&password.length()>=5&&password.length()<=16)
{
//查询用户
User u = userService.findByUserName(username);
//判断是否存在用户
if (u==null){
//没有占用
//注册
userService.register(username,password);
return Result.success();
}
else {
//占用
return Result.error("用户名已被占用");
}
}
else {
return Result.error("参数不合法");
}
}
Spring Validation
Spring提供的一个参数校验框架,使用预定义的注解完成参数校验
1、引入Spring Validation依赖
<!--validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、在参数前面添加@Pattern注解
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password){
}
3、在Controller类上添加@Validation注解
@Validated
@RestController
@RequestMapping("/user")
public class UserController {
}
参数校验失败异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage())? e.getMessage():"操作失败");
}
}
2、登录
2.1、登录功能
编写Controller
@RequestMapping("/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())){
return Result.success("JWT token令牌");
}
return Result.error("密码错误");
}
编写异常处理器
package org.jjq.exception;
import org.jjq.pojo.Result;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage())? e.getMessage():"操作失败");
}
}
在postman中测试
{
"code": 0,
"message": "操作成功",
"data": "JWT token令牌"
}
2.2、登录认证
**令牌:**令牌就是一段字符串
- 承载业务数据,减少后续请求查询数据库的次数
- 防篡改,保证信息的合法性和有效性
JWT:
简介·:
- 全称:JSON Web Token(https://jwt.io/)
- 定义了一种简介的、自包含的格式,用于通信双方以json数据格式安全的传输信息
- 组成
- 第一部分:Header(头),记录令牌类型、签名算法等。例如:{“alg”:HS256,“type”:“JWT”}
- 第二部分:Payload(有效载荷),携带一些自定义信息,默认信息等。例如:{“id”:1,“username”:“Tom”}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来
JWT-生成:
在Maven中引入JWT
<!--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>
注意事项:
- JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的
- 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法
导入工具类JwtUtil
package org.jjq.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static final String KEY = "iloveyou";
//接收业务数据,生成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();
}
}
在Controller文件夹下创建ArticleController
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/list")
public Result<String> list(){
//验证token
return Result.success("所有文章的数据");
}
}
创建拦截器LoginInterceptor
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try{
Map<String, Object> claims = JwtUtil.parseToken(token);
//放行
return true;
}catch (Exception e){
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
}
创建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");
}
}
测试:
在postman的请求中添加Authorization将用户登录生成的token输到postman的Authorization中
取消Authorization后显示401状态码
3、获取用户详细信息
3.1、实现功能
在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");
User user = userService.findByUserName(username);
return Result.success(user);
}
测试:在postman中添加Authorization
成功!
{
"code": 0,
"message": "操作成功",
"data": {
"id": 2,
"username": "jinjiaqi",
"password": "e10adc3949ba59abbe56e057f20f883e",
"nickname": "",
"email": "",
"userPic": null,
"createTime": null,
"updateTime": null
}
}
统一配置请求头
让springmvc把当前对象转换成json字符串的时候,忽略password,最终的json字符串中就没有password这个属性了
@JsonIgnore
private String password;
返回的json数据已经没有password了
{
"code": 0,
"message": "操作成功",
"data": {
"id": 2,
"username": "jinjiaqi",
"nickname": "",
"email": "",
"userPic": null,
"createTime": null,
"updateTime": null
}
数据库中crete_time和update_time有值而返回的json数据没有,是因为我们在User实体类中的命名使用了驼峰命名而数据库中使用了下划线,我们需要配置application.yml来解决这个问题
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换
3.2、利用ThreadLocal优化
ThreadLocal:提供线程局部安全
- 用来存取数据:set()/get()
- 使用ThreadLocal存储的数据,线程安全
- 用完记得调用remove方法释放
导入工具类ThreadLocalUtil
package org.jjq.utils;
import java.util.HashMap;
import java.util.Map;
/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
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();
}
}
修改LoginInterceptor
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try{
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储在ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
}catch (Exception e){
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
优化UserController中的代码
@GetMapping("/userInfo")
public Result<User> userInfo(){
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User user = userService.findByUserName(username);
return Result.success(user);
}
使用ThreadLocal来优化代码,通过将数据存储在ThreadLocal中实现线程隔离,避免了重复代码的出现。
使用ThreadLocal优化获取用户详细新信息代码的方法,以及需要注意内存泄漏的问题
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
4、更新用户基本信息
4.1、参数校验
UserController
@PutMapping("/update")
public Result update(@RequestBody 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);
在postman中测试:
4.2、参数校验
- 实体类的成员变量上添加注解
- @NotNull
- @NotEmpty
- 接口方法的实体参数上添加@Validated注解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@NotNull
private Integer id;//主键ID
private String username;
@JsonIgnore
private String password;
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
userService.update(user);
return Result.success();
}
5、更新用户头像
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);
)
6、更新用户密码
UserController
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> params){
//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);
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,Integer id);
测试:
文章分类
1、新增文章分类
- 使用validation完成参数校验
- 在service层需要为Category的createUser、createtime、updateTime属性赋值
Category
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Category {
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
CategoryController
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@PostMapping
public Result add(@RequestBody @Validated Category category){
categoryService.add(category);
return Result.success();
}
}
CategoryService
//添加文章
void add(Category category);
CategoryServiceImpl
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public void add(Category category) {
//补充属性值
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
Map<String,Object> map = ThreadLocalUtil.get();
Integer userId = (Integer) map.get("id");
category.setCreateUser(userId);
categoryMapper.add(category);
}
}
CategoryMapper
@Mapper
public interface 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);
}
2、文章分类列表
CategoryController
@GetMapping
public Result<List<Category>> list(){
List<Category> cs = categoryService.list();
return Result.success(cs);
}
CategoryService
//查询文章分类列表
List<Category> list();
CategoryServiceImpl
@Override
public List<Category> list() {
Map<String,Object> map = ThreadLocalUtil.get();
Integer userId = (Integer) map.get("id");
return categoryMapper.list(userId);
}
CategoryMapper
//查询所有
@Select("select * from category where create_user=#{userId}")
List<Category> list(Integer userId);
3、获取文章分类详情
CatetoryController
@GetMapping("/detail")
public Result<Category> detail(Integer id){
Category c =categoryService.findById(id);
return Result.success(c);
}
CatetoryService
//获取文章详情
Category findById(Integer id);
CateServiceImpl
@Override
public Category findById(Integer id) {
Category c = categoryMapper.findById(id);
return c;
}
CategoryMapper
//获取文章详细信息
@Select("select * from category where id=#{id}")
Category findById(Integer id);
4、更新文章分类和删除文章分类
CategoryController
@PutMapping
public Result update(@RequestBody @Validated Category category){
categoryService.update(category);
return Result.success();
}
@DeleteMapping
public Result delete(Integer id){
categoryService.delete(id);
return Result.success();
}
CategoryService
//更新分类
void update(Category category);
//删除分类
void delete(Integer id);
CategoryServiceImpl
@Override
public void update(Category category) {
category.setUpdateTime(LocalDateTime.now());
categoryMapper.update(category);
}
@Override
public void delete(Integer id) {
categoryMapper.delete(id);
}
CategoryMapper
//更新分类
@Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id=#{id}")
void update(Category category);
//删除分类
@Delete("DELETE FROM category WHERE id=#{id}")
void delete(Integer id);
5、分组校验
把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项
- 定义分组
- 定义校验项时指定归属的分组
- 校验时指定要校验的分组
注意:定义校验项时如果没有指定分组,则属于Default分组,分组可以继承
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Category {
@NotNull(groups = Update.class)
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
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 extends B,那么A中拥有B中所有的校验项
public interface Add extends Default {
}
public interface Update extends Default{
}
@PostMapping
public Result add(@RequestBody @Validated(Category.Add.class) Category category){
categoryService.add(category);
return Result.success();
}
@PutMapping
public Result update(@RequestBody @Validated(Category.Update.class) Category category){
categoryService.update(category);
return Result.success();
}
文章管理
1、新增文章
ArticleController
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
@PostMapping
public Result add(@RequestBody Article article){
articleService.add(article);
return Result.success();
}
}
ArticleService
public interface ArticleService {
//添加文章
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);
}
ArticleMapper
@Mapper
public interface 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);
}
2、参数校验
自定义校验
已有的注解不能满足所有的校验要求,特殊的情况需要自定义校验(自定义校验注解)
- 自定义注解State
- message 、groups、payload
- 自定义校验数据的类StateValidation实现ConstraintValidation接口
- 在需要校验的地方使用自定义注解
修改Article实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
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;//更新时间
}
创建anno文件夹下创建State接口
@Documented //元注解
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {StateValidation.class}) //指定提供校验规则的类
@Target({ ElementType.FIELD})
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 s 将来要校验的数据
* @param constraintValidatorContext
* @return 如果返回false,则校验不通过,如果返回true,则校验通过
*/
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
//提供校验规则
if (s == null){
return false;
}
if (s.equals("已发布") || s.equals("草稿")){
return true;
}
return false;
}
}
在postman测试,如果在State下不填写已发布或者草稿,数据将无法传输到数据库
3、文章列表(条件分页 )
编写PageBean实体类
package org.jjq.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
//分页返回结果对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean <T>{
private Long total;//总条数
private List<T> items;//当前页数据集合
}
在Maven中导入坐标
<!--PageHelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
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);
ArtivleServiceImpl
@Override
public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
//1.创建PageBean对象
PageBean<Article> pb = new PageBean<>();
//2.开启分页查询PageHelper
PageHelper.startPage(pageNum,pageSize);
//3.调用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;
}
ArticleMapper
//条件分页
List<Article> list(Integer userId, Integer categoryId, String state);
ArticleMapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jjq.mapper.ArticleMapper">
<!--动态SQL-->
<select id="list" resultType="org.jjq.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>
测试:
文件上传
FileUploadController
@RestController
public class FileUploadController {
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
//把文件的内容存储到本地磁盘上
String originalFilename = file.getOriginalFilename();
//保证文件的名字是唯一的,从而防止文化覆盖
String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
file.transferTo(new File("C:\\Users\\13478\\Desktop\\files\\"+filename));
return Result.success("url访问地址。。。");
}
}
上传文件:文件夹中生成一个UUID的文件
阿里云OSS使用步骤
- 注册登录(实名认证)
- 充值
- 开通对象存储服务(OSS)
- 创建bucket
- 获取AccessKey(秘钥)
- 参照官方SDK编写入门程序
- 案例集成OSS
Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间
SDK:Software Decelopment Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码实例,都可以叫做SDK
在Maven中导入JavaSDK
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
导入并修改AliOssUtil
package org.jjq.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.InputStream;
public class AliOssUtil {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
private static final String ENDPOINT = "=";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
// EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
private static final String ACCESS_KEY_ID="";
private static final String ACCESS_KEY_SECRET="";
// 填写Bucket名称,例如examplebucket。
private static final String BUCKET_NAME = "";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
public static String uploadFile(String objectName, InputStream in) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID,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名称,区域节点/objectName
url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objectName;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
}
导入这个工具类后上传文件Controller修改为
@RestController
public class FileUploadController {
@PostMapping("/upload")
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("C:\\Users\\13478\\Desktop\\files\\"+filename));
String url = AliOssUtil.uploadFile(filename,file.getInputStream());
return Result.success(url);
}
}
在postman中测试可以返回阿里云中的url地址
登录优化-Redis
令牌主动失效机制
- 登录成功后,给浏览器响应令牌的同时,把该令牌存储在redis中
- LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
- 当用户修改密码成功后,删除redis中存储的旧令牌
SpringBoot集成Redis
- 导入spring-boot-starter-data-redis起步依赖
- 在yml配置文件中,配置redis连接信息
- 调用API(StringRedisTemplate)完成字符串的存取操作
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml
data:
redis:
host: localhost
port: 6379
创建测试类RedisTest
@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");
}
}
下载并开启redis服务
验证数据是否传入到redis中,在测试类中继续编写
@Test
public void testGet(){
ValueOperations<String,String> operations = stringRedisTemplate.opsForValue();
System.out.println(operations.get("username"));
}
输出username的字符串代表数据成功传入到redis
令牌主动失效
第一步:修改UserController
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password){
......
}
//判断密码是否正确,loginUser对象中的的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())){
......
//把token存储到redis中
ValueOperations<String,String> operations = stringRedisTemplate.opsForValue();
operations.set(token,token,1, TimeUnit.HOURS);
return Result.success(token);
}
return Result.error("密码错误");
}
第二步:修改LoginInterceptor
@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;
}
}
第三步:修改UserController中的登录和更新密码部分代码
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/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("密码错误");
}
@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();
}
第四步:在postman中测试
- 登录后复制token到pre-request修改之前的token
- 修改密码
- 修改密码后测试能否获取到用户详细信息,如果是401则表示token在修改密码之后就被删除了
SpringBoot项目部署
在Maven中引入打包插件
<build>
<plugins>
<!-- 打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.3</version>
</plugin>
</plugins>
</build>
在Maven声明周期中点击package生成的target文件夹中的jar包就是我们打包的文件
注意保持Redis开启
配置优先级
- 项目中resources目录下的application.yml
- jar包所在目录下的application.yml
- 操作系统环境变量
- 命令行参数
多环境开发-Profiles
SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定的环境下指定部分配置生效
多环境开发-Profiles-分组
- 按照配置的类别,把配置信息配置到不同的配置文件中
- application.yml
- 在application.yml中定义分组
- spring-profiles.group
- 在application.yml中激活分组
- spring-profiles.active
前端开发
ElementUI+Vue3
创建一个Vue3初始项目
API风格
-
Vue的组件有两种不同的风格:组合式API和选项式API
<script setup> import {ref,onMounted} from 'vue' //声明响应式数据 const count = ref(0);//在组合式api中,一般需要把数据定义为响应式数据 //声明函数 function increment() { count.value++; } //声明钩子函数 onMounted onMounted(() => { console.log('vue 已经挂载') }); </script> <template> <!-- 写html元素 --> <button @click="increment">count:{{ count }}</button> </template>
- setup:是一种标识,告诉Vue需要进行一些处理,让我们可以更简洁的使用组合式API
- ref():接收一个内部值,返回一个响应式ref对象,此对象只有一个指向内部值的属性value
- onMounted():在组合式API中的钩子方法,注册一个回调函数,在组件挂载完成后执行
Element-plus-快速入门
安装依赖element-plus、axios、sass
npm install element-plus --save
npm install axios
npm install sass -D
在main.js中
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import locale from 'element-plus/dist/locale/zh-cn.js'
const app = createApp(App)
app.use(ElementPlus,{locale})
app.mount('#app')
App.vue
<!-- 根组件 -->
<script setup>
import ButtonVue from './Buttton.vue'
import ArticleVue from "./Article.vue";
</script>
<template>
<!-- <ButtonVue/> -->
<ArticleVue/>
</template>
<style scoped>
</style>
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'
import type { ComponentSize } from 'element-plus'
const currentPage4 = ref(4)
const pageSize4 = ref(5)
const size = ref<ComponentSize>('default')
const background = ref(false)
const disabled = ref(false)
const handleSizeChange = (val: number) => {
console.log(`${val} items per page`)
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
}
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
} 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:'已发布',
},
]
</script>
<template>
<el-card>
<div class="card-header">
<span>文章管理</span>
<el-button type="primary">发布文章</el-button>
</div>
<div style="margin-top: 15px;">
<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">
<div>
<el-button type="primary" :icon="Edit" circle />
<el-button type="danger" :icon="Delete" circle />
</div>
</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]"
:size="size"
:disabled="disabled"
:background="background"
layout="jumper,total,sizes, prev, pager, next"
:total="20"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
</template>
<style scoped>
.el-p{
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.demo-form-inline .el-input {
--el-input-width: 220px;
}
.demo-form-inline .el-select {
--el-select-width: 220px;
}
.card-header{
display: flex;
justify-content: space-between;
}
</style>
实现效果
目录调整
- 删除components下面自动生成的内容
- 新建目录api,utils,views
- 将资料中的静态资源拷贝到assets目录下
- 删除App.vue中自动生成的内容
注册-页面搭建
Login.vue
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
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'}
]
}
</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>
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
<!-- 登录表单 -->
<el-form ref="form" size="large" autocomplete="off" v-else>
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item>
<el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item class="flex">
<div class="flex">
<el-checkbox>记住我</el-checkbox>
<el-link type="primary" :underline="false">忘记密码?</el-link>
</div>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space>登录</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true">
注册 →
</el-link>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
/* 样式 */
.login-page {
height: 100vh;
background-color: #fff;
.bg {
background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
url('@/assets/login_bg.jpg') no-repeat center / cover;
border-radius: 0 20px 20px 0;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
.title {
margin: 0 auto;
}
.button {
width: 100%;
}
.flex {
width: 100%;
display: flex;
justify-content: space-between;
}
}
}
</style>
实现效果
注册-接口调用
启动项目和redis
修改Login.vue中的代码并创建user.js
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
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 } from "@/api/user";
const register = async () => {
//registerData是一个响应式对象,如果要获取值,需要.value
let result =await userRegisterService(registerData.value);
if(result.code === 0){
//success
alert(result.message? result.message:'注册成功');
}else{
//fall
alert('注册失败')
}
}
</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">
← 返回
</el-link>
</el-form-item>
</el-form>
//导入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);
}
在页面中测试注册功能会弹出服务器异常,这是跨域问题
跨域问题
由于浏览器的同源策略限制,向不同源(不同协议、不同域名、不同端口)发送ajax请求会失败
将导入request.js中的baseURL修改
//定制请求的实例
//导入axios npm install axios
import axios from 'axios';
//定义一个变量,记录公共的前缀 , baseURL
const baseURL = '/api';
const instance = axios.create({baseURL})
//添加响应拦截器
instance.interceptors.response.use(
result=>{
return result.data;
},
err=>{
alert('服务异常');
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'
// 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替换为''
}
}
}
})
登录-绑定数据
修改Login.vue和user.js
<el-form ref="form" size="large" autocomplete="off" v-else: v-model="registerData" :rules="rules">
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item>
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
</el-form-item>
<el-form-item>
<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">
注册 →
</el-link>
</el-form-item>
</el-form>
//提供调用登录接口的函数
export const userLoginService = (loginData)=>{
const params = new URLSearchParams();
for(let key in loginData){
params.append(key,loginData[key])
}
return request.post('/user/login',params)
}
在用户点击登录或注册清除在之前页面的数据
//定义函数,清空数据模型的数据
const ClearRegisterData = ()=>{
registerData.value={
username:'',
password:'',
rePassword:''
}
}
<el-link type="info" :underline="false" @click="isRegister = false;ClearRegisterData()">
← 返回
</el-link>
<el-link type="info" :underline="false" @click="isRegister = true;ClearRegisterData()">
注册 →
</el-link>
优化axios响应拦截器
修改Login.vue
import { ElMessage } from 'element-plus';
//调用后台接口,完成注册
import { userRegisterService,userLoginService} from "@/api/user";
const register = async () => {
//registerData是一个响应式对象,如果要获取值,需要.value
let result =await userRegisterService(registerData.value);
// if(result.code === 0){
// //success
// alert(result.message? result.message:'注册成功');
// }else{
// //fall
// alert('注册失败')
// }
ElMessage.success(result.message? result.message:'注册成功')
}
const login =async()=>{
//调用接口,完成登录
let result = await userLoginService(registerData.value);
// if(result.code===0){
// alert(result.message? result.message:'登录成功')
// }else{
// alert('登录失败')
// }
ElMessage.success(result.message? result.message:'登录成功')
}
修改request.js
import { ElMessage } from 'element-plus';
//添加响应拦截器
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=>{
alert('服务异常');
return Promise.reject(err);//异步的状态转化成失败的状态
}
)
主界面-页面搭建
<script setup>
import {
Management,
Promotion,
UserFilled,
User,
Crop,
EditPen,
SwitchButton,
CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside width="200px">
<div class="el-aside__logo"></div>
<el-menu active-text-color="#ffd04b" background-color="#232323" text-color="#fff"
router>
<el-menu-item >
<el-icon>
<Management />
</el-icon>
<span>文章分类</span>
</el-menu-item>
<el-menu-item >
<el-icon>
<Promotion />
</el-icon>
<span>文章管理</span>
</el-menu-item>
<el-sub-menu >
<template #title>
<el-icon>
<UserFilled />
</el-icon>
<span>个人中心</span>
</template>
<el-menu-item >
<el-icon>
<User />
</el-icon>
<span>基本资料</span>
</el-menu-item>
<el-menu-item >
<el-icon>
<Crop />
</el-icon>
<span>更换头像</span>
</el-menu-item>
<el-menu-item >
<el-icon>
<EditPen />
</el-icon>
<span>重置密码</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 右侧主区域 -->
<el-container>
<!-- 头部区域 -->
<el-header>
<div>黑马程序员:<strong>东哥</strong></div>
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box">
<el-avatar :src="avatar" />
<el-icon>
<CaretBottom />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
<el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<!-- 中间区域 -->
<el-main>
<div style="width: 1290px; height: 570px;border: 1px solid red;">
内容展示区
</div>
</el-main>
<!-- 底部区域 -->
<el-footer>大事件 ©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-router npm install vue-router@4
- 在src/router/index.js中创建路由器,并导出
- 在vue应用实例中使用vue-router
- 声明router-view标签,展示组件内容
main.js
import router from './router'
app.use(router)
注意:app.use(router)应该放在上面才能正常使用
index.js
//导入vue-router
import { createRouter ,createWebHistory } from "vue-router";
//导入组件
import LoginVue from "@/views/Login.vue";
import LayoutVue from "@/views/Layout.vue";
//定义路由关系
const routes = [
{path:'/login',component:LoginVue},
{path:'/',component:LayoutVue}
]
//创建路由器
const router = createRouter({
history: createWebHistory(),
routes: routes
});
//导出路由
export default router
App.vue
<!-- 根组件 -->
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>
让用户成功登录后跳转到主页面
//登录函数
import {useRouter} from 'vue-router'
const router = useRouter()
const login =async()=>{
//调用接口,完成登录
let result = await userLoginService(registerData.value);
ElMessage.success(result.message? result.message:'登录成功')
//跳转到首页,路由完成跳转
router.push('/')
}
子路由
- 复制资料中提供好的五个组件
- 配置子路由
- 声明router-view标签
- 为菜单项el-menu-item设置index属性,设置点击后的路由路径
index.js
//导入组件
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}
]
}
]
Layout.vue
<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-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>
文章分类列表
article.js
import request from '@/utils/request.js'
//文章列表查询
export const ArticleCategoryListService = ()=>{
return request.get('/category')
}
ArticleCategory.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} from '@/api/article.js'
const articleCategoryList = async()=>{
let result = await articleCategoryListService();
categorys.value = result.data;
}
articleCategoryList();
</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-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" ></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-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状态管理库
由于没有携带token,不能正常访问,可以使用Pinia
Pinia是Vue的专属状态管理库,它允许你跨组件或页面共享状态
使用步骤:
- 安装pinia npm install pinia
- 在vue应用实例中使用pinia
- 在src/stores/token.js中定义store
- 在组件中使用store
token.js
//定义store
import { defineStore } from "pinia";
import {ref} from 'vue'
/**
* 第一个参数:名字,唯一性
* 第二个参数:函数,函数的内部可以定义状态的所有内容
*
* 返回值:函数
*/
export const useTokenStore = defineStore('token',()=>{
//定义状态的内容
//1.响应式变量
const token = ref('')
//2.定义一个函数,修改的token的值
const setToken = (newToken)=>{
token.value = newToken
}
//3.函数,移除token的值
const removeToken = ()=>{
token.value=''
}
return {
token,setToken,removeToken
}
});
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}})
}
Login.vue
//绑定数据,复用注册表单的数据模型
//表单数据检验
//登录函数
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);
ElMessage.success(result.message? result.message:'登录成功')
//把得到的token存储到pinia中
tokenStore.setToken(result.data)
//跳转到首页,路由完成跳转
router.push('/')
}
这样写过于繁琐
应该优化成Axios请求拦截器
import { useTokenStore } from '@/stores/token';
//添加请求拦截器
instance.interceptors.request.use(
(config)=>{
//请求前的回调
//添加token
const tokenStore = useTokenStore();
//判断有没有token
if(tokenStore.token){
config.headers.Authorization = tokenStore.token
}
return config;
},
(err)=>{
//请求错误的回调
Promise.reject(err)
}
)
Pinia持久化插件-persist
- Pinia默认是内存储存,当刷新浏览器的时候会丢失数据
- Persist插件可以将Pinia中的数据持久化的存储
使用步骤:
- 安装persist
npm install pinia-persistedstate-plugin
- 在pinia中使用persist
- 定义状态Store时指定持久化配置参数
main.js
import { createPersistedState } from 'pinia-persistedstate-plugin'
const persist = createPersistedState();
token.js
export const useTokenStore = defineStore('token',()=>{
//定义状态的内容
//1.响应式变量
const token = ref('')
//2.定义一个函数,修改的token的值
const setToken = (newToken)=>{
token.value = newToken
}
//3.函数,移除token的值
const removeToken = ()=>{
token.value=''
}
return {
token,setToken,removeToken
}
},
{
persist:true //持久化存储
}
);
这样在主页面刷新的时候token就会被保留了,不会出现服务器异常
未登录统一处理
在request.js中添加代码,如果用户没有登录会返回到登录页面
//添加响应拦截器
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);//异步的状态转化成失败的状态
}
)
添加文章分类
添加分类弹窗页面:
</el-dialog><!-- 添加分类弹窗 -->
<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" @click="addCategory"> 确认 </el-button>
</span>
</template>
</el-dialog>
//声明一个异步函数
import {articleCategoryListService,articleCategoryAddService} 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.message? result.message:'添加成功')
//调用获取所有文章分类的函数
articleCategoryList();
dialogVisible=false;
}
article.js
//文章分类添加
export const articleCategoryAddService = (categoryData)=>{
return request.post('/category',categoryData)
}
修改文章分类
弹窗显示
//展示编辑弹窗
const showDialog = (row)=>{
dialogVisible.value = true; title.value='编辑分类'
//数据拷贝
categoryModel.value.categoryName = row.categoryName;
categoryModel.value.categoryAlias = row.categoryAlias;
//扩展id属性,将来需要传递给后台,完成分类的修改
categoryModel.value.id = row.id
}
<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"></el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="没有数据" />
</template>
</el-table>
调用接口
ArticleCategory.vue
</script>
//编辑分类
const updateCategory = async()=>{
//调用接口
let result = await articleCategoryUpdateService(categoryModel.value);
ElMessage.success(result.message? result.message:'修改成功')
//调用获取所有分类的函数
articleCategoryList();
//隐藏弹窗
dialogVisible.value=false;
}
<script>
//清空模型数据
const clearData = ()=>{
categoryModel.value.categoryName='';
categoryModel.value.categoryAlias='';
}
<el-button type="primary" @click="dialogVisible=true;title='添加分类';clearData()">添加分类</el-button>
<el-button type="primary" @click="title=='添加分类'? addCategory() : updateCategory()"> 确认 </el-button>
article.js
//文章分类修改
export const articleCategoryUpdateService = (categoryData)=>{
return request.put('/category',categoryData)
}
删除文章分类
//删除分类
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: '取消删除',
})
})
}
<el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button>
article.js
//文章分类修改
export const articleCategoryDeleteService = (id)=>{
return request.delete('/category?id='+id)
}
文章列表查询
article.js
//文章列表查询
export const articleListService = (params)=>{
return request.get('/article',{params:params})
}
ArticleManage.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"
}
])
//用户搜索时选中的分类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
}
//回显文章分类
import {articleCategoryListService,articleListService} from '@/api/article.js'
const articleCategoryList = async()=>{
let result = await articleCategoryListService();
categorys.value = result.data
}
//获取文章列表数据
const articleList = async ()=>{
let params = {
pageNum: pageNum.value,
pageSize: pageSize.value,
categoryId: categoryId.value? categoryId.value: null,
state: state.value? state.value: null
}
let result = await articleListService(params);
//渲染视图
total.value = result.data.total;
articles.value = result.data.items;
}
articleList();
</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>
将分类中的数字转换为分类名称
//处理数据,给数据模型扩展一个属性categoryName,分类名称
for (let i = 0; i < articles.value.length; i++) {
let article = articles.value[i]
for(let j = 0; j<categorys.value.length;j++){
article.categoryName = categorys.value[j].categoryName
}
}
<el-table-column label="分类" prop="categoryName"></el-table-column>
事件处理
<el-form-item>
<el-button type="primary" @click="articleList">搜索</el-button>
<el-button @click="categoryId='';state=''">重置</el-button>
</el-form-item>
//当每页条数发生了变化,调用此函数
const onSizeChange = (size) => {
pageSize.value = size
articleList()
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
pageNum.value = num
articleList()
}
添加文章
添加文章抽屉组件
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})
22.3 接口调用
在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
}
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
}