首页 > 其他分享 >从零搭建SpringBoot3+Vue3前后端分离项目基座,中小项目可用

从零搭建SpringBoot3+Vue3前后端分离项目基座,中小项目可用

时间:2024-04-05 15:32:14浏览次数:27  
标签:return user 基座 token SpringBoot3 Vue3 import public const

文章目录


项目使用SpringBoot3+Vue3, 后端使用springboot3, mybatisPlus, druid,knife4j(swagger3),Jwt; 前端 vue3 , element-plus, axios, pinia, vue-router; 项目前后端分离, 可持续扩展, 代码放到最后

1. 后端项目搭建

1.1 环境准备

- spring-boot3 最低支持jdk17, 所以需要准备jdk17环境
- Idea 版本IntelliJ IDEA 2021.2及以后,版本关系参考[https://blog.csdn.net/m0_62258564/article/details/134527268](https://blog.csdn.net/m0_62258564/article/details/134527268)
- maven 版本参考以上链接
- MySql8

1.2 数据表准备

创建数据库 base_manage, 并创建表

CREATE TABLE user(
    id INT NOT NULL AUTO_INCREMENT COMMENT '主键',
    login_name VARCHAR(255) NOT NULL COMMENT '登录名(账号)',
    password VARCHAR(255) NOT NULL COMMENT '密码',
    name varchar(50) NOT NULL COMMENT '姓名',
    sex char(1) NOT NULL COMMENT '性别',
    phone VARCHAR(20) COMMENT '联系电话',
    PRIMARY KEY(id)
 )COMMENT '用户信息表';

用户表只是便于后续过程搭建操作,可根据需求修改

1.3 SpringBoot3项目创建

在这里插入图片描述
springboot版本可根据需求选择,这里选则默认的3.2.4
在这里插入图片描述
耐心等待项目构建完成, 构建完成pom文件如下,请注意,mysql驱动包<groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId>较之前版本有所改变

<?xml version="1.0" encoding="UTF-8"?>
<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 https://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.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.buzhisuoyun</groupId>
    <artifactId>base_manage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>base_manage</name>
    <description>base_manage</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.4 MySql环境整合,使用druid连接池

添加 jdbc依赖,防止项目启动时找不到Bean报错

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

添加druid依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-3-starter -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>1.2.20</version>
</dependency>

项目使用yml格式配置文件,修改resources下application.properties
为application.yml, 并删除文件内容

配置项目端口号
server:
  port: 8099

配置数据源和druid 连接池

spring:
  # 数据源
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/base_manage?serverTimezone=UTC
    username: root
    password: root

启动项目, 查看日志,端口和druid 初始化, 整合成功
在这里插入图片描述

1.5 整合mybatis-plus

1.5.1 引入mybatis-plus

官网地址: https://baomidou.com/
1、添加依赖

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.5</version>
</dependency>

2、配置

# mybatis-plus
mybatis-plus:
  configuration:
    # sql日志, 开发调试时开启
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto

启动项目,出现mybatis-plus日志
在这里插入图片描述

1.5.2 配置代码生成器

1.引入相关依赖

<!-- mybatis-plus 代码生成器-->
<dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-generator</artifactId>
     <version>3.5.5</version>
</dependency>

<!-- mybatis-plus代码生成器模板引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
 </dependency>
  1. 新建utis包,并在下边创建MybatisPlusGenerator.java代码生成器配置类
    在这里插入图片描述
package com.buzhisuoyun.base_manage.utils;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.io.File;
import java.sql.Types;
import java.util.Collections;

public class MybatisPlusGenerator {
    static final String url = "jdbc:mysql://127.0.0.1:3306/base_manage?serverTimezone=UTC";    // 数据库地址
    static final String username = "root";  // 数据库用户名
    static final String password = "root";  // 数据库密码
    static final String authorName = "buzhisuoyun"; // 作者名
    static final String parentPackageNameResource = "com/buzhisuoyun/base_manage";  // mapper.xml路径
    static final String parentPackageNameJava = "com.buzhisuoyun.base_manage";  // java 文件父包名
    // 要生成代码对应的数据表名
    static final String tableName = "user";


    public static void main(String[] args) {

        FastAutoGenerator.create(url, username, password)
                // 1.全局配置
                .globalConfig(builder -> {
                    builder.author(authorName)                // 设置作者
                            .enableSpringdoc()               // 开启 swagger 模式
                            // 获取当前工程路径并定位到项目java目录下
                            .outputDir(System.getProperty("user.dir") + "/src/main/java");            // 指定输出目录
                })

                // 2.数据源配置
                .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
                    int typeCode = metaInfo.getJdbcType().TYPE_CODE;
                    if (typeCode == Types.SMALLINT) {
                        // 自定义类型转换
                        return DbColumnType.INTEGER;
                    }
                    return typeRegistry.getColumnType(metaInfo);

                }))

                // 3.包名策略配置
                .packageConfig(builder -> {
                    builder.parent(parentPackageNameJava) // 设置父包名
                            .entity("entity")
                            .mapper("mapper")
                            .service("service")
                            .serviceImpl("service.impl")
                            .controller("controller")
                            //.moduleName("system") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/" + parentPackageNameResource + "/mapper")); // 设置mapperXml生成路径
                })
                // 策略配置
                .strategyConfig(builder -> {
                    builder.addInclude(tableName) // 设置需要生成的表名
                            // 覆盖已生成文件
                            .entityBuilder().enableFileOverride()
                            .mapperBuilder().enableFileOverride()
                            .serviceBuilder().enableFileOverride().formatServiceFileName("%sService");
                            //.addTablePrefix("t_", "c_"); // 设置过滤表前缀

                })

                // 配置模板
                .templateConfig(builder -> {
                    //builder.controller("");         // 不生成controller
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}
  • 修改数据库相关信息
  • 修改authorName
  • 修改parentPackageNameResource, parentPackageNameJava
  • 修改tableName, 要生成代码的数据表名称,多个表使用,分割
  • .enableSpringdoc() 可以选择,生成swagger3文档注释
    修改完成后运行main函数,生成相应代码,mapper.xml在resource下与java同路径下
    在这里插入图片描述
    实体类import io.swagger.v3.oas.annotations.media.Schema;报错,是因为swagger3依赖还未导入,下边整合swagger3后就不会报错了

1.5.3 配置分页插件

新建config包,并在下边创建配置类MybatisPlusConfig.java
在这里插入图片描述

package com.buzhisuoyun.base_manage.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com/buzhisuoyun/base_manage/mapper")
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
        //interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
        return interceptor;
    }
}

