全栈SpringBoot项目

文章目录


前言

全栈SpringBoot项目


一、校验

1.1 依赖

        <!-- 验证起步依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

1.2 类上增加注解

类上增加了@Validated,方法参数上增加了@Pattern(regexp = "^\\S{5,16}$"),标识5到16位,否则抛出异常

@Validated
@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;

    @PostMapping("/register")
    public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password){
        return loginService.register(username, password);
    }
}

以上方式抛出的异常,返回前端不是格式统一的结果需要全局异常处理

二、全局异常处理

1.定义全局异常处理类

package com.web.springbootall.exception;

import com.web.springbootall.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){

        log.error("系统异常:{}",e.getMessage(), e);

        return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "系统异常");
    }

}

三. 返回字段忽略(比如密码等敏感字段)

需要忽略的返回字段增加@JsonIgnore

    //对象变为JSON时,不设置该字段
    @JsonIgnore
    private String password;//密码

四. 使用ThreadLocal存储用户登陆信息

4.1. 定义ThreadLocal工具类

这里提供了设置,获取,清除方法

package com.web.springbootall.util;

public class ThreadLocalUtil {

    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    public static void set(Object value) {
        THREAD_LOCAL.set(value);
    }

    public static <T> T get() {
        return (T)THREAD_LOCAL.get();
    }

    public static void remove() {
        THREAD_LOCAL.remove();
    }
}

4.2. 定义拦截器,在请求前为用户登陆信息设置值,请求后清除值

preHandle在从token中获取到用户信息时,设置用户信息到ThreadLocal中,afterCompletion在请求完成后,清除ThreadLocal中的用户登陆信息

package com.web.springbootall.interceptor;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.interfaces.Claim;
import com.web.springbootall.pojo.Result;
import com.web.springbootall.pojo.User;
import com.web.springbootall.util.JwtUtil;
import com.web.springbootall.util.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        if (StringUtils.hasLength(token)) {
            Claim claim = null;
            try {
                claim = jwtUtil.parse(token);
            } catch (Exception e) {
                log.error("token解析失败", e);
            }
            if (claim != null) {
                //每次请求前,拦截器设置用户信息,确保用户信息在ThreadLocal中
                ThreadLocalUtil.set(claim.as(User.class));
                return true;
            }
        }
        Result tokenInvalid = Result.error("token无效");
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(401);
        response.getWriter().write(JSON.toJSONString(tokenInvalid));
        return false;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //请求完成后,清除用户信息,防止出现内存泄露
        ThreadLocalUtil.remove();
    }
}

4.3. 在拦截器设置了用户信息后,从ThreadLocal中获取

import com.web.springbootall.mapper.UserMapper;
import com.web.springbootall.pojo.User;
import com.web.springbootall.service.UserService;
import com.web.springbootall.util.ThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserInfo() {
        User user = ThreadLocalUtil.get();
        if (user != null) {
            user = userMapper.getById(user.getId());
        }
        return user;
    }
}

5. 对象字段校验

5.1 对象字段增加注解,以及错误提示信息

@NotNull(message = "id不能为空")
@NotEmpty @Pattern(regexp = "^\\S{1,10}$", message = "昵称请输入1-10个字符")
@Email

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.ToString;

import java.time.LocalDateTime;

