前几天有个粉丝跟我说:“现在AI影响太大了,公司全面要求开始搞全栈。前端的一起写后端,后端的一起写前端。虽然AI能生成代码,可一旦报错或者需要微调的时候,实在是太难受了”。
AI是放大器,但它不能替代你的基本功,所以今天我就基于 Vue3 + SpringBoot 实现一个注册登录的前后端分离的项目。
一、功能需求描述(注册 + 登录)
在正式开始写代码之前,我们先明确一下这次要实现的完整功能。我们要做的是一个完整的用户注册与登录模块,这也是绝大多数 Web 应用的入口功能。
前后端交互流程
【注册流程】┌─────────┐ ┌─────────┐ ┌─────────┐│ 前端 │ │ 后端 │ │ 数据库 │└────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ ① 输入用户名失焦 │ │ │──校验用户名────────>│ │ │<──是否可用──────────│ │ │ │ │ │ ② POST /api/auth/register │ │───────────────────>│ │ │ │ ③ 校验用户名唯一 │ │ │───────────────────>│ │ │ │ │ │ ④ 密码加密存库 │ │ │───────────────────>│ │ │ │ │ ⑤ 注册成功/失败 │ │ │<───────────────────│ │ │ │ │ │ ⑥ 自动跳转登录页 │ │【登录流程】┌─────────┐ ┌─────────┐ ┌─────────┐│ 前端 │ │ 后端 │ │ 数据库 │└────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ ① POST /api/auth/login │ │───────────────────>│ │ │ │ ② 查询用户 │ │ │───────────────────>│ │ │<───────────────────│ │ │ │ │ │ ③ 验证密码+状态 │ │ │ │ │ │ ④ 生成 JWT Token │ │ │ │ │ ⑤ 返回Token+用户信息│ │ │<───────────────────│ │ │ │ │ │ ⑥ localStorage存储 │ │ │ │ │ │ ⑦ 跳转首页 │ │麻雀虽小但五脏俱全,通过这个功能你可以掌握前后端分离开发的核心流程。接下来直接跟着代码复制粘贴就可以了。
项目效果图:



二、环境前置条件
在开始之前,你的电脑至少要准备这些环境和工具:
• Node.js:版本 16.0 或更高(用于运行 Vue3 项目) • JDK 17:用于运行 SpringBoot 项目 • MySQL:版本 5.7 或 8.0(作为数据库) • Maven:版本 3.6 或更高(用于管理 SpringBoot 依赖) • IDE:推荐 IntelliJ IDEA(后端)和 VS Code(前端) • 接口测试工具:Postman 或 Apifox • 数据库管理工具:Navicat 或者 Dbeaver
三、技术选型说明
后端技术栈
前端技术栈
四、数据库设计
4.1 创建数据库
可以在工具新建

也可以直接执行下面这条sql。
CREATE DATABASE IF NOT EXISTS vue3_springboot_demoDEFAULT CHARACTER SET utf8mb4COLLATE utf8mb4_unicode_ci;意思是创建一个数据库,名字叫 vue3_springboot_demo,并使用 utf8mb4 字符编码(支持中文和表情),utf8mb4_unicode_ci 排序规则(不区分大小写、排序更准确)。
这里分版本,如果是 MySQL 5.7建议用 utf8mb4_unicode_ci,MySQL 8.0以上建议 utf8mb4_0900_ai_ci。
4.2 用户表结构
在工具里面执行下面的sql。

USE vue3_springboot_demo;CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码(加密存储)', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_email` (`email`), UNIQUE KEY `uk_phone` (`phone`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';执行后,刷新或者重新打开这个数据库,就能看到新建的数据表了。

五、后端项目搭建
创建项目可以先不用管具体代码的意思,大概看一下结构,或者方法就行。
5.1 创建 SpringBoot 项目
访问 Spring Initializr 快速创建项目:
https://start.spring.io/
添加依赖:Spring Web、Spring Security、MySQL Driver、Lombok。

然后点击下面的生成,就会把这个项目下载下来了。
5.2 用工具打开项目
下载解压后,用工具打开项目