@MapperScan(“com/buzhisuoyun/base_manage/mapper”) 扫描mapper路劲,也可在启动类配置

1.6 整合swagger3(knife4j)

1.6.1 整合

引入依赖

<!-- API 文档 knife4j -->
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

配置:

# springdoc-openapi 配置
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'default'
      paths-to-match: '/**'
      packages-to-scan: com.buzhisuoyun.base_manage

# knife4j 配置
knife4j:
  # 是否启用增强
  enable: true
  # 开启生产环境屏蔽
  production: false
  # 是否认证登录
  basic:
    # basic是否开启,默认为false
    enable: true
    username: knife4j
    password: knife4j
  setting:
    language: zh_cn
    enable-version: true
    enable-swagger-models: true

在config下创建配置类

package com.buzhisuoyun.base_manage.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Knife4jConfig {
    @Bean
    public OpenAPI springShopOpenApi() {
        return new OpenAPI()
                // 接口文档标题
                .info(new Info().title("接口文档")
                        .description("api接口文档")
                        .version("1.0版本")
                );
    }
}

配置完后,启动项目,访问路径http://localhost:9999/doc.html,用户名密码就是配置文件中的,结果如图
在这里插入图片描述

1.6.2 使用

实体类使用@Schema 注解,在mybatis-plus选择的话会自动生成
在这里插入图片描述
Conntroller 层使用:

@Tag(name = "用户接口")
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
/**
     * 用户列表分页
     * @param pageSize 每页显示的条数
     * @param currentPage  要查询的页
     * @param name  用户姓名
     * @return  Result<PageResultBean<List<User>>>
     */
    @GetMapping("/pageList")
    @Operation(summary = "用户列表分页查询")
    @Parameters({
            @Parameter(name = "Authorization", in = ParameterIn.HEADER, required = true, description = "token"),
            @Parameter(name = "pageSize", required = true, description = "每页显示的条数"),
            @Parameter(name = "currentPage", required = true, description = "要查询的页"),
            @Parameter(name = "name", description = "用户姓名", required = false)
    })
    public Result<PageResultBean<User>> pageList(@RequestParam int pageSize, @RequestParam int currentPage, @Nullable @RequestParam String name) {
        IPage<User> page = userService.pageList(pageSize, currentPage, name);
        if (page == null) {
            return Result.error("查询失败");
        }
        //PageResultBean<User> pageResultBean = new PageResultBean<User>(page.getTotal(), page.getRecords());
        return Result.success(PageResultBean.getInstance(page.getTotal(), page.getRecords()));

    }
}

在这里插入图片描述

1.7 数据交互处理

1.7.1 响应数据封装(公共返回数据类)