@Data
@ToString
public class User {
    @NotNull(message = "id不能为空")
    private Integer id;//主键ID
    private String username;//用户名
    //对象变为JSON时,不设置该字段
    @JsonIgnore
    private String password;//密码
    @NotEmpty
    @Pattern(regexp = "^\\S{1,10}$", message = "昵称请输入1-10个字符")
    private String nickname;//昵称
    @NotEmpty
    @Email
    @Pattern(regexp = "^\\S{5,16}$", message = "邮箱格式不正确,5-16位")
    private String email;//邮箱
    private String userPic;//用户头像地址
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

5.2 Controller接收对象出增加注解

@Validated User user这里表示对User进行校验

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

5.3 全局异常处理针对该异常进行处理

针对该种类型异常(MethodArgumentNotValidException),获取message的内容e.getBindingResult().getAllErrors().get(0) .getDefaultMessage()


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidateException(MethodArgumentNotValidException e) {
        log.error("数据校验异常:{}", e.getMessage(), e);
        String defaultMessage = e.getBindingResult().getAllErrors().get(0)
                .getDefaultMessage();
        return Result.error(StringUtils.hasLength(defaultMessage) ? defaultMessage : e.getMessage());
    }

六. 分组校验

6.1 实体类增加校验注解

下面的类定义了两个interface接口AddUpdate, 然后在id属性的@NotNull注解的gourps指定Update.class,表明id属性仅修改时不能为空,但是新增的时候,因为id数据库自增,所以可以为空。
categoryNamecategoryAlias无论新增还是修改都不能为空,所以groups = {Add.class, Update.class}

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;

@Data
@ToString
public class Category {
    @NotNull(message = "修改的分类数据不能为空", groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty(message = "分类名称不能为空", groups = {Add.class, Update.class})
    private String categoryName;//分类名称
    @NotEmpty(message = "分类别名不能为空", 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;//更新时间

    public interface Add {

    }

    public interface Update {

    }

}

6.2 Controller校验注解增加分组,表明该操作为何分组

@Validated(Category.Update.class)指定该操作属于修改分组

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

@Validated(Category.Add.class)指定该操作属于新增分组

    @PostMapping("/add")
    public Result addCategory(@RequestBody @Validated(Category.Add.class) Category category){
        categoryService.addCategory(category);
        return Result.success();
    }

6.3 校验属于默认分组

categoryNamecategoryAlias没有指定分组,则属于默认分组DefaultAddUpdate都继承了Default,意味着categoryNamecategoryAlias属于两个组,他们的操作校验,都需要满足

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.groups.Default;
import lombok.Data;
import lombok.ToString;

import java.time.LocalDateTime;


@Data
@ToString
public class Category {
    @NotNull(message = "修改的分类数据不能为空", groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty(message = "分类名称不能为空")
    private String categoryName;//分类名称
    @NotEmpty(message = "分类别名不能为空")
    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;//更新时间

    public interface Add extends Default {

    }

    public interface Update extends Default {

    }

}

七.自定义校验

7.1自定义注解

这里使用定义了StateValidation进行校验

import com.web.springbootall.validation.StateValidation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(
        validatedBy = {StateValidation.class}
)
public @interface StateValid {

    String message() default "文章状态只能为:草稿/已发布";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

7.2 自定义校验类,实现校验接口

StateValidation实现了ConstraintValidator接口,定义了状态为草稿已发布才满足条件,返回true

package com.web.springbootall.validation;

import com.web.springbootall.anno.StateValid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.util.StringUtils;

public class StateValidation implements ConstraintValidator<StateValid, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (StringUtils.hasLength(value) && (value.equals("草稿") || value.equals("已发布"))){
            return true;
        }
        return false;
    }
}

7.3 需要校验的字段使用注解

状态为已发布或者草稿才可校验通过

@StateValid
private String state;//发布状态 已发布|草稿

八.登陆使用Redis再次校验token正确性

8.1问题产生

修改密码后,按道理必须重新登陆,使用新的token访问别的接口。但是在拦截器处,仅仅判断了JWT中的token是否正确,应该增加判断token是否为新的token才对。

8.2 Redis解决实现方式

用户登陆时,Redis存储token信息,key为id,value为token,一小时过期。
用户修改密码后,Redis中移除该用户的token。
拦截器从请求头取的token进行token判断时,除了从JWT判断是否正常,还判断是否和Redis一致。

如果修改密码后未进行重新登陆,访问其他请求(比如用户信息)使用的旧的token,那么拦截器不会放行,访问不到。
如果使用重新登陆后的token,因为登陆时token重新设置在了Redis中,所以后续访问其他请求,和拦截器token一致,可以访问到。

8.3 具体实现

引入Redis依赖:

       <!--springboot集成redis的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

登陆:

		@Autowired
    	private StringRedisTemplate redisTemplate;
		...
       //存储token到Redis,key为userid,value为token,一小时过期
        redisTemplate.opsForValue().set(user.getId().toString(), token, 1L, TimeUnit.HOURS);

修改密码:

        //修改密码后从Redis移除该用户对应的token信息,以便后续请求访问时,拦截器的token的redis不一致,跳转登陆
        redisTemplate.delete(id.toString());

拦截器:

				...
				User user = claim.as(User.class);
                //判断用户登陆存储到Redis中的token和新获取的是否一致,如果不一致,可能是修改密码后未重新登陆,此时应该让用户重新登陆。
                // 修改密码后,从Redis移除该用户的token,此处判断token不一致,不放行,进行登陆
                String tokenRedis = redisTemplate.opsForValue().get(user.getId().toString());
                if (token.equals(tokenRedis)){
                    //每次请求前,拦截器设置用户信息,确保用户信息在ThreadLocal中
                    ThreadLocalUtil.set(user);
                    return true;
                 }

九.前端

9.1 js方法的使用

定义script.js

function a(msg){
    console.log("in a...............:" + msg);
}

function b(msg){
    console.log(new Date() + "in b...............:" + msg);
}

定义html,引入使用js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        <button id="btn">点我展示信息</button>
    </div>
    <script src="./script.js"></script>
    <script>
        document.getElementById("btn").onclick = function(){
            a('哈哈,今天也要加油');
        }
    </script>
</body>
</html>

html页面,VSCode,alt+b打开浏览器,点击按钮,控制台输出信息

9.2 js export的使用

9.2.1 初步使用

修改以上js,function前面加export

export function a(msg){
    console.log("in a...............:" + msg);
}

export function b(msg){
    console.log(new Date() + "in b...............:" + msg);
}

修改以上html,script的type为module,然后在其中使用import导入方法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        <button id="btn">点我展示信息</button>
    </div>
    <script type="module">
        import {a} from "./script.js"
        document.getElementById("btn").onclick = function(){
            a('哈哈,今天也要加油');
        }
    </script>
</body>
</html>

VSCode使用open with live server,不然会出现跨域问题。
在这里插入图片描述

9.2.2js批量导出方法在这里插入代码片

export {a, b}即为导出a,b方法

function a(msg){
    console.log("in a...............:" + msg);
}

function b(msg){
    console.log(new Date() + "in b...............:" + msg);
}

export {a, b}

方法名称太长,导入导出可以as取别名

export {a as c, b as d}

9.2.3 js导出默认

使用export default {a, b}导出默认

function a(msg){
    console.log("in a...............:" + msg);
}

function b(msg){
    console.log(new Date() + "in b...............:" + msg);
}

export default {a, b}

html import时不需要{},如下面的import on from "./script.js",on是随便起的名字,包含了导入的所有方法,
on.a('哈哈,今天也要加油');使用on.a可以使用a方法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        <button id="btn">点我展示信息</button>
    </div>
    <script type="module">
        import on from "./script.js"
        document.getElementById("btn").onclick = function(){
            on.a('哈哈,今天也要加油');
        }
    </script>
</body>
</html>

9.3 Vue入门

9.3.1 Vue快速热门

这里首先定义了一个scripttypemodule,然后引入了Vue官方cdn的js中的createApp,
import {createApp} from "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
然后createApp方法中传入了一个对象参数,

	{	
         data() {
                 return {
                     msg: '看起来,你在学习Vue'
                 }
         }
    }    

data中定义数据msg,值为看起来,你在学习Vue
并且createApp挂在到了id为app的元素上.mount("#app");
上面定义了一个div,id为app,则该div中,可以使用createApp定义的data中的数据msg
使用了{{msg}}这种方式获取

	<div id="app">
        <h1>{{msg}}</h1>    
    </div>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <div id="app">
        <h1>{{msg}}</h1>    
    </div>

    <script type="module">
        import {createApp} from "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
        createApp({
             data() {
                 return {
                     msg: '看起来,你在学习Vue'
                 }
             }
        }).mount("#app");
    </script>
</body>
</html>

9.3.2 v-for循环界面展示数据

这里使用了v-for,articleList是数据来源(具体根据data中的名称确定),article是遍历集合,每个元素的值(自定义名称),index为下标。

			<tr v-for="(article, index) in articleList">
                <th>{{article.title}}</th>
                <th>{{article.category}}</th>
                <th>{{article.time}}</th>
                <th>{{article.state}}</th>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>

使用的数据在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">
        <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">
                <th>{{article.title}}</th>
                <th>{{article.category}}</th>
                <th>{{article.time}}</th>
                <th>{{article.state}}</th>
                <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>

9.3.3 v-bind动态绑定数据

<a v-bind:href="url">百度</a>,这里绑定了a标签的href属性值为urlurldata中定义
也可以使用<a :href="url">百度</a>,这种简化方式

<!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>
    </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.baidu.com'
                }
            }
        }).mount("#app")//控制html元素
    </script>