打开项目后,再打开设置,设置一下你maven的路径。

5.3 配置 pom.xml
在已生成的基础上,添加以下依赖:

<dependencies> <!-- ...其它配置 --> <!-- validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot4-starter</artifactId> <version>3.5.15</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency></dependencies>5.4 配置 application.yml

server: port: 8080spring: datasource: url: jdbc:mysql://localhost:3306/vue3_springboot_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8 username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8# MyBatis-Plus 配置mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0# JWT 配置jwt: secret: your-256-bit-secret-key-here-please-change-it-in-production expiration: 86400000 # 24小时,单位毫秒 header: Authorization token-prefix: "Bearer "先来启动一下试试能不能跑的通,为了方便后面的调试和重启,这里先配一下

然后右键点击这个服务,就可以启动或者debug了,这里我们先选 Run。

如果看到右边的日志是这样的,就证明启动成功了,可以明显的看到我们的端口号。

如果发现报错,建议把报错信息丢给AI,当然也可以在评论区留下你的脚印。
5.5 项目结构设计
在项目的的 src/main/java/com/example/demo 包下,创建这些目录,也叫包。

src/main/java/com/example/demo├── config/ # 配置类├── controller/ # 控制器├── entity/ # 实体类├── mapper/ # Mapper 接口├── service/ # 服务层│ └── impl/ # 服务实现├── utils/ # 工具类├── security/ # 安全相关├── dto/ # 数据传输对象└── exception/ # 异常处理
5.6 核心代码实现
创建的时候选择默认的 class 类。
实体类 User.java
package com.example.demo.entity;import com.baomidou.mybatisplus.annotation.*;import lombok.Data;import java.time.LocalDateTime;@Data@TableName("user")public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private String email; private String phone; private String avatar; private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;}Mapper 接口 UserMapper.java
接口类要选择 interface。