封装返回数据封装类,放到common包下:
在这里插入图片描述

package com.buzhisuoyun.base_manage.common;

import io.swagger.v3.oas.annotations.media.Schema;

public class Result<T> {
    @Schema(description = "业务状态码 0:成功  1: 失败")
    private int code;        // 业务状态码 0:成功  1: 失败
    @Schema(description = "提示信息")
    private String message;  // 提示信息
    @Schema(description = "返回数据")
    private T data;          // 响应数据

    public Result(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // 操作成功返回响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    public static <E> Result<E> success() {
        return new Result<>(0, "操作成功", null);
    }

    public static <E> Result<E> error(String message) {
        return new Result<>(1, message, null);
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "Result{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }
}

1.7.2 分页查询返回数据封装

封装分页查询数据,放到common包下:
在这里插入图片描述

package com.buzhisuoyun.base_manage.common;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

public class PageResultBean<T> {
    @Schema(description = "数据总条数")
    private Long total;
    @Schema(description = "当前页数据集合")
    private List<T> items;

    public PageResultBean() {
    }

    public PageResultBean(Long total, List<T> items) {
        this.total = total;
        this.items = items;
    }

    public static <E> PageResultBean<E> getInstance(Long total, List<E> items) {
        return new PageResultBean<>(total, items);
    }

    public Long getTotal() {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total;
    }

    public List<T> getItems() {
        return items;
    }

    public void setItems(List<T> items) {
        this.items = items;
    }
}

1.8 全局异常处理

创建xception包,在包下放全局异常处理类GlobalExceptionHandler.java,类名放置 位置可以随意,为便于分类整理, 同类包放到一个包下
xception包
使用springboot @RestControllerAdvice 注解配置

package com.buzhisuoyun.base_manage.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
    private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    @ExceptionHandler(Exception.class)
    public Result<String> handlerException(Exception e) {
        logger.warn(e.getMessage());
        return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
    }
}

1.9 整合JWT,生成token

引入依赖

 <!-- jwt -->
 <dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>4.4.0</version>
 </dependency>

封装工具类, 放到utils包下:

package com.buzhisuoyun.base_manage.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 = "buzhisuoyun";   // 密钥

    // 接收数据,生成token并返回
    public static String getToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60))       // 失效时间1小时
                .sign(Algorithm.HMAC256(KEY));
    }

    // 接收token,验证并返回数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }
}

1.10 封装ThreadLocal 工具类

因项目使用前后端分离,使用ThreadLocal 线程变量存储用户登录信息,替代session

package com.buzhisuoyun.base_manage.utils;

public class ThreadLocalUtil {
    // 提供ThreadLocal 对象
    private static final ThreadLocal<Object> THREAD_LOCAL = new ThreadLocal<>();

    // 获取存储值
    public static <T> T get() {
        return (T) THREAD_LOCAL.get();
    }

    // 存储值
    public static void set(Object value) {
        THREAD_LOCAL.set(value);
    }

    // 清除THREAD_LOCAL 防止内存泄漏
    public static void remove() {
        THREAD_LOCAL.remove();
    }
}

1.11 MD5封装

package com.buzhisuoyun.base_manage.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

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

    protected static MessageDigest messagedigest = null;

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

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

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


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

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

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

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

    // 测试
    public static void main(String[] args) {
        System.out.println(Md5Util.getMD5String("admin"));
    }

}

1.12登录验证拦截

1、创建包interceptors,在包下配置登录拦截器

package com.buzhisuoyun.base_manage.interceptors;


import com.buzhisuoyun.base_manage.utils.JwtUtil;
import com.buzhisuoyun.base_manage.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取token
        String token = request.getHeader("Authorization");
        // 验证token
        try {
            Map<String, Object> claims = JwtUtil.parseToken(token);
            // 存储业务信息到线程变量
            ThreadLocalUtil.set(claims);
            // 放行拦截
            return true;
        } catch (Exception e) {
            // 登录信息异常或未登录,http响应状态码为401
            response.setStatus(401);
            // 拦截请求
            return false;
        }
    }

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

2、在config包下创建WebConfig配置类,注册登录拦截器

package com.buzhisuoyun.base_manage.config;

import com.buzhisuoyun.base_manage.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 配置放行资源
        // 无需拦截的接口集合
        List<String> ignorePath = new ArrayList<>();
        // knife4j(swagger)
        ignorePath.add("/swagger-resources/**");
        ignorePath.add("/doc.html");
        ignorePath.add("/v3/**");
        ignorePath.add("/webjars/**");
        ignorePath.add("/static/**");
        ignorePath.add("/templates/**");
        ignorePath.add("/error");
        // 登录页面
        ignorePath.add("/user/login");
        registry.addInterceptor(loginInterceptor).excludePathPatterns(ignorePath);
    }
}