</body>
</html>

9.3.4 v-if和v-show

v-if会根据判断展示和移除元素,在浏览器中看不到,适用于不频繁切换的场景;
v-show通过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="user.level == 1">9.9</span>  
                   <span v-else-if="user.level > 1 && user.level <=5 ">19.9</span> 
                   <span v-else="user.level > 5">29.9</span>

        <br>
        
                   <span v-show="user.level == 1">9.9</span>  
                   <span v-show="user.level > 1 && user.level <=5 ">19.9</span> 
                   <span v-show="user.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 {
                    user: {
                        name: 'zs',
                        level: 1
                    }
                }
            }
        }).mount("#app")//控制html元素
    </script>
</body>

</html>

9.3.5 v-on

定义了v-on:
v-on:click="weather"
定义了方法:

			methods: {
                weather() {
                    alert('今天天气很好');
                },
                learn() {
                    alert('今天学习得好');
                }    
            }

简写:
@click="learn"

<!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="weather">点我有惊喜</button> &nbsp;
        <button @click="learn">再点更惊喜</button>
    </div>

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

        //创建vue应用实例
        createApp({
            data() {
                return {
                    //定义数据
                }
            },
            methods: {
                weather() {
                    alert('今天天气很好');
                },
                learn() {
                    alert('今天学习得好');
                }    
            },
        }).mount("#app");//控制html元素

    </script>