package com.example.demo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.example.demo.entity.User;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface UserMapper extends BaseMapper<User> {}注册请求 DTO RegisterRequest.java
package com.example.demo.dto;import jakarta.validation.constraints.*;import lombok.Data;@Datapublic class RegisterRequest { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]+$", message = "密码必须包含字母和数字") private String password; @NotBlank(message = "确认密码不能为空") private String confirmPassword; @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phone;}登录请求 DTO LoginRequest.java
package com.example.demo.dto;import jakarta.validation.constraints.NotBlank;import lombok.Data;@Datapublic class LoginRequest { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password;}用户名校验请求 DTO UsernameCheckRequest.java
package com.example.demo.dto;import jakarta.validation.constraints.NotBlank;import lombok.Data;@Datapublic class UsernameCheckRequest { @NotBlank(message = "用户名不能为空") private String username;}登录/注册响应 DTO AuthResponse.java
package com.example.demo.dto;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class AuthResponse { private String token; private String tokenType; private String username; private String email; private String avatar;}统一响应结果 Result.java
package com.example.demo.dto;import lombok.Data;@Datapublic class Result<T> { private Integer code; private String message; private T data; private Result(Integer code, String message, T data) { this.code = code; this.message = message; this.data = data; } public static <T> Result<T> success(String message, T data) { return new Result<>(200, message, data); } public static <T> Result<T> success(T data) { return new Result<>(200, "操作成功", data); } public static <T> Result<T> success(String message) { return new Result<>(200, message, null); } public static <T> Result<T> error(String message) { return new Result<>(400, message, null); } public static <T> Result<T> error(Integer code, String message) { return new Result<>(code, message, null); }}JWT 工具类 JwtUtils.java
package com.example.demo.utils;import io.jsonwebtoken.*;import io.jsonwebtoken.security.Keys;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import javax.crypto.SecretKey;import java.nio.charset.StandardCharsets;import java.util.Date;@Componentpublic class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.token-prefix}") private String tokenPrefix; private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } // 生成 Token public String generateToken(String username) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(getSigningKey()) .compact(); } // 从 Token 中获取用户名 public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } // 验证 Token public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } public String getTokenPrefix() { return tokenPrefix; }}服务接口 UserService.java
package com.example.demo.service;import com.baomidou.mybatisplus.extension.service.IService;import com.example.demo.dto.AuthResponse;import com.example.demo.dto.LoginRequest;import com.example.demo.dto.RegisterRequest;import com.example.demo.entity.User;/** * 用户服务接口 * * 继承 MyBatis-Plus 的 IService,提供基础 CRUD 功能 * 并扩展用户登录、注册等业务方法 */public interface UserService extends IService<User> { /** * 用户登录 * * @param loginRequest 登录请求参数(用户名、密码) * @return 登录结果(包含 token、用户信息等) */ AuthResponse login(LoginRequest loginRequest); /** * 用户注册 * * @param registerRequest 注册请求参数 * @return 注册结果(包含 token、用户信息等) */ AuthResponse register(RegisterRequest registerRequest); /** * 根据用户名查询用户 * * @param username 用户名 * @return 用户信息 */ User findByUsername(String username); /** * 检查用户名是否存在 * * @param username 用户名 * @return true 存在 / false 不存在 */ boolean checkUsernameExists(String username); /** * 检查邮箱是否存在 * * @param email 邮箱 * @return true 存在 / false 不存在 */ boolean checkEmailExists(String email); /** * 检查手机号是否存在 * * @param phone 手机号 * @return true 存在 / false 不存在 */ boolean checkPhoneExists(String phone);}这里注意一下项目结构,因为刚刚在 service 包下创建了一个 impl 的包,直接创建会把 UserService 接口类放到 impl这个包下了。
.impl 删掉,等创建了这个接口类后再创建。
服务实现 UserServiceImpl.java
package com.example.demo.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.example.demo.dto.AuthResponse;import com.example.demo.dto.LoginRequest;import com.example.demo.dto.RegisterRequest;import com.example.demo.entity.User;import com.example.demo.mapper.UserMapper;import com.example.demo.service.UserService;import com.example.demo.utils.JwtUtils;import lombok.RequiredArgsConstructor;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service@RequiredArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { private final JwtUtils jwtUtils; private final BCryptPasswordEncoder passwordEncoder; @Override public AuthResponse login(LoginRequest loginRequest) { // 查询用户 User user = findByUsername(loginRequest.getUsername()); if (user == null) { throw new RuntimeException("用户名或密码错误"); } // 验证密码 if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { throw new RuntimeException("用户名或密码错误"); } // 检查用户状态 if (user.getStatus() == 0) { throw new RuntimeException("账号已被禁用"); } // 生成 Token String token = jwtUtils.generateToken(user.getUsername()); return AuthResponse.builder() .token(token) .tokenType(jwtUtils.getTokenPrefix().trim()) .username(user.getUsername()) .email(user.getEmail()) .avatar(user.getAvatar()) .build(); } @Override @Transactional public AuthResponse register(RegisterRequest registerRequest) { // 校验两次密码是否一致 if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) { throw new RuntimeException("两次输入的密码不一致"); } // 检查用户名是否已存在 if (checkUsernameExists(registerRequest.getUsername())) { throw new RuntimeException("用户名已被注册"); } // 检查邮箱是否已存在 if (registerRequest.getEmail() != null && checkEmailExists(registerRequest.getEmail())) { throw new RuntimeException("邮箱已被注册"); } // 检查手机号是否已存在 if (registerRequest.getPhone() != null && checkPhoneExists(registerRequest.getPhone())) { throw new RuntimeException("手机号已被注册"); } // 创建用户 User user = new User(); user.setUsername(registerRequest.getUsername()); user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); user.setEmail(registerRequest.getEmail()); user.setPhone(registerRequest.getPhone()); user.setStatus(1); save(user); // 注册成功后自动生成 Token(可选,也可以让用户重新登录) String token = jwtUtils.generateToken(user.getUsername()); return AuthResponse.builder() .token(token) .tokenType(jwtUtils.getTokenPrefix().trim()) .username(user.getUsername()) .email(user.getEmail()) .avatar(user.getAvatar()) .build(); } @Override public User findByUsername(String username) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); return getOne(wrapper); } @Override public boolean checkUsernameExists(String username) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); return count(wrapper) > 0; } @Override public boolean checkEmailExists(String email) { if (email == null) return false; LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getEmail, email); return count(wrapper) > 0; } @Override public boolean checkPhoneExists(String phone) { if (phone == null) return false; LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getPhone, phone); return count(wrapper) > 0; }}全局异常处理器 GlobalExceptionHandler.java
package com.example.demo.exception;import com.example.demo.dto.Result;import org.springframework.http.HttpStatus;import org.springframework.validation.BindException;import org.springframework.validation.FieldError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.stream.Collectors;@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result<String> handleRuntimeException(RuntimeException e) { return Result.error(e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result<String> handleValidationException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return Result.error(message); } @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result<String> handleBindException(BindException e) { String message = e.getBindingResult().getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return Result.error(message); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result<String> handleException(Exception e) { e.printStackTrace(); return Result.error(500, "服务器内部错误"); }}控制器 AuthController.java
package com.example.demo.controller;import com.example.demo.dto.*;import com.example.demo.service.UserService;import jakarta.validation.Valid;import lombok.RequiredArgsConstructor;import org.springframework.web.bind.annotation.*;@RestController@RequestMapping("/api/auth")@RequiredArgsConstructorpublic class AuthController { private final UserService userService; @PostMapping("/login") public Result<AuthResponse> login(@Valid @RequestBody LoginRequest loginRequest) { AuthResponse response = userService.login(loginRequest); return Result.success("登录成功", response); } @PostMapping("/register") public Result<AuthResponse> register(@Valid @RequestBody RegisterRequest registerRequest) { AuthResponse response = userService.register(registerRequest); return Result.success("注册成功", response); } @PostMapping("/check-username") public Result<Boolean> checkUsername(@Valid @RequestBody UsernameCheckRequest request) { boolean exists = userService.checkUsernameExists(request.getUsername()); return Result.success(exists ? "用户名已被占用" : "用户名可用", !exists); } @GetMapping("/test") public Result<String> test() { return Result.success("接口连通成功"); }}安全配置 SecurityConfig.java
package com.example.demo.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.web.SecurityFilterChain;@Configuration@EnableWebSecuritypublic class SecurityConfig { @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers( "/api/auth/login", "/api/auth/register", "/api/auth/check-username", "/api/auth/test" ).permitAll() .anyRequest().authenticated() ); return http.build(); }}跨域配置 SecurityConfig.java
package com.example.demo.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import org.springframework.web.filter.CorsFilter;import java.util.Arrays;@Configurationpublic class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许的域名(前端地址) config.setAllowedOrigins(Arrays.asList( "http://localhost:5173", "http://localhost:5174", "http://127.0.0.1:5173" )); // 允许的请求方法 config.setAllowedMethods(Arrays.asList( "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH" )); // 允许的请求头 config.setAllowedHeaders(Arrays.asList( "Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers" )); // 允许暴露的响应头(前端可以访问的头信息) config.setExposedHeaders(Arrays.asList( "Authorization", "Content-Disposition" )); // 是否允许携带 Cookie config.setAllowCredentials(true); // 预检请求的缓存时间(秒) config.setMaxAge(3600L); // 应用 CORS 配置到所有路径 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }}好了,springboot后端的代码暂时完成了,来看下整体的代码文件结构。

可真折腾,来启动一下项目,然后用 postman 测下 http://127.0.0.1:8080/api/auth/test 这个接口。

可算是跑通了。
如果有报错,建议报错信息丢给ai或者在评论区留言。
六、前端项目搭建
6.1 创建 Vue3 项目
cmd 打开命令执行窗口,执行 Vite 创建项目这个命令。
# 使用 Vite 创建项目npm create vite@latest vue-login-demo -- --template vue
然后打开 http://localhost:5173/,得到界面如下。

我们现在关掉命令执行窗口,用 vscode 打开项目,在项目根目录右键打开。

然后执行下面的命令
# 安装额外依赖npm install axios element-plus vue-router@4执行后可以从 package.json 看到是否已经安装。

6.2 项目结构设计
src/├── api/ # API 接口│ └── auth.ts├── assets/ # 静态资源├── components/ # 公共组件├── router/ # 路由配置│ └── index.ts├── utils/ # 工具函数│ └── request.ts├── types/ # 类型定义│ └── auth.ts├── views/ # 页面组件│ ├── Login.vue│ ├── Register.vue│ └── Home.vue├── App.vue # 根组件├── main.ts # 入口文件└── vite-env.d.ts # Vite 环境类型声明6.3 类型定义
创建 src/types/auth.ts:
// 登录请求参数export interface LoginRequest { username: string password: string}// 注册请求参数export interface RegisterRequest { username: string password: string confirmPassword: string email?: string phone?: string}// 用户名校验请求export interface UsernameCheckRequest { username: string}// 认证响应数据export interface AuthResponse { token: string tokenType: string username: string email: string | null avatar: string | null}// API 统一响应结构export interface ApiResponse<T = any> { code: number message: string data: T}// 用户信息(存储用)export interface UserInfo { username: string email: string | null avatar: string | null}// 用户名校验状态export interface UsernameCheckStatus { type: 'success' | 'danger' | 'info' | 'warning' text: string}// 表单规则类型export interface FormRule { required?: boolean message?: string trigger?: string | string[] min?: number max?: number pattern?: RegExp validator?: (rule: any, value: any, callback: any) => void}6.4 配置 main.ts
import { createApp } from 'vue'import ElementPlus from 'element-plus'import 'element-plus/dist/index.css'import * as ElementPlusIconsVue from '@element-plus/icons-vue'import App from './App.vue'import router from './router'const app = createApp(App)// 注册所有图标for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component)}app.use(ElementPlus)app.use(router)app.mount('#app')6.5 封装 Axios 请求
创建 src/utils/request.ts:
import axios from 'axios'import type { AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'axios'import { ElMessage } from 'element-plus'import type { ApiResponse } from '@/types/auth'// 创建 axios 实例const request = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 10000})// 请求拦截器request.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 从 localStorage 获取 token 并添加到请求头 const token = localStorage.getItem('token') if (token && config.headers) { config.headers['Authorization'] = `Bearer ${token}` } return config }, (error: AxiosError) => { return Promise.reject(error) })// 响应拦截器request.interceptors.response.use( (response: AxiosResponse<ApiResponse>) => { const res = response.data // 根据后端返回的状态码处理 if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message || '请求失败')) } return response }, (error: AxiosError<ApiResponse>) => { const message = error.response?.data?.message || error.message || '网络错误' ElMessage.error(message) return Promise.reject(error) })export default request6.6 编写 API 接口
创建 src/api/auth.ts:
import request from '@/utils/request'import type { LoginRequest, RegisterRequest, UsernameCheckRequest, AuthResponse, ApiResponse} from '@/types/auth'// 登录接口export function login(data: LoginRequest): Promise<ApiResponse<AuthResponse>> { return request({ url: '/auth/login', method: 'post', data }).then(res => res.data)}// 注册接口export function register(data: RegisterRequest): Promise<ApiResponse<AuthResponse>> { return request({ url: '/auth/register', method: 'post', data }).then(res => res.data)}// 检查用户名是否存在export function checkUsername(username: string): Promise<ApiResponse<boolean>> { return request({ url: '/auth/check-username', method: 'post', data: { username } as UsernameCheckRequest }).then(res => res.data)}// 测试接口export function testApi(): Promise<ApiResponse<string>> { return request({ url: '/auth/test', method: 'get' }).then(res => res.data)}6.7 创建登录页面
创建 src/views/Login.vue:
<template> <div class="login-container"> <el-card class="login-card"> <template #header> <div class="card-header"> <h2>用户登录</h2> </div> </template> <el-form ref="loginFormRef" :model="loginForm" :rules="rules" label-width="80px" > <el-form-item label="用户名" prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名" :prefix-icon="User" clearable /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" :prefix-icon="Lock" show-password clearable /> </el-form-item> <el-form-item> <el-button type="primary" :loading="loading" @click="handleLogin" style="width: 100%" > 登录 </el-button> </el-form-item> <div class="form-footer"> <span>还没有账号?</span> <el-link type="primary" @click="goToRegister">立即注册</el-link> </div> </el-form> </el-card> </div></template><script setup lang="ts">import { ref, reactive } from 'vue'import { useRouter } from 'vue-router'import { ElMessage, type FormInstance, type FormRules } from 'element-plus'import { User, Lock } from '@element-plus/icons-vue'import { login } from '@/api/auth'import type { LoginRequest } from '@/types/auth'const router = useRouter()const loginFormRef = ref<FormInstance>()const loading = ref(false)const loginForm = reactive<LoginRequest>({ username: '', password: ''})// 表单验证规则const rules: FormRules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' } ]}// 处理登录const handleLogin = async () => { if (!loginFormRef.value) return await loginFormRef.value.validate(async (valid) => { if (!valid) return loading.value = true try { const res = await login(loginForm) if (res.code === 200) { // 保存 token 和用户信息 localStorage.setItem('token', res.data.token) localStorage.setItem('userInfo', JSON.stringify(res.data)) ElMessage.success('登录成功!') // 跳转到首页 setTimeout(() => { router.push('/home') }, 1000) } } catch (error) { console.error('登录失败:', error) } finally { loading.value = false } })}// 跳转到注册页const goToRegister = () => { router.push('/register')}</script><style scoped>.login-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);}.login-card { width: 420px; border-radius: 8px;}.card-header { text-align: center;}.card-header h2 { margin: 0; color: #333;}.form-footer { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 10px;}.tips { text-align: center; margin-top: 10px;}</style>6.8 创建注册页面
创建 src/views/Register.vue:
<template> <div class="register-container"> <el-card class="register-card"> <template #header> <div class="card-header"> <h2>用户注册</h2> </div> </template> <el-form ref="registerFormRef" :model="registerForm" :rules="rules" label-width="100px" > <el-form-item label="用户名" prop="username"> <el-input v-model="registerForm.username" placeholder="请输入用户名(3-20位字母/数字/下划线)" :prefix-icon="User" clearable @blur="checkUsernameExists" > <template #append v-if="usernameCheckStatus"> <el-tag :type="usernameCheckStatus.type" size="small"> {{ usernameCheckStatus.text }} </el-tag> </template> </el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="registerForm.password" type="password" placeholder="请输入密码(6-20位,必须包含字母和数字)" :prefix-icon="Lock" show-password clearable /> <div class="password-strength" v-if="registerForm.password"> <el-progress :percentage="passwordStrength" :color="passwordStrengthColor" :stroke-width="6" /> </div> </el-form-item> <el-form-item label="确认密码" prop="confirmPassword"> <el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" :prefix-icon="Lock" show-password clearable /> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="registerForm.email" placeholder="请输入邮箱" :prefix-icon="Message" clearable /> </el-form-item> <el-form-item label="手机号" prop="phone"> <el-input v-model="registerForm.phone" placeholder="请输入手机号" :prefix-icon="Phone" clearable /> </el-form-item> <el-form-item> <el-button type="primary" :loading="loading" @click="handleRegister" style="width: 100%" > 注册 </el-button> </el-form-item> <div class="form-footer"> <span>已有账号?</span> <el-link type="primary" @click="goToLogin">立即登录</el-link> </div> </el-form> </el-card> </div></template><script setup lang="ts">import { ref, reactive, computed, watch } from 'vue'import { useRouter } from 'vue-router'import { ElMessage, type FormInstance, type FormRules } from 'element-plus'import { User, Lock, Message, Phone } from '@element-plus/icons-vue'import { register, checkUsername } from '@/api/auth'import type { RegisterRequest, UsernameCheckStatus } from '@/types/auth'const router = useRouter()const registerFormRef = ref<FormInstance>()const loading = ref(false)const registerForm = reactive<RegisterRequest>({ username: '', password: '', confirmPassword: '', email: '', phone: ''})// 用户名校验状态const usernameCheckStatus = ref<UsernameCheckStatus | null>(null)// 验证确认密码const validateConfirmPassword = (_rule: any, value: string, callback: (error?: Error) => void) => { if (value === '') { callback(new Error('请再次输入密码')) } else if (value !== registerForm.password) { callback(new Error('两次输入的密码不一致')) } else { callback() }}// 表单验证规则const rules: FormRules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }, { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }, { pattern: /^(?=.*[A-Za-z])(?=.*\d)/, message: '密码必须包含字母和数字', trigger: 'blur' } ], confirmPassword: [ { required: true, validator: validateConfirmPassword, trigger: 'blur' } ], email: [ { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' } ], phone: [ { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' } ]}// 密码强度计算const passwordStrength = computed(() => { const pwd = registerForm.password if (!pwd) return 0 let strength = 0 if (pwd.length >= 6) strength += 20 if (pwd.length >= 10) strength += 20 if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength += 20 if (/\d/.test(pwd)) strength += 20 if (/[^a-zA-Z0-9]/.test(pwd)) strength += 20 return Math.min(strength, 100)})// 密码强度颜色const passwordStrengthColor = computed(() => { const strength = passwordStrength.value if (strength < 40) return '#f56c6c' if (strength < 70) return '#e6a23c' return '#67c23a'})// 监听密码变化,触发确认密码校验watch(() => registerForm.password, () => { if (registerForm.confirmPassword) { registerFormRef.value?.validateField('confirmPassword') }})// 检查用户名是否存在const checkUsernameExists = async () => { if (!registerForm.username || registerForm.username.length < 3) { usernameCheckStatus.value = null return } try { const res = await checkUsername(registerForm.username) if (res.data) { usernameCheckStatus.value = { type: 'success', text: '用户名可用' } } else { usernameCheckStatus.value = { type: 'danger', text: '用户名已被占用' } } } catch (error) { usernameCheckStatus.value = null }}// 处理注册const handleRegister = async () => { if (!registerFormRef.value) return await registerFormRef.value.validate(async (valid) => { if (!valid) return // 再次检查用户名 if (usernameCheckStatus.value?.type === 'danger') { ElMessage.error('用户名已被占用,请更换') return } loading.value = true try { const res = await register(registerForm) if (res.code === 200) { ElMessage.success('注册成功!即将跳转到登录页...') setTimeout(() => { router.push('/login') }, 1500) } } catch (error) { console.error('注册失败:', error) } finally { loading.value = false } })}// 跳转到登录页const goToLogin = () => { router.push('/login')}</script><style scoped>.register-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px;}.register-card { width: 500px; border-radius: 8px;}.card-header { text-align: center;}.card-header h2 { margin: 0; color: #333;}.password-strength { margin-top: 8px;}.form-footer { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 10px;}</style>6.9 创建首页
创建 src/views/Home.vue:
<template> <div class="home-container"> <el-card class="welcome-card"> <template #header> <div class="card-header"> <h1>欢迎回来,{{ userInfo.username }}!</h1> </div> </template> <el-descriptions title="用户信息" :column="1" border> <el-descriptions-item label="用户名"> {{ userInfo.username }} </el-descriptions-item> <el-descriptions-item label="邮箱"> {{ userInfo.email || '未设置' }} </el-descriptions-item> <el-descriptions-item label="Token"> <el-tag type="success">{{ tokenPreview }}</el-tag> </el-descriptions-item> <el-descriptions-item label="头像"> <el-avatar v-if="userInfo.avatar" :src="userInfo.avatar"> {{ userInfo.username?.charAt(0) }} </el-avatar> <span v-else>未设置</span> </el-descriptions-item> </el-descriptions> <div class="actions"> <el-button type="primary" @click="testConnection"> 测试接口连通性 </el-button> <el-button type="danger" @click="handleLogout"> 退出登录 </el-button> </div> <el-alert v-if="testResult" :title="testResult" type="success" :closable="false" style="margin-top: 20px" /> </el-card> </div></template><script setup lang="ts">import { ref, computed } from 'vue'import { useRouter } from 'vue-router'import { ElMessage, ElMessageBox } from 'element-plus'import { testApi } from '@/api/auth'import type { UserInfo } from '@/types/auth'const router = useRouter()// 从 localStorage 获取用户信息const getUserInfoFromStorage = ():UserInfo => { const stored = localStorage.getItem('userInfo') if (stored) { try { return JSON.parse(stored) } catch { return { username: '', email: null, avatar: null } } } return { username: '', email: null, avatar: null }}const userInfo = ref<UserInfo>(getUserInfoFromStorage())const testResult = ref('')// Token 预览const tokenPreview = computed(() => { const token = localStorage.getItem('token') || '' return token.length > 20 ? token.substring(0, 20) + '...' : token})// 测试接口连通性const testConnection = async () => { try { const res = await testApi() testResult.value = res.message ElMessage.success('接口连通测试成功!') } catch (error) { ElMessage.error('接口连通测试失败') }}// 退出登录const handleLogout = () => { ElMessageBox.confirm('确定要退出登录吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 清除本地存储 localStorage.removeItem('token') localStorage.removeItem('userInfo') ElMessage.success('已退出登录') // 跳转到登录页 router.push('/login') }).catch(() => {})}</script><style scoped>.home-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f5f5f5; padding: 20px;}.welcome-card { width: 600px;}.card-header { text-align: center;}.card-header h1 { margin: 0; font-size: 24px; color: #333;}.actions { margin-top: 20px; display: flex; gap: 10px; justify-content: center;}</style>6.10 配置路由
创建 src/router/index.ts:
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'import { ElMessage } from 'element-plus'const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/login' }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') }, { path: '/register', name: 'Register', component: () => import('@/views/Register.vue') }, { path: '/home', name: 'Home', component: () => import('@/views/Home.vue'), meta: { requiresAuth: true } }]const router = createRouter({ history: createWebHistory(), routes})// 路由守卫router.beforeEach((to, _from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { ElMessage.warning('请先登录') next('/login') } else if ((to.path === '/login' || to.path === '/register') && token) { // 已登录用户访问登录/注册页,重定向到首页 next('/home') } else { next() }})export default router6.11 修改 App.vue
<template> <router-view /></template><script setup lang="ts"></script><style>* { margin: 0; padding: 0; box-sizing: border-box;}body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;}</style>6.12 添加 Vite 环境类型声明(可选)
如果 vite-env.d.ts 不存在,创建它:
/// <reference types="vite/client" />declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}6.13 配置 tsconfig.json(调整)
确保 tsconfig.json 包含以下配置:
{ "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ]}tsconfig.app.json 包含以下配置:
{ "extends": "@vue/tsconfig/tsconfig.dom.json", "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "types": ["vite/client"], "baseUrl": ".", "paths": { "@/*": ["./src/*"] }, "ignoreDeprecations": "6.0", "verbatimModuleSyntax":false, // 关闭严格模式 /* Linting */ "noUnusedLocals":true, "noUnusedParameters":true, "erasableSyntaxOnly":true, "noFallthroughCasesInSwitch":true }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]}tsconfig.node.json 包含以下配置:
{ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2023", "lib": ["ES2023"], "module": "ESNext", "types": ["node"], "skipLibCheck":true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions":true, "verbatimModuleSyntax":true, "moduleDetection": "force", "noEmit":true, /* Linting */ "noUnusedLocals":true, "noUnusedParameters":true, "erasableSyntaxOnly":true, "noFallthroughCasesInSwitch":true }, "include": ["vite.config.ts"]}6.14 配置 vite.config.ts
import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'import { resolve } from 'path'// https://vitejs.dev/config/export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, './src') } }, server: { port: 5173, host: true }})以上就是完整的前端 TypeScript 版本实现。
现在我们来启动一下
npm run dev可以看到界面啦。


以上就是完整注册登录的完整代码了,能看到这里的你,看来你的执行力也不简单,非常感谢你的观看。
如果这篇文章对你有帮助,欢迎点赞、在看支持一下。
夜雨聆风