1.13 登录和获取当前用户信息接口处理

	@PostMapping("/login")
    @Operation(summary = "用户登录")
    @Parameters({
            @Parameter(name = "loginName", description = "登录名", required = true, schema = @Schema(type = "sting")),
            @Parameter(name = "password", description = "密码", required = true, schema = @Schema(type = "sting"))
    })
    public Result<String> login(@RequestBody User user) {
        if (user == null || "".equals(user.getLoginName()) || user.getLoginName() == null || "".equals(user.getPassword()) || user.getPassword() == null) {
            return Result.error("用户名密码不能为空");
        }
        // 检验用户名是否存在
        User eruser = userService.findByLoginName(user.getLoginName());
        if (eruser == null) {
            return Result.error("用户名不存在");
        }
        // 检验用户密码是否正确
        if (Md5Util.getMD5String(user.getPassword()).equals(eruser.getPassword())) {
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", eruser.getId());
            claims.put("name", eruser.getName());
            claims.put("loginName", eruser.getLoginName());
            String token = JwtUtil.getToken(claims);
            return Result.success(token);
        }
        return Result.error("密码错误");
    }



/**
     * 获取当前登录用户信息
     * @return  User
     */
    @GetMapping("/currentUser")
    @Operation(summary = "获取当前登录用户信息")
    @Parameter(name = "Authorization", in = ParameterIn.HEADER, required = true, description = "token")
    public Result<User> getCurrentUser() {
        Map<String, Object> userSession = ThreadLocalUtil.get();
        int id = (int) userSession.get("id");
        User user = userService.getUserById(id);
        if (user != null) {
            return Result.success(user);
        }
        return Result.error("用户状态异常");
    }

因为是做简单后端项目搭建,所以token存在一定问题,如在修改用户密码后必须强制重新登录或采用redis缓存token,同时验证浏览器token和用户token,或采用第三方认证中心解决等
项目工程结构如图
在这里插入图片描述

2. 前端项目搭建

2.1 环境准备

- node安装
- vscode安装

2.2 创建Vue3项目

在将要存放vue3项目的路径打开cmd, 使用以下命令创建项目

npm init vue@latest

在这里插入图片描述
此时项目创建完成,vscode打开项目目录,在资源目录空白右键,打开终端
在这里插入图片描述
执行命令 npm install安装依赖,等待安装完成后执行 npm run dev 运行项目
在这里插入图片描述
访问路径可访问项目
在这里插入图片描述
在终端ctrl c 可停止运行项目
项目描述如图,图引自黑马开源教程PPT
在这里插入图片描述

2.3 项目搭建准备

说明:项目中使用组合式API
删掉components下所有文件,删除App.vue文件不需要的东西,最后如下所示

<script setup>

</script>

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

<style scoped>

</style>

2.4 Element Plus 安装使用

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')
  • 使用: 访问Element官方文档,复制组件代码,修改调整

2.5 axios 安装使用

2.5.1 安装

npm install axios

2.5.2 配置(创建实例,配置请求、响应拦截器)

在src目录下新建 utils, 并在utils 下创建request.js 进行axios配置
在这里插入图片描述

// 请求配置

import axios from "axios";

// 定义公共前缀,创建请求实例
// const baseUrl = "http://localhost:8080";
const baseURL = '/api/';
const instance = axios.create({baseURL})


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


import router from "@/router";
// 添加响应拦截器
instance.interceptors.response.use(
    result => {
        // 判断业务状态码
        if (result.data.code === 0) {
            return result.data;
        }
        // 操作失败
        ElMessage.error(result.data.message ? result.data.message : '服务异常')
        // 异步操作的状态转换为失败
        return Promise.reject(result.data)
    },
    err => {
        // 判断响应状态码, 401为未登录,提示登录并跳转到登录页面
        if (err.response.status === 401) {
            ElMessage.error('请先登录')
            router.push('/login')
        } else {
            ElMessage.error('服务异常')
        }
        // 异步操作的状态转换为失败
        return Promise.reject(err)  
    }
)

export default instance

2.5.3 配置跨域

在vite.config.js 中defineConfig配置代理,实现跨域

server: {
    proxy: {
        '/api': {   // 获取路径中包含了/api的请求
            target: 'http://localhost:9999',        // 服务端地址
            changeOrigin: true, // 修改源
            rewrite:(path) => path.replace(/^\/api/, '')   // api 替换为 ''
        }
    }
  }

在这里插入图片描述

2.6 Vue Router 安装使用

  • 安装 npm install vue-router@4
  • 在src/router/index.js中创建路由器,并导出