</body>
</html>

9.3.6 v-model双向绑定

这里的methods中的clear,使用了this.query获取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="query.type"/> {{query.type}}

        发布状态: <input type="text" v-model="query.state"/> {{query.state}}

        <button>搜索</button>
        <button @click="clear">重置</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </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 {
                    //定义数据
                    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: "已发布"
                    }],
                    query: {
                        type: '人文',
                        state: '草稿'
                    }
                }
            },
            methods: {
                clear(){
                    this.query = {
                        type: '',
                        state: ''
                    };
                }    
            },
        }).mount("#app")//控制html元素
    </script>
</body>

</html>

9.3.7 axios获取数据

引入js:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
发送:

				axios.get('http://localhost:8080/article/getAll').then(res=>{
                    this.articleList = res.data;
                }).catch(err=>{
                    console.log(err);
                })
<!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="query.category"/>
        发布状态: <input type="text" v-model="query.state"/>

        <button @click="queryArticle">搜索</button>
        <button @click="clear">重置</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </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 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: [],
                    query: {
                        category: '',
                        state: ''
                    }
                }
            },
            methods: {
                clear(){
                    this.query = {
                        category: '',
                        state: ''
                    };
                },
                queryArticle(){
                    axios.get('http://localhost:8080/article/search?category='+this.query.category + '&state='+this.query.state).then(res=>{
                        this.articleList = res.data;
                    }).catch(err=>{
                        console.log(err);
                    })
                }    
            },
            mounted() {
                axios.get('http://localhost:8080/article/getAll').then(res=>{
                    this.articleList = res.data;
                }).catch(err=>{
                    console.log(err);
                })
            },    
        }).mount("#app")//控制html元素
    </script>
</body>

</html>

9.4 Vue工程化

9.4.1 node的安装配置

设置nodejs全局安装路径,prefix 后的内容请修改为自己的
npm config set prefix /Users/jieqin/utils/nodejs
设置淘宝镜像
npm config set registry http://registry.npm.taobao.org/
查看淘宝镜像
npm config get registry
显示以下设置成功
http://registry.npm.taobao.org/

9.4.2 create-vue创建项目

npm init vue@latest
输入项目名称,其他选择否
然后cd vue-project这里的vue-project为自己输入的项目名称
然后npm install
然后npm run dev
访问:http://localhost:5173/

9.4.3 组合式API

Api.vue,ref类似data,不过组合式使用不一样,先import {ref, onMounted} from 'vue',
const count = ref(0);,如果想要设置变量的值,需要设置给value,从count.value++; 可以看出来

<script setup>
    import {ref, onMounted} from 'vue'
    const count = ref(0);

    function add(){
       count.value++;     
    }
	//挂载完成
    onMounted(()=>{
        console.log('onMounted');
    })
</script>

<template>
        <button @click="add">{{count}}</button>
</template>

App.vue,import了Api.vue
注意import ApiVue from './Api.vue',<ApiVue/>,以及<script setup>

<script setup>
  import ApiVue from './Api.vue'
</script>

<template>
    <h1>西安</h1>
    <br>
    <ApiVue/>
</template>

<style scoped>

  h1{
    color: rgb(151, 87, 87);
  }

</style>

9.4.4 组合式API使用(发送axios请求)

首先在项目目录下执行sudo npm install axios,安装axios,安装axios后,在node-modules下有axios文件夹, import axios from 'axios'即可进行导入

<script setup>
    import {ref, onMounted} from 'vue'
    import axios from 'axios'
    
    const articleList = ref([]);

    onMounted(() => {
        axios.get('http://localhost:8080/article/getAll')
        .then(res=>{
            articleList.value = res.data;
        }).catch(err=>{
            console.log(err);
        })
    });

    const query = ref({
        category: '时事',
        state: '已发布'
    })

    function queryArticle(){
        axios.get('http://localhost:8080/article/search?category=' + query.value.category + '&state=' + query.value.state)
        .then(res=>{
            articleList.value = res.data;
        }).catch(err=>{
            console.log(err);
        })
    }

    function clear(){
        query.value = {}
    }

</script>


<template>
    <div>
        文章分类: <input type="text" v-model="query.category"/>
        发布状态: <input type="text" v-model="query.state"/>

        <button @click="queryArticle">搜索</button>
        <button @click="clear">重置</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
             <tr v-for="article 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>

9.4.5 抽取发送请求方法到js

发送axios请求的js

import axios from 'axios'

//这里定义请求前缀baseURL
const baseURL = 'http://localhost:8080';
//这里axios进行创建,创建后使用instance调用和axios调用一样,这里注意{baseURL}有个花括号
const instance = axios.create({baseURL});

//这里定义发送请求的函数并导出,引入js的的地方直接使用。因为是异步请求,方法前面加上async表示异步,里面同步等待返回。在使用方法的地方,也是类似的。
//比如
// async function all(){
//     articleList.value = await getAll();
// }

// async function queryArticle(){
//     articleList.value = await search({...query.value});
// }
export async function getAllArticleService(){
    return await instance.get('/article/getAll')
            .then(res=>{
                 return res.data;
            }).catch(err=>{
                console.log(err);
            })
}

export async function queryArticleService(queryValue){
    return await instance.get('/article/search?', {params: queryValue})
    .then(res=>{
        return res.data;
    }).catch(err=>{
        console.log(err);
    })
}

使用该js的vue

<script setup>
    import {ref, onMounted} from 'vue'
    import axios from 'axios'
    import {getAllArticleService as getAll, queryArticleService as search} from '@/api/article.js'
    
    const articleList = ref([]);

    onMounted(() => {
        all();
    });

    async function all(){
        articleList.value = await getAll();
    }

    const query = ref({
        category: '时事',
        state: '已发布'
    })

    async function queryArticle(){
        articleList.value = await search({...query.value});
    }

    function clear(){
        query.value = {}
    }

</script>


<template>
    <div>
        文章分类: <input type="text" v-model="query.category"/>
        发布状态: <input type="text" v-model="query.state"/>

        <button @click="queryArticle">搜索</button>
        <button @click="clear">重置</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
             <tr v-for="article 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>

9.4.5 axios响应拦截器统一对结果进行返回

定义util包下的request.js,对请求的响应和异常进行统一处理

import axios from 'axios'

//这里定义请求前缀baseURL
const baseURL = 'http://localhost:8080';
//这里axios进行创建,创建后使用instance调用和axios调用一样,这里注意{baseURL}有个花括号
const instance = axios.create({baseURL});

//响应统一拦截器,定义返回和错误处理
instance.interceptors.response.use(
    //2xx请求状态码
    result => {
        return result.data;
    },
    //其他请求状态码
    err => {
        console.log(err);
        Promise.reject(err);
    }
);

//导出供外部使用
export default instance;

引用刚刚的js,import request from '@/util/request.js',这里的await和async去掉了,拦截器本身处理了,但是vue处调用异步方法时,还是需要加上await和async

//引入发送请求,返回响应的js
import request from '@/util/request.js'

//这里定义发送请求的函数并导出,引入js的的地方直接使用。因为是异步请求,方法前面加上async表示异步,里面同步等待返回。在使用方法的地方,也是类似的。
//比如
// async function all(){
//     articleList.value = await getAll();
// }

// async function queryArticle(){
//     articleList.value = await search({...query.value});
// }
export function getAllArticleService(){
    //请求进行了拦截返回处理,这里直接调用即可
    return request.get('/article/getAll');
}

export function queryArticleService(queryValue){
    return request.get('/article/search?', {params: queryValue});
}

9.5 Element-Plus

9.5.1快速入门

create-vue创建项目
npm init vue@latest,输入项目名称,其他选择默认否
cd 项目目录下,npm install,再npm run dev,看是否可以访问,如果可以证明创建的Vue没问题。
npm install element-plus --save进行element-plus的安装
main.js复制官网全局的

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