// 导入vue-router
import {createRouter, createWebHistory} from 'vue-router'

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

// 定义路由关系
const routes = [
    {path: '/login', component: LoginVue},
    {
        path: '/', component: LayoutVue, redirect: '', children: [
            {path: '/user/userlist', name: "/user/userlist", component: UserList, meta: {
                title: "用户列表"
              },},
            {path: '/user/editpassword', name: "/user/editpassword", component: EditPassword, meta: {
                title: "修改密码"
            }
            },
            {path: '/user/displayuser', name: "/user/displayuser", component: DisplayUser, meta: {
                title: "个人信息"
            }}
        ]
    }
]

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

export default router
  • 在vue应用实例中使用vue-router
在main.js 中

import router from '@/router'
app.use(router)
  • 声明router-view标签,展示组件内容
在app.vue 中

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

2.7 Pinia状态管理库

  • 安装 npm install pinia

2.7.1 Pinia持久化插件-persist

  • 安装persist npm install pinia-persistedstate-plugin
  • 在pinia中使用persist
main.js

import {createPersistedState} from'pinia-persistedstate-plugin’ const persist = createPersistedState()
pinia.use(persist)
  • 定义状态Store时指定持久化配置参数
    在这里插入图片描述

  • 在src/stores/下定义token.js和userInfo.js 用来存储token和用户相关信息

token.js

// 定义 store
import { defineStore } from "pinia"
import {ref} from 'vue'
/*
    第一个参数:名字,唯一性
    第二个参数:函数,函数的内部可以定义状态的所有内容

    返回值: 函数
 */
export const useTokenStore = defineStore('token', () => {
    // 响应式变量
    const token = ref('')

    // 修改token值函数
    const setToken = (newToken) => {
        token.value = newToken
    }

    // 移除token值函数
    const removeToke = () => {
        token.value = ''
    }

    return {
        token, setToken, removeToke
    }
}, 
{
    persist: true   // 持久化存储
}
)
userInfo.js 

import { defineStore } from "pinia"
import {ref} from 'vue'

const useUserInfoStore = defineStore('userInfo', () => {
    const info = ref({})
    
    const setInfo = (newInfo) => {
        info.value = newInfo
    }

    const removeInfo = () => {
        info.value = {}
    }

    return {info, setInfo, removeInfo}
},
{
    persist: true
}
)

export default useUserInfoStore;
  • 在组件中使用store 示例
import {userLoginService} from '@/api/user.js'
import {useTokenStore} from '@/stores/token.js'
import {useRouter} from 'vue-router'
const router = useRouter()
const tokenStore = useTokenStore();
const login = async ()=>{
    // 校验表单
    if (!ruleFormRef.value) return
    console.log("校验")
    await ruleFormRef.value.validate(async (valid) => {
        if (valid) {
            console.log("校验成功")
            // 调用接口,完成登录
            let result = await userLoginService(registerData.value);
            /* if(result.code===0){
                alert(result.msg? result.msg : '登录成功')
            }else{
                alert('登录失败')
            } */
            //alert(result.msg? result.msg : '登录成功')
            ElMessage.success(result.msg ? result.msg : '登录成功')
            //token存储到pinia中
            tokenStore.setToken(result.data)
            //跳转到首页 路由完成跳转
            router.push('/')
        } else {
            console.log("校验失败")
        }
    })
}



// 配置请求拦截器
instance.interceptors.request.use(
    (config) => {
        // 请求前回调
        // 添加token
        const tokenStore = useTokenStore()
        // 判断有无token
        if (tokenStore.token) {
            config.headers.Authorization = tokenStore.token
        }
        return config
    },
    (err) => {
        // 请求错误的回调
        Promise.reject(err)
    }
)

2.8 搭建管理页面基础框架

2.8.1 在src下创建api目录,次目录存放请求http方法的封装,创建user.js, 里边写封装请求方法

import request from "@/utils/request.js"

// 登录接口调用函数
export const userLoginService = (loginData) => {
    return request.post('/user/login', loginData)
}

// 获取当前登录用户信息
export const currentUserService = () => {
    return request.get('/user/currentUser')
}

// 获取所有用户信息
export const allUserService = () => {
    return request.get('/user/userList')
}

// 分页查询
export const pageListService = (pageParam) => {
    return request.get('/user/pageList', {params: pageParam})
}

// 新增用户
export const addUserService = (addData) => {
    return request.post('/user/add', addData)
}

// 根据id获取用户信息
export const getUserById = (id) => {
    return request.get('/user/getuserById', {params: id})
}

// 修改用户信息
export const updateUserService = (data) => {
    return request.put('/user/update', data)
}

// 删除用户
export const deleteByIdService = (id) => {
    console.log("deleteRequestid:", id)
    return request.delete('/user/delete/' + id)
}

2.8.2 登录页面

  • 安装 npm install sass -D

在src下创建views目录,用于存放vue页面组件
在这里插入图片描述
登录 Login.vue

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
//定义数据模型
const registerData = ref({
    loginName: 'admin',
    password:'admin',
    rePassword: ''
})

// 定义表单组件的引用
const ruleFormRef = ref(null)

//定义表单校验规则
const rules = ref({
    loginName: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 5, max: 16, message: '长度为5~16位非空字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 5, max: 16, 2: '长度为5~16位非空字符', trigger: 'blur' }
    ]
})

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

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

<template>
    <el-row class="login-page">
        <el-col :span="12" class="bg"></el-col>
        <el-col :span="6" :offset="3" class="form">
            <!-- 登录表单 -->
            <el-form ref="ruleFormRef" :model=registerData size="large" autocomplete="off" :rules="rules">
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="loginName">
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.loginName"></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item class="flex">
                    <div class="flex">
                        <el-checkbox>记住我</el-checkbox>
                        <!-- <el-link type="primary" :underline="false">忘记密码?</el-link> -->
                    </div>
                </el-form-item>
                <!-- 登录按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
                </el-form-item>
            </el-form>
        </el-col>
    </el-row>
</template>