然后复制官网组件进行修改,自己写一个包含卡片,表单,按钮,表格,分页的界面
类似下面的图片:
在这里插入图片描述
分页想要变成中文的,main.js引入中文包:
import locale from 'element-plus/dist/locale/zh-cn.js'
然后使用app.use(ElementPlus, { locale })

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')

所有相关代码:
src/util/request.js封装axios发送请求,响应结果。这里需要项目目录下安装了axios,
可以使用sudo npm install axios安装

import axios from 'axios'

const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL});

instance.interceptors.response.use(
    result=>{
        return result.data;
    },
    err=>{
        console.error(err);
        Promise.reject(err);
    }
);

export default instance;

src/api/article.js,引入request.js,封装发送请求

import request from '@/util/request.js'

//分页查询文章数据
export const getArticlePage = function(queryParams) {
    return request.get("/article/getAll", {params: queryParams});
};

src/Article.vue引入article.js,界面展示

<script lnag='ts' setup>
    import { reactive, ref, onMounted } from 'vue'
    import {
        Check,
        Delete,
        Edit,
        Message,
        Search,
        Star,
    } from '@element-plus/icons-vue'
    import {getArticlePage} from '@/api/article.js'

    const article = reactive({
        category: '',
        state: ''
    })

    const tableData = ref([
        
    ]);

    const currentPage = ref(1)
    const pageSize = ref(10)
    const small = ref(false)
    const background = ref(false)
    const disabled = ref(false)
    const total = 20

    const handleSizeChange = (number) => {
        console.log(number);
    }
    const handleCurrentChange = (number) => {
        console.log(number);
    }

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

    const queryParams = ref({
        queryNum: 1,
        querySize: 10,
        category: '',
        state: ''
    });

    //页面加载完毕,查询表格数据
    onMounted(() => {
        getPage();
    });
    //分页查询文章方法,异步请求,需要async和await
    const getPage = async function(){
        let getData = await getArticlePage({...queryParams.value});
        console.log(getData);
        tableData.value = getData;
    }

</script>

<template>
            <!-- 所有在一个卡片中 -->
            <el-card class="box-card">
                <!-- 首行 -->
                <div class="card-header">
                    <span>文章管理</span>
                    <el-button type="primary">发布文章</el-button>
                </div>
                
                <!-- 分割线 -->
                <el-divider />

                <!-- 搜索表单 -->
                <el-form :inline="true" :model="article">
                    <el-form-item label="文章分类:" width="120">
                        <el-select
                        v-model="article.category"
                        placeholder="请选择"
                        clearable
                        size="default"
                        style="width: 240px"
                        >
                        </el-select>
                    </el-form-item>
                    <el-form-item label="发布状态:">
                        <el-select
                        v-model="article.region"
                        placeholder="请选择"
                        clearable
                        size="default"
                        style="width: 240px"
                        >
                        <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-button type="primary" @click="onSubmit" plain>重置</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="操作" fixed="right" width="180">
                        <el-button type="primary" :icon="Edit" circle />
                        <el-button type="danger" :icon="Delete" circle />
                    </el-table-column>
                </el-table>

                <!-- 分页 -->
                <el-pagination class="el-p"
                    v-model:current-page="currentPage"
                    v-model:page-size="pageSize"
                    :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>
    .card-header {
        display: flex;
        justify-content: space-between;
    }
    .el-p {
        display: flex;
        justify-content: end;
        margin-top: 20px;
    }
</style>

App.Vue使用article.vue

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

<template>
      <div id="app">
        <ArticleVue/>
      </div>
</template>

<style scoped>

</style>

main.js引入Element-Plus和其样式,以及中文包

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')

9.5.2 创建新的Element-Plus项目

  1. 首先参考创建一个Vue项目
    9.4.2 create-vue创建项目
  2. npm install element-plus --save进行element-plus的安装
  3. sudo npm install axios安装axios
  4. npm install sass -D安装saas
  5. 文件目录整理,删除components下的内容,新建api,utils,views目录,删除App.vue中的内容
  6. 修改main.js
import './assets/main.scss'
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')
  1. request.js放到src/utils下,baseURL路径根据实际情况修改
import axios from 'axios'

const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL});

instance.interceptors.response.use(
    result=>{
        return result.data;
    },
    err=>{
        console.error(err);
        Promise.reject(err);
    }
);
export default instance;
  1. 登录出现跨域,需要处理request.js和vite.config.js
    vite.config.js
    这里拦截了/api的请求,代理到http://localhost:8080/api,,并且去掉了/api
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,
          //将/api去掉
          rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

request.js
这里将请求都加上前缀/api,在上面代理处理

import axios from 'axios'
//弹出消息提示,自动取消
import { ElMessage } from 'element-plus'
//失败提示
const fail = (msg) => {
  ElMessage({
    message: msg,
    type: 'warning',
  })
}

// const baseURL = 'http://localhost:8080';
//统一接口入口
const baseURL = '/api';
const instance = axios.create({baseURL});

instance.interceptors.response.use(
    result=>{
        if(result.data.code === 0){
            return result.data;
        }
        fail(result.data.message);
        Promise.reject(result.data);
    },
    err=>{
        fail(err);
        Promise.reject(err);
    }
);
export default instance;
  1. 登陆请求的api写在src/api/login.js中
    这里发送请求的参数为表单类型Content-Type: application/x-www-form-urlencoded;charset=UTF-8,使用URLSearchParams进行封装传递
import request from '@/utils/request.js'

//注册发送请求
export const registerService = (registerParam)=>{
    //因为表单参数,使用URLSearchParams进行参数封装
    let registerParams = new URLSearchParams();
    for(let key in registerParam){
        registerParams.append(key, registerParam[key]);
    }
    return request.post('/user/register', registerParams);
}

//登陆发送请求
export const loginService = (loginParam)=>{
    //因为表单参数,使用URLSearchParams进行参数封装
    let loginParams = new URLSearchParams();
    for(let key in loginParam){
        loginParams.append(key, loginParam[key]);
    }
    return request.post('/user/login', loginParams);
}
  1. 注册登陆界面
    这里面有对表单的校验,数据绑定
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref, reactive } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//注册参数
const registerData = ref({
    username: '',
    password: '',
    rePassword: ''
})

//密码和再次输入密码校验
const validateRePassword = (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: validateRePassword, trigger: 'blur' },
  ],
};

import {registerService, loginService} from '@/api/login.js'
//注册
const register = async ()=> {
    let registerResult = await registerService(registerData.value);
    success(registerResult.message);
};

//登陆
const login = async ()=> {
    let loginResult = await loginService(registerData.value);
    success(loginResult.message);
};

//弹出消息提示,自动取消
import { ElMessage } from 'element-plus'
//成功提示
const success = (msg) => {
  ElMessage({
    message: msg,
    type: 'success',
  })
}

</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" :rules="rules" :model="registerData">
                <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>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else :rules="rules" :model="registerData">
                <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>
                    <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>
        </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>

9.5.3 Router使用

  1. 安装vue-router
    npm install vue-router@4
  2. src/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
  1. vue应用实例中使用vue-router
    main.js中导入index.js,并使用。这里index.js不用指明,app.use(router);需要单独写,不然界面出不来
import './assets/main.scss'

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'
//导入路由
import router from '@/router'

const app = createApp(App);

app.use(router);
app.use(ElementPlus, { locale });
app.mount('#app');

  1. 声明router-view标签,展示组件内容
    在App.vue中,添加<router-view/>
<script setup>

</script>

<template>
    <router-view/>
</template>

<style scoped>

</style>

  1. 登陆完成,跳转首页路由处理
    Login.vue导入路由,登陆完成的方法中,增加路由跳转
//导入路由
import {useRouter} from 'vue-router'
const router = useRouter();
//登陆
const login = async ()=> {
    let loginResult = await loginService(registerData.value);
    success(loginResult.message);
    //登陆成功跳转到首页
    router.push('/');
};

主要是import {useRouter} from 'vue-router' const router = useRouter();,以及router.push('/');
6. 主页菜单跳转
增加vue页面
router/index.js, import 导入的页面,再增加子路由children,定义跳转的页面。
redirect定义默认跳转的界面。

        //定义路由关系
const routes = [
    {
        path: '/login',
        component: LoginVue
    },
    {
        path: '/',
        component: LayoutVue,
        //定义子组件
        children: [
            {
                path: '/user/avatar',
                component: UserAvatarVue
            },
            {
                path: '/user/info',
                component: UserInfoVue
            },
            {
                path: '/user/resetPwd',
                component: UserResetPasswordVue
            },
            {
                path: '/article/category',
                component: ArticleCategoryVue
            },
            {
                path: '/article/manage',
                component: ArticleManageVue
            },
        ],
        redirect: '/article/manage'
    }
]
  1. 主页中需要路由跳转后展示的地方定义<router-view/>
    Layout.vue,其中<el-menu-item index="/article/category">,这些el-menu-item,增加了index属性进行跳转,路径对应index.js的path,然后在跳转展示位置设置了router-view
    <el-main> <router-view/> </el-main>