<style lang="scss" scoped>
/* 样式 */
.login-page {
    height: 100vh;
    background-color: #fff;

    .bg {
        background: 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>

2.8.3 布局页面

  • Layout .vue
<script setup>
import LeftLayout from './LeftLayout.vue'
import Header from './Header.vue'
import MainView from './MainView.vue'

import {ref} from 'vue'
const isCollapse = ref(false)
const parentClick = (isCollapseValue) => {
    isCollapse.value = isCollapseValue;
    console.log(isCollapse.value)
}
</script>


<template>
  <div class="common-layout">
    <el-container>
      <LeftLayout :isCollapse='isCollapse' />
      <el-container>
        <el-header style="padding: 0"><Header @parentClick='parentClick'/></el-header>
        
        <el-main style="padding: 16px 8px 6px 8px"><MainView/></el-main>
        <el-footer>后台 ©2024 Created by buzhisuoyun</el-footer>
      </el-container>
    </el-container>
  </div>
</template>

<style scoped>
    .el-footer {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 14px;
        color: #666;
        height: 38px;
        padding: 0;
        background-color: #FFFFFF; 
    }
</style>
  • LeftLayout.vue
<template>
  <el-row class="tac">
    <el-col >
      <el-menu
        default-active="2"
        class="el-menu-vertical-demo"
        :collapse="isCollapse"
        :router="true"
      >
        <!-- 标题 -->
        <div class="containerdiv">  
            <img src="../assets/favicon.ico" alt="Your Image" class="image">  
            <span class="text">后台管理</span>  
        </div>

        <!-- 菜单 -->
        <el-sub-menu index="1">
          <template #title>
            <el-icon><Share /></el-icon>
            <span>API管理</span>
          </template>
            <el-menu-item index="/api/apilist">API列表</el-menu-item>
            <el-menu-item index="1-2">item two</el-menu-item>
            <el-menu-item index="1-3">item three</el-menu-item>
          <el-sub-menu index="1-4">
            <template #title>item four</template>
            <el-menu-item index="1-4-1">item one</el-menu-item>
          </el-sub-menu>
        </el-sub-menu>
        <el-menu-item index="2">
          <el-icon><icon-menu /></el-icon>
          <span>Navigator Two</span>
        </el-menu-item>
        <el-menu-item index="3" disabled>
          <el-icon><document /></el-icon>
          <span>Navigator Three</span>
        </el-menu-item>
        <el-menu-item index="4">
          <el-icon><setting /></el-icon>
          <span>Navigator Four</span>
        </el-menu-item>
        <el-sub-menu index="5">
          <template #title>
            <el-icon><UserFilled /></el-icon>
            <span>用户管理</span>
          </template>
            <el-menu-item index="/user/userlist">用户列表</el-menu-item>
            <el-menu-item index="/user/displayuser">个人信息</el-menu-item>
            <el-menu-item index="/user/editpassword">修改密码</el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-col>
  </el-row>
</template>

<script lang="ts" setup>
import {
  Document,
  Menu as IconMenu,
  Location,
  Share,
  UserFilled,
  Setting,
} from '@element-plus/icons-vue'
import {ref, defineProps} from 'vue'

type Props = {
    isCollapse: boolean
}
defineProps<Props>()

</script>

<style scoped>
    .el-menu-vertical-demo {
        height: 100vh;
    }
    .el-menu-item {
        min-width: 0;
    }
    .containerdiv {  
        /* 你可以设置容器的样式,例如宽度、高度、背景色等 */  
        /* width: 300px; /* 示例宽度 */  
        height: 48px;  
        
        padding: 10px; /* 内边距 */  
        border-bottom: 2px solid; /* 设置下边框宽度和样式 */  
        border-bottom-color: #F5F5F5; /* 设置下边框颜色为红色 */  
    }  
  
    .image {  
        display: inline-block;  
        vertical-align: middle; /* 图片与文字垂直居中对齐 */  
        margin-right: 6px; /* 图片右边距,可选 */  
        width: 20px;
    }  
    
    .text {  
        display: inline-block;  
        vertical-align: middle; /* 文字与图片垂直居中对齐 */  
        font-weight: bold; /* 加粗文字 */  
        font-size: 14px;
    }
</style>
  • Heard.vue
<template>
    <div class="container">
        <!-- div left -->
        <div class="left">
            <!-- 折叠按钮-->
            <div @click="toggleCollapse()">
                <el-icon size="24" v-show="!isMenuOpen"><Fold /></el-icon>
                <el-icon size="24" v-show="isMenuOpen"><Expand /></el-icon>
            </div>
            <!-- 面包屑 -->
            <div>
                <el-breadcrumb separator="/">
                    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                    <template v-for="(item, index) in breadList">
                    <el-breadcrumb-item
                        v-if="item.name"
                        :key="index"
                        :to="item.path"
                    >{{ item.meta.title }}</el-breadcrumb-item>
                    </template>
                </el-breadcrumb>
            </div>
        </div>
        <!-- div right -->
        <div class="right">
            <div>
                <span >账号:{{userData.loginName}}</span>
            </div>
            <div>
                <el-avatar> {{userData.name}} </el-avatar>
            </div>
            <div>
                <el-dropdown>
                <el-icon size="24"><MoreFilled /></el-icon>
                <template #dropdown>
                <el-dropdown-menu>
                    <el-dropdown-item>
                        <el-icon><UserFilled /></el-icon>个人信息
                    </el-dropdown-item>
                    <el-dropdown-item>
                        <el-icon><EditPen /></el-icon>修改密码
                    </el-dropdown-item>
                    <el-dropdown-item>
                        <el-icon><ArrowLeft /></el-icon>退出登录
                    </el-dropdown-item>
                </el-dropdown-menu>
                </template>
            </el-dropdown>
            </div>
        </div>
    </div>
</template>

<script setup>
    import {
        Fold,
        Expand,
        MoreFilled,
        EditPen,
        ArrowLeft,
        UserFilled
    } from '@element-plus/icons-vue'
    import {ref, defineEmits, watch} from 'vue'

    // 面包屑
    import { useRouter,useRoute } from 'vue-router'

    let router = useRouter()
    let route = useRoute()

    let breadList = ref()
    let getMatched=()=>{
        console.log("route.matched:",route.matched);
        console
        breadList.value = route.matched.filter(item => item.meta && item.meta.title);
    }
    getMatched()
    
    watch(() => route.path, (newValue, oldValue) => { //监听路由路径是否发生变化,之后更改面包屑
        console.log("======")
        breadList.value = route.matched.filter(item => 
            item.meta && item.meta.title
    );
        console.log("breadList.value", breadList.value)
    })



    import useUserInfoStore from '@/stores/userInfo.js'
    const userInfoStore = useUserInfoStore();

    // 用户数据模型
    let userData = ref({
        id: '',
        name: '',
        loginName: ''
    })


    import {currentUserService} from '@/api/user.js'
    // 获取登录用户信息
    const getUser = async () => {
        let result = await currentUserService()
        // console.log(result)
        userData.value = result.data;
        userInfoStore.setInfo(result.data)
        // console.log("userData:",userData)
    }
    getUser()


    // 折叠按钮处理
    const emits = defineEmits(["parentClick"])
    const isMenuOpen = ref(false)
    const toggleCollapse = () => {
        isMenuOpen.value = !isMenuOpen.value
        console.log(isMenuOpen.value)
        emits("parentClick", isMenuOpen.value)
    }


</script>

<style lang="scss" scope>
    .container {  
        overflow: auto; /* 清除浮动影响 */  
        height: 48px;
        padding: 10px; /* 内边距 */  
        border-bottom: 2px solid; /* 设置下边框宽度和样式 */  
        border-bottom-color: #F5F5F5; /* 设置下边框颜色为红色 */  
        background-color: #FFFFFF;   
    }  
    
    .left {  
        height: 48px;
        float: left;   
        display: flex;
        align-items: center; /* 垂直居中子项 */  
        justify-content: center; /* 水平居中子项(如果需要)*/ 
    }  

    .left > div {  
        padding-right: 10px; /* 设置直接子元素的 padding */  
    }
    
    .right {  
        float: right; 
        display: flex; 
        align-items: center; /* 垂直居中子项 */  
        justify-content: center; /* 水平居中子项(如果需要)*/   
    }
    .right > div {  
        padding-right: 10px; /* 设置直接子元素的 padding */  
    }
</style>
  • MainView.vue
<template>
	<div class="app-main">
		<!-- <transition name="fade-transfrom" mode="out-in">
			<router-view></router-view>
		</transition> -->
        <router-view v-slot="{ Component }">
            <transition>
                <component :is="Component" />
            </transition>
        </router-view>
	</div>
</template>

<style lang="scss" scope>
	.app-main{
		width:100%;
		height:100%;
        background-color: #FFFFFF; 
	}
</style>

项目结构:
在这里插入图片描述

3.项目代码

https://download.csdn.net/download/qq_51355375/89085020

标签:return,user,基座,token,SpringBoot3,Vue3,import,public,const
From: https://blog.csdn.net/qq_51355375/article/details/137360256

相关文章

  • vue3创建项目实例
    在控制台输入vuecreate文件名(vue3_basis) 选择第三个,默认自行选择安装 选择Router和vuex  选择3.x->vue3全部的选择项如下图 全部选择完后,开始构建项目 ......
  • Vue3 Diff 之 patchKeyedChildren 动态示例
    在学习全网学习各路大神的关于Vue3Diff算法分析文章的时候,一定离不开关键方法patchKeyedChildren。patchKeyedChildren处理的场景比较多,大致有5个主要过程。如果你希望查看不同测试用例下,patchKeyedChildren具体的内部处理过程,可以尝试一下这个:《Vue3Diff之patchKey......
  • VUE3 使用资源路径加载
    1.使用场景有些情况下,我需要使用组件路径动态的方式加载组件。2.实现方法import{defineAsyncComponent}from'vue';/***根据view组件路径异步加载组件.*@param{String}view组件路径这个文件在views下.*@returns{*}*/AppUtil.loadComponent=function(v......
  • Java登陆第三十七天——VUE3响应式基础、条件渲染、列表渲染
    响应式数据什么是响应式数据? 当数据发生改变时,DOM树的内容,会和数据同步更新。 vue3不是自动响应式数据,需要经过函数处理得到响应式数据对象。ref和reactive这两个函数都会返回响应式数据对象,但也有不同。refref通常用于将一个基本类型转为响应式数据对象。基本类型包括:数......
  • Java登陆第三十七天——VUE3插值表达式、文本渲染、属性渲染、事件绑定
    插值表达式VUE中最基本的数据绑定形式。语法格式如下:{{数据来源}}插值表达式的特点:不依靠标签。可以调用函数。支持运算符。栗子App.vue<scriptsetup>letmsg="字符串";letnum=10;functionf1(){return"有返回值的方法"}letf2=()=>{......
  • Vue3+vite打包Failed to load module script
    完整報錯信息:Failedtoloadmodulescript:ExpectedaJavaScriptmodulescriptbuttheserverrespondedwithaMIMEtypeof"text/html".StrictMIMEtypecheckingisenforcedformodulescriptsperHTMLspec.目前vite.config.js打包寫法://拆分js到......
  • Vue3+TypeScript项目(SKU管理模块)
    一、SKU模块静态页面src\views\product\sku\index.vue<template><el-card><el-tableborderstyle="margin:10px0px"><el-table-columntype="index"label="序号"width="80px"></el-table......
  • vue3配置使用环境变量.env.development
    参考:https://blog.csdn.net/perfect2011/article/details/1299308191.新建环境配置文件,根目录.env基础系统变量,无论何种环境,都可使用其中配置的值,其他环境中的变量会覆盖.env中的同名变量。.env.development开发环境.env.production生产环境.env.staging测试环境2.vue3使用vit......
  • 重读vue3
    基础风格指南结构风格vue推荐:先声明,后使用<scriptsetup></script><template></template><stylescoped></style>子组件命名<!--在单文件组件中,推荐为子组件使用PascalCase的标签名,以此来和原生的HTML元素作区分--><!--✅:推荐风格:PascalCase,文件名使用keba......
  • vue3 手机端 手写签字
    <template><div><div><canvasclass="canvas"id="canvas"ref="canvas"></canvas><canvasid="blank"style="display:none"></canvas><p......