<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 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/resetPwd">
                        <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>
                <router-view/>
            </el-main>
            <!-- 底部区域 -->
            <el-footer> ©2024 Created by xxx </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>

9.5.4 Pinia使用完成请求token的存储

  1. 安装pinia
    npm install pinia
  2. main.js使用
import {createPinia} from 'pinia'

const pinia = createPinia();
app.use(pinia);
  1. src/stores/token.js定义store
import {defineStore} from 'pinia';
import {ref} from 'vue';

export const useTokenStore = defineStore('token', ()=>{
	//定义描述token
	const token = ref('');
	//设置token的方法
	const setToken = (newToken)=>{
		token.value = newToken;
	}
	//移除token的方法
	const removeToken = ()=>{
		token.value = '';
	}
	return {
		token,setToken,removeToken
	}
});
  1. Login.vue/article.vue使用
    Login.vue,这里使用tokenStore.setToken(loginResult.data);设置了登陆返回的token到pinia中
//导入token.js
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
//登陆
const login = async ()=> {
    let loginResult = await loginService(registerData.value);
    success(loginResult.message);
    tokenStore.setToken(loginResult.data);
    //登陆成功跳转到首页
    router.push('/');
};

article.js
这里通过{headers: {'token': tokenStore.token}},在请求头中携带了token

import request from '@/utils/request.js'
//导入token.js
import {useTokenStore} from '@/stores/token.js'

//查询分类列表
export const queryCategoryService = function(){
    const tokenStore = useTokenStore();
    //pinia中配置的响应式数据不需要通过.value获取
    return request.get('/category', {headers: {'token': tokenStore.token}});
}
  1. request.js的axios的请求拦截器统一添加请求token
    instance.interceptors.request.use这里方法有两个参数,一个正常配置参数,一个异常处理。
    正常配置的参数config可以用来设置请求头config.headers.token = useToken.token;
import axios from 'axios'
//弹出消息提示,自动取消
import { ElMessage } from 'element-plus'
//失败提示
const fail = (msg) => {
  ElMessage({
    message: msg,
    type: 'warning',
  })
}

// const baseURL = 'http://localhost:8080';
//统一接口入口
const baseURL = '/api';
const instance = axios.create({baseURL});
//响应拦截器
instance.interceptors.response.use(
    result=>{
        if(result.data.code === 0){
            return result.data;
        }
        fail(result.data.message);
        Promise.reject(result.data);
    },
    err=>{
        fail(err);
        Promise.reject(err);
    }
);

//导入token.js
import {useTokenStore} from '@/stores/token.js'
//请求拦截器
instance.interceptors.request.use(
  (config) => {
      //获取pinia中的token,如果存在,请求头携带
      const useToken = useTokenStore();
      if(useToken){
        config.headers.token = useToken.token;
      }
      return config;
  },
  err => {
    fail(err);
    Promise.reject(err);
  }

);


export default instance;

6.pinia持久化
安装插件:npm install pinia-persistedstate-plugin
main.js,导入,pinia使用

	import {createPersistedState} from 'pinia-persistedstate-plugin'
	const persist = createPersistedState();
	pinia.use(persist);

token.js的defineStore增加第三个参数:
{persisted: true}
7.未登陆跳转到首页
request.js中先导入路由的index.js,import router from '@/router',然后err.response.status === 401
失败响应状态吗为401时,使用router.push('/login');跳转到登陆界面。

//导入路由router/index.js
import router from '@/router'
...
//统一接口入口
const baseURL = '/api';
const instance = axios.create({baseURL});
//响应拦截器
instance.interceptors.response.use(
    //响应码2xx进入该处
    result=>{
        if(result.data.code === 0){
            return result.data;
        }
        fail(result.data.message);
        Promise.reject(result.data);
    },
    //响应码不为2xx进入此处
    err=>{
        if(err.response.status === 401){
          fail('请重新登陆');
          router.push('/login');
        }else{
          fail(err);
        }
        Promise.reject(err);
    }
);

8.富文本编辑器
npm install @vueup/vue-quill@latest --save
使用的vue的script中导入

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

vue使用:

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

vue的script设置样式:

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

总结

该项目代码太多,文章并非完整代码,以上为示例核心代码。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值