前端登录/注册页面初步实现,还没和后端对接

lzt
哆哆咯哆哆咯 2 months ago
parent 9fb5e620b6
commit 82fd60a4d7

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

@ -0,0 +1,7 @@
package com.luojia_channel.common.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
}

@ -0,0 +1,22 @@
package com.luojia_channel.common.config;
import com.luojia_channel.common.interceptor.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login",
"/user/register"
);
}
}

@ -0,0 +1,8 @@
package com.luojia_channel.common.constants;
public class RedisConstant {
// redis存储的refreshToken前缀
public static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
// redis存储的黑名单前缀
public static final String BLACKLIST_PREFIX = "blacklist:";
}

@ -12,7 +12,6 @@ import lombok.NoArgsConstructor;
public class UserDTO {
private Long userId;
private String username;
private String email;
private String studentId;
private String token;
private String accessToken;
private String refreshToken;
}

@ -0,0 +1,51 @@
package com.luojia_channel.common.interceptor;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.JWTUtil;
import com.luojia_channel.common.utils.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final JWTUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 获取请求中的双Token
String accessToken = request.getHeader("Authorization");
String refreshToken = request.getHeader("X-Refresh-Token");
try {
// 验证Token并处理自动刷新
UserDTO user = jwtUtil.checkLogin(accessToken, refreshToken);
// 将新Token写入响应头
if (user.getAccessToken() != null) {
response.setHeader("New-Access-Token", JWTUtil.TOKEN_PREFIX + user.getAccessToken());
response.setHeader("New-Refresh-Token", user.getRefreshToken());
}
// 将用户信息存入请求上下文
UserContext.setUser(user);
return true;
} catch (UserException ex) {
// Token验证失败处理
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(
JSON.toJSONString(Result.fail(ex.getMessage()))
);
return false;
}
}
}

@ -2,32 +2,65 @@ package com.luojia_channel.common.utils;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.luojia_channel.common.constants.RedisConstant.BLACKLIST_PREFIX;
import static com.luojia_channel.common.constants.RedisConstant.REFRESH_TOKEN_PREFIX;
@Slf4j
@Component
@RequiredArgsConstructor
public final class JWTUtil {
private static final long EXPIRATION = 86400L;
private static final long ACCESS_EXPIRATION = 60 * 60 * 1000; //一小时
private static final long REFRESH_EXPIRATION = 60 * 60 * 24 * 15 * 1000; //15天
private static final Long NEED_REFRESH_TTL = 7L;
private static final String USER_ID_KEY = "userId";
private static final String USER_NAME_KEY = "username";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String ISS = "luojiachannel";
//后缀加了点字符保证密钥>=512位其他无更改
public static final String SECRET = "SecretKeyCreatedByForely0234523935489354315795647652568575435297576extrastringluojiachannel";
public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576";
private final RedisUtil redisUtil;
/**
* accessToken
* @param userInfo
* @return
*/
public String generateAccessToken(UserDTO userInfo) {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())
.setIssuer(ISS)
.setSubject(JSON.toJSONString(customerUserMap))
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION))
.compact();
return jwtToken;
}
/**
* Token
* refreshToken
* @param userInfo
* @return
*/
public static String generateAccessToken(UserDTO userInfo) {
public String generateRefreshToken(UserDTO userInfo) {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
@ -36,29 +69,111 @@ public final class JWTUtil {
.setIssuedAt(new Date())
.setIssuer(ISS)
.setSubject(JSON.toJSONString(customerUserMap))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION))
.compact();
return TOKEN_PREFIX + jwtToken;
return jwtToken;
}
/**
* Token
* token
* @param accessToken
* @return
*/
public static UserDTO parseJwtToken(String jwtToken) {
if (StringUtils.hasText(jwtToken)) {
String actualJwtToken = jwtToken.replace(TOKEN_PREFIX, "");
try {
Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(actualJwtToken).getBody();
Date expiration = claims.getExpiration();
if (expiration.after(new Date())) {
String subject = claims.getSubject();
return JSON.parseObject(subject, UserDTO.class);
}
} catch (ExpiredJwtException ignored) {
} catch (Exception ex) {
log.error("JWT Token解析失败请检查", ex);
public UserDTO parseJwtToken(String accessToken) {
if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(TOKEN_PREFIX)) {
throw new UserException("Token格式错误");
}
String rawAccessToken = accessToken.replace(TOKEN_PREFIX, "");
try {
Claims claims = parseToken(rawAccessToken);
Date expiration = claims.getExpiration();
if (expiration.after(new Date())) {
String subject = claims.getSubject();
return JSON.parseObject(subject, UserDTO.class);
}
} catch (ExpiredJwtException ignored) {
} catch (Exception ex) {
log.error("JWT Token解析失败请检查", ex);
}
return null;
}
/**
*
* @param accessToken
* @param refreshToken
* @return
*/
public UserDTO checkLogin(String accessToken, String refreshToken) {
if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(TOKEN_PREFIX)) {
throw new UserException("Token格式错误");
}
String rawAccessToken = accessToken.replace(TOKEN_PREFIX, "");
// 在黑名单中
if (redisUtil.hasKey(BLACKLIST_PREFIX + rawAccessToken)) {
throw new UserException("Token已失效");
}
try {
Claims claims = parseToken(rawAccessToken);
UserDTO user = extractUserFromClaims(claims);
if (needRefresh(claims)) {
return refreshTokens(user, refreshToken);
}
return user;
} catch (ExpiredJwtException ex) {
// accessToken过期尝试用refreshToken刷新
// 注意,这里不能直接用过期时间与现在时间做差判断,因为过期会抛异常
return handleExpiredToken(ex, refreshToken);
} catch (Exception ex) {
throw new UserException("Token无效");
}
}
private boolean needRefresh(Claims claims) {
long remaining = claims.getExpiration().getTime() - System.currentTimeMillis();
return remaining < 30 * 60 * 1000; // 剩余30分钟刷新
}
private UserDTO refreshTokens(UserDTO user, String refreshToken) {
String redisKey = REFRESH_TOKEN_PREFIX + user.getUserId();
String storedRefreshToken = redisUtil.get(redisKey, String.class);
if (!refreshToken.equals(storedRefreshToken)) {
throw new UserException("refreshToken无效");
}
// 生成新token并更新redis
String newAccessToken = generateAccessToken(user);
String newRefreshToken = generateRefreshToken(user);
// 惰性刷新refreshToken
Long ttl = redisUtil.getExpire(redisKey, TimeUnit.DAYS);
if(ttl < NEED_REFRESH_TTL)
redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS);
user.setAccessToken(newAccessToken);
return user;
}
private UserDTO handleExpiredToken(ExpiredJwtException ex, String refreshToken) {
// 从过期token中提取用户信息
UserDTO user = extractUserFromClaims(ex.getClaims());
// 刷新双 Token
return refreshTokens(user, refreshToken);
}
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
private UserDTO extractUserFromClaims(Claims claims) {
String subject = claims.getSubject();
return JSON.parseObject(subject, UserDTO.class);
}
public long getRemainingTime(String rawToken){
Claims claims = parseToken(rawToken);
return claims.getExpiration().getTime()-System.currentTimeMillis();
}
}

@ -46,6 +46,15 @@ public class RedisUtil {
redisTemplate.delete(key);
}
public boolean hasKey(String key) {
Boolean result = redisTemplate.hasKey(key);
return result != null;
}
public Long getExpire(String key, TimeUnit timeUnit){
return redisTemplate.getExpire(key, timeUnit);
}
/**
* set
*/

@ -9,7 +9,7 @@ import java.util.Optional;
public final class UserContext {
private static final ThreadLocal<UserDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();
void setUser(UserDTO user) {
public static void setUser(UserDTO user) {
USER_THREAD_LOCAL.set(user);
}
@ -25,9 +25,14 @@ public final class UserContext {
}
public static String getToken() {
public static String getAccessToken() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getToken).orElse(null);
return Optional.ofNullable(userInfoDTO).map(UserDTO::getAccessToken).orElse(null);
}
public static String getRefreshToken() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getRefreshToken).orElse(null);
}
public static void removeUser() {

@ -39,6 +39,12 @@
<scope>import</scope>
</dependency>
<!-- springboot3配套的MybatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Redisson -->
<dependency>
@ -57,6 +63,10 @@
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok (全局管理) -->
<dependency>
<groupId>org.projectlombok</groupId>

@ -20,17 +20,10 @@
<version>1.0.0</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- Redis -->
@ -51,11 +44,6 @@
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 分布式系统支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>

@ -2,10 +2,8 @@ package com.luojia_channel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.luojia_channel"})
public class LuojiaChannelApplication {
public static void main(String[] args) {

@ -8,5 +8,6 @@ public class UserConstant {
// 过期时间
public static final long EXPIRE_TIME_MINUTES = 30L;
// 过期时间
public static final long EXPIRE_TIME = 7L;
public static final long EXPIRE_TIME = 15L;
}

@ -1,4 +1,11 @@
package com.luojia_channel.modules.user.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user/info")
@RequiredArgsConstructor
public class UserInfoController {
}

@ -6,31 +6,31 @@ import com.luojia_channel.modules.user.dto.UserLoginDTO;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.service.UserLoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserLoginController {
private final UserLoginService userLoginService;
//登录
@PostMapping("/login")
public Result<UserDTO> login(@RequestBody UserLoginDTO userLoginDTO){
return Result.success(userLoginService.login(userLoginDTO));
}
//注册
@PostMapping("/register")
public Result<UserDTO> register(@RequestBody UserRegisterDTO userRegisterDTO){
return Result.success(userLoginService.register(userRegisterDTO));
}
//测试接口
@PostMapping("/logout")
public Result logout(@RequestParam String accessToken){
userLoginService.logout(accessToken);
return Result.success();
}
@PostMapping("/hello")
public Result<String> hello() {
public Result<String> hello(){
return Result.success("hello");
}

@ -5,6 +5,16 @@ import lombok.Data;
@Data
public class UserRegisterDTO {
private String username;
private String realName;
private String password;
private String phone;
private String email;
private String studentId;
private String captcha;
}

@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
import java.io.Serializable;
import java.time.LocalDateTime;
@ -76,4 +75,5 @@ public class User implements Serializable {
*
*/
private String college;
}

@ -8,13 +8,11 @@ import com.luojia_channel.modules.user.entity.User;
public interface UserLoginService extends IService<User> {
//登录
UserDTO login(UserLoginDTO userLoginDTO);
UserDTO checkLogin(String token);
UserDTO checkLogin(String accessToken, String refreshToken);
void logout(String token);
void logout(String accessToken);
//注册
UserDTO register(UserRegisterDTO userRegisterDTO);
}

@ -1,11 +1,13 @@
package com.luojia_channel.modules.user.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.user.dto.UserLoginDTO;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.entity.User;
@ -13,74 +15,73 @@ import com.luojia_channel.modules.user.mapper.UserMapper;
import com.luojia_channel.modules.user.service.UserLoginService;
import com.luojia_channel.common.utils.JWTUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.user.utils.ValidateParameterUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static com.luojia_channel.modules.user.constant.UserConstant.EXPIRE_TIME;
import static com.luojia_channel.common.constants.RedisConstant.BLACKLIST_PREFIX;
import static com.luojia_channel.common.constants.RedisConstant.REFRESH_TOKEN_PREFIX;
import static com.luojia_channel.common.utils.JWTUtil.TOKEN_PREFIX;
import static com.luojia_channel.modules.user.constant.UserConstant.*;
@Service
@RequiredArgsConstructor
public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implements UserLoginService {
private final UserMapper userMapper;
private PasswordEncoder passwordEncoder;
private final RedisUtil redisUtil;
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
private final JWTUtil jwtUtil;
private final ValidateParameterUtil validateParameterUtil;
/**
*
* @param userFlag
* @return
*/
private User getUserByFlag(String userFlag){
User user;
boolean mailFlag = false;
for(char c : userFlag.toCharArray()){
if(c == '@'){
mailFlag = true;
break;
}
private User getUserByFlag(String userFlag) {
if (StrUtil.isBlank(userFlag)) {
throw new UserException("用户标识不能为空");
}
// 邮箱登录
if(mailFlag){
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getEmail, userFlag);
user = userMapper.selectOne(wrapper);
if(user == null){
throw new UserException("用户邮箱不存在");
}
return user;
// 使用正则表达式判断类型,之前直接判断长度,虽然不合法的数据在数据库中仍然查不到
boolean isEmail = userFlag.contains("@");
boolean isPhone = !isEmail && userFlag.matches(ValidateParameterUtil.PHONE_REGEX);
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
if (isEmail) {
wrapper.eq(User::getEmail, userFlag);
} else if (isPhone) {
wrapper.eq(User::getPhone, userFlag);
} else {
// 默认学号登录
wrapper.eq(User::getStudentId, userFlag);
}
// 手机号登录
if(userFlag.length() == 11){
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getPhone, userFlag);
user = userMapper.selectOne(wrapper);
if(user == null){
throw new UserException("用户手机号不存在");
}
return user;
}
// 剩下的就是学号登录了
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getStudentId, userFlag);
user = userMapper.selectOne(wrapper);
if(user == null){
throw new UserException("用户学号不存在");
User user = userMapper.selectOne(wrapper);
if (user == null) {
throw new UserException("用户不存在");
}
return user;
}
/**
* token
* @param userDTO
*/
private void generateTokens(UserDTO userDTO){
String accessToken = jwtUtil.generateAccessToken(userDTO);
String refreshToken = jwtUtil.generateRefreshToken(userDTO);
userDTO.setAccessToken(accessToken);
userDTO.setRefreshToken(refreshToken);
//存储refreshToken到redis
String key = "refresh_token:" + userDTO.getUserId();
redisUtil.set(key, refreshToken, EXPIRE_TIME, TimeUnit.DAYS);
}
/**
*
* token
* @param userLoginDTO
* @return
*/
@ -88,122 +89,72 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
public UserDTO login(UserLoginDTO userLoginDTO) {
String userFlag = userLoginDTO.getUserFlag();
String password = userLoginDTO.getPassword();
//验证密码
// TODO 选择密码加密格式
User user = getUserByFlag(userFlag);
if(!passwordEncoder.matches(password, user.getPassword())) {
if(!user.getPassword().equals(password)) {
throw new UserException("密码错误");
}
UserDTO userDTO = UserDTO.builder()
.userId(user.getId())
.username(user.getUsername())
.studentId(user.getStudentId())
.build();
String jwtToken = JWTUtil.generateAccessToken(userDTO);
userDTO.setToken(jwtToken);
// 存储用户至redis
// 当jwt过期时若redis仍存在则刷新jwt
redisUtil.set(jwtToken, userDTO, EXPIRE_TIME, TimeUnit.DAYS);
generateTokens(userDTO);
return userDTO;
}
/**
* jwt
* @param token
*
* @param accessToken
* @param refreshToken
* @return
*/
@Override
public UserDTO checkLogin(String token) {
// 解析JWT若有效则刷新redis过期时间
UserDTO user = JWTUtil.parseJwtToken(token);
if (user != null) {
redisUtil.expire(token, EXPIRE_TIME, TimeUnit.DAYS);
return user;
}
// 若JWT过期检查Redis中是否存在该token对应的用户信息
UserDTO cachedUser = redisUtil.get(token, UserDTO.class);
if (cachedUser != null) {
String newToken = JWTUtil.generateAccessToken(cachedUser);
redisUtil.delete(token);
redisUtil.set(newToken, cachedUser, EXPIRE_TIME, TimeUnit.DAYS);
return cachedUser;
}
throw new UserException("登录失效");
public UserDTO checkLogin(String accessToken, String refreshToken) {
return jwtUtil.checkLogin(accessToken, refreshToken);
}
/**
*
* @param accessToken
*/
@Override
public void logout(String token){
if (StrUtil.isNotBlank(token)) {
redisUtil.delete(token);
public void logout(String accessToken) {
Long userId = UserContext.getUserId();
// 删除refreshToken
String refreshKey = REFRESH_TOKEN_PREFIX + userId;
redisUtil.delete(refreshKey);
// 将accessToken加入黑名单
if (StrUtil.isNotBlank(accessToken)) {
String rawToken = accessToken.replace(TOKEN_PREFIX, "");
long expire = jwtUtil.getRemainingTime(rawToken);
redisUtil.set(BLACKLIST_PREFIX + rawToken, "1", expire, TimeUnit.MILLISECONDS);
}
}
/**
*
* token
* TODO 使UUIDredis
* @param userRegisterDTO
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public UserDTO register(UserRegisterDTO userRegisterDTO) {
//检查注册信息是否完整
if (StrUtil.hasBlank(userRegisterDTO.getUsername(),
userRegisterDTO.getPassword(),
userRegisterDTO.getEmail())) {
throw new UserException("注册信息不能为空");
}
//检查用户名、邮箱是否已存在
LambdaQueryWrapper<User> usernameWrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getUsername, userRegisterDTO.getUsername());
if (userMapper.selectOne(usernameWrapper) != null) {
throw new UserException("用户名已存在");
}
LambdaQueryWrapper<User> emailWrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getEmail, userRegisterDTO.getEmail());
if (userMapper.selectOne(emailWrapper) != null) {
throw new UserException("邮箱已存在");
}
//TODO 邮箱格式校验,前端完成
//if (!userRegisterDTO.getEmail().matches("^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$")) {
//throw new UserException("邮箱格式不正确");
//}
//检查密码长度,不知道是不是前端做的
if (userRegisterDTO.getPassword().length() < 6) {
throw new UserException("密码长度不能小于6位");
}
//TODO 检查密码是否一致,前端完成
//if (!userRegisterDTO.getPassword().equals(userRegisterDTO.getConfirmPassword())) {
//throw new UserException("两次密码不一致");
//}
//加密密码
String encodedPassword = passwordEncoder.encode(userRegisterDTO.getPassword());
//保存用户信息至mysql
User newuser = new User();
newuser.setUsername(userRegisterDTO.getUsername());
newuser.setPassword(encodedPassword);
newuser.setEmail(userRegisterDTO.getEmail());
if(!save(newuser)){
throw new UserException("注册失败");
}
//生成JWT
// 校验注册参数
validateParameterUtil.validateUser(userRegisterDTO);
User user = BeanUtil.copyProperties(userRegisterDTO, User.class);
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
save(user);
UserDTO userDTO = UserDTO.builder()
.userId(newuser.getId())
.username(newuser.getUsername())
.email(newuser.getEmail())
.userId(user.getId())
.username(user.getUsername())
.build();
//生成token
String jwtToken = JWTUtil.generateAccessToken(userDTO);
userDTO.setToken(jwtToken);
//存储用户信息至redis
redisUtil.set(jwtToken, userDTO, EXPIRE_TIME, TimeUnit.DAYS);
generateTokens(userDTO);
return userDTO;
}
}

@ -0,0 +1,100 @@
package com.luojia_channel.modules.user.utils;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ValidateParameterUtil {
// 参数校验正则表达式,学号校验不一定正确
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
public static final String STUDENTID_REGEX = "^(?:\\d{12,13}|[A-Z]{2}\\d{8,10})$";
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
private final UserMapper userMapper;
/**
*
*
* @param userRegisterDTO
*/
public void validateUser(UserRegisterDTO userRegisterDTO) {
String username = userRegisterDTO.getUsername();
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
if (StrUtil.isBlank(username)){
throw new UserException("用户名不能为空");
}
if (StrUtil.isBlank(password)){
throw new UserException("密码不能为空");
}
// 前端做了校验后端还要校验,保证选择一种注册方式
int cnt = 0;
if(StrUtil.isNotBlank(phone)) cnt++;
if(StrUtil.isNotBlank(email)) cnt++;
if(StrUtil.isNotBlank(studentId)) cnt++;
if (cnt == 0) {
throw new UserException("必须填写手机号、邮箱或学号其中一种注册方式");
}
if (cnt > 1) {
throw new UserException("只能选择一种注册方式(手机/邮箱/学号)");
}
// 格式校验
validateFormats(userRegisterDTO);
}
// 格式校验,未来更改用户信息时可能使用
// TODO 实际上,用户更改信息校验时数据库查询的不是是否存在,而是是否等于要修改的用户
public void validateFormats(UserRegisterDTO userRegisterDTO) {
String username = userRegisterDTO.getUsername();
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
String captcha = userRegisterDTO.getCaptcha();
// 仅对非空字段做格式校验
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username))) {
throw new UserException("用户名已存在");
}
if(!password.matches(PASSWORD_REGEX)){
throw new UserException("密码格式错误");
}
if(StrUtil.isNotBlank(phone)){
if(!phone.matches(PHONE_REGEX))
throw new UserException("手机号格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getPhone, phone))) {
throw new UserException("手机已存在");
}
}
if(StrUtil.isNotBlank(email)){
if(!email.matches(EMAIL_REGEX))
throw new UserException("邮箱格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getEmail, email))) {
throw new UserException("邮箱已存在");
}
}
if(StrUtil.isNotBlank(studentId)){
if(!studentId.matches(STUDENTID_REGEX))
throw new UserException("学号格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getStudentId, studentId))) {
throw new UserException("学号已存在");
}
}
}
}

@ -1,29 +1,28 @@
# 本地开发环境
lj:
db:
host: 192.168.59.129
password: Forely123!
redis:
host: 192.168.59.129
port: 6379
password: Forely123!
rabbitmq:
host: 192.168.59.129
port: 5672
username: admin
password: Forely123!
# 本地开发环境,cf的配置
#lj:
# db:
# host: localhost
# password: 123456
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: localhost
# host: 192.168.59.129
# port: 6379
# password: 123456
# password: Forely123!
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
lj:
db:
host: localhost
passwprd: 123456
redis:
host: localhost
port: 6379
password: 123456
rabbitmq:
host: localhost
port: 15672
username: root
password: 123456

@ -1,29 +1,28 @@
# 本地开发环境
lj:
db:
host: 192.168.59.129
password: Forely123!
redis:
host: 192.168.59.129
port: 6379
password: Forely123!
rabbitmq:
host: 192.168.59.129
port: 5672
username: admin
password: Forely123!
# 本地开发环境,cf的配置
#lj:
# db:
# host: localhost
# password: 123456
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: localhost
# host: 192.168.59.129
# port: 6379
# password: 123456
# password: Forely123!
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
lj:
db:
host: localhost
passwprd: 123456
redis:
host: localhost
port: 6379
password: 123456
rabbitmq:
host: localhost
port: 15672
username: root
password: 123456

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,24 @@
# vue-frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
{
"name": "vue-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.8.4",
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Responsive Login And Registration</title>
<link href='<%= BASE_URL %>css/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="<%= BASE_URL %>css/style.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="<%= BASE_URL %>js/src/main.js"></script>
</body>
</html>

@ -0,0 +1,31 @@
<!-- 模板部分生成HTML -->
<template>
<div id="app">
<AppHeader />
<router-view/>
</div>
</template>
<!-- 控制模板数据与行为 -->
<script>
import AppHeader from './components/Header.vue';
export default {
name: 'App',
components: {
AppHeader
}
}
</script>
<!-- 样式部分,定义CSS样式 -->
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,145 @@
<template>
<header class="header">
<!-- 网站 Logo 和标题部分 -->
<div class="logo-section">
<router-link to="/" class="logo-link">
<!-- Logo 图片 -->
<img src="@/assets/logo.png" alt="Logo" class="logo-img"/>
<!-- 标题区域包含中文和英文标题 -->
<div class="title-section">
<div class="divider-section">
<!-- 分隔线 -->
<div class="divider"></div>
<!-- 网站标题组 -->
<div class="titles">
<h1>珞珈岛</h1>
<h2>LuoJia-Island</h2>
</div>
</div>
</div>
</router-link>
</div>
<!-- 导航部分登录/注册按钮,后续可添加更多功能 -->
<div class="nav-section">
<button @click="showModal" class="login-btn">登录/注册</button>
</div>
<!-- 登录/注册模态框组件仅在 isModalVisible true 时显示 -->
<LoginRegisterModal v-if="isModalVisible" @close="hideModal" />
</header>
</template>
<script>
//
import LoginRegisterModal from './LoginRegisterModal.vue'
export default {
name: 'AppHeader',
components: {
LoginRegisterModal //
},
data() {
return {
isModalVisible: false // /
}
},
methods: {
//
showModal() {
this.isModalVisible = true
},
//
hideModal() {
this.isModalVisible = false
}
}
}
</script>
<style scoped>
/* 头部容器样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px;
border-bottom: 2px solid #e0e0e0;
background: white;
}
/* Logo 区域样式 */
.logo-section {
display: flex;
align-items: center;
}
/* Logo 链接样式 */
.logo-link {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
}
/* Logo 图片样式 */
.logo-img {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
}
/* 分隔线容器样式 */
.divider-section {
display: flex;
align-items: center;
}
/* logo-标题分隔线样式 */
.divider {
width: 2px;
height: 40px;
background-color: #333;
margin-right: 15px;
}
/* 标题组样式 */
.titles {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* 中文标题样式 */
.titles h1 {
font-size: 22px;
margin: 0;
color: #333;
}
/* 英文标题样式 */
.titles h2 {
font-size: 12px;
margin: 0;
color: #666;
}
/* 登录按钮样式 */
.login-btn {
height: 30px;
padding: 0px 10px;
border: none;
background: none;
font-size: 15px;
color: #333;
cursor: pointer;
transition: color 0.3s;
}
/* 登录按钮悬停效果 */
.login-btn:hover {
color: #6FBD87;
background: #f0f0f0;
}
</style>

@ -0,0 +1,343 @@
<template>
<!-- 登录表单容器 -->
<div class="form-container">
<h2>登录</h2>
<!-- 登录方式选择器 -->
<div class="login-type">
<span
v-for="type in loginTypes"
:key="type.value"
:class="{ active: currentType === type.value }"
@click="currentType = type.value">
{{ type.label }}
</span>
</div>
<!-- 登录表单 -->
<form @submit.prevent="login" class="login-form">
<div class="input-group">
<!-- 用户标识输入框用户名/学号/邮箱/手机 -->
<input
:type="inputType"
v-model="loginForm.userFlag"
:placeholder="placeholder"
required />
<!-- 验证码输入区域仅邮箱和手机登录时显示 -->
<template v-if="showVerifyCode">
<div class="verify-code">
<input
type="text"
v-model="loginForm.verifyCode"
placeholder="验证码"
required />
<!-- 获取验证码按钮 -->
<button type="button" @click="sendCode" :disabled="cooldown > 0">
{{ cooldown > 0 ? `${cooldown}s` : '获取验证码' }}
</button>
</div>
</template>
<!-- 密码输入框非验证码登录时显示 -->
<template v-else>
<input
type="password"
v-model="loginForm.password"
placeholder="密码"
required />
</template>
</div>
<!-- 登录按钮 -->
<button type="submit">登录</button>
</form>
<!-- 自动登录选项 -->
<div class="remember-login">
<input
type="checkbox"
id="remember"
v-model="loginForm.remember" />
<label for="remember">自动登录</label>
<!-- 切换到注册表单的链接 -->
<p class="switch-form">
没有账号
<button @click="$emit('toggleForm')" class="link-button">注册</button>
</p>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import axios from 'axios';
export default {
name: 'UserLogin',
setup() {
//
const currentType = ref('username'); //
const cooldown = ref(0); //
//
const loginTypes = [
{ label: '用户名登录', value: 'username' }, //
{ label: '学号登录', value: 'student' }, //
{ label: '邮箱登录', value: 'email' }, //
{ label: '手机登录', value: 'phone' } //
];
//
const loginForm = ref({
userFlag: '', // ///
password: '', //
verifyCode: '', // /使
remember: false //
});
//
const inputType = computed(() => {
switch(currentType.value) {
case 'email': return 'email'; //
case 'phone': return 'tel'; //
default: return 'text'; //
}
});
//
const placeholder = computed(() => {
switch(currentType.value) {
case 'username': return '用户名';
case 'student': return '学号';
case 'email': return '邮箱';
case 'phone': return '手机号';
default: return '';
}
});
//
const showVerifyCode = computed(() => {
return ['email', 'phone'].includes(currentType.value);
});
//
async function sendCode() {
if (cooldown.value > 0) return; //
try {
//
const response = await axios.post('/user/sendCode', {
type: currentType.value,
target: loginForm.value.userFlag
});
if (response.data.success) {
// 60
cooldown.value = 60;
const timer = setInterval(() => {
cooldown.value--;
if (cooldown.value <= 0) {
clearInterval(timer);
}
}, 1000);
}
} catch (error) {
console.error('发送验证码失败:', error);
}
}
//
async function login() {
try {
//
const response = await axios.post('/user/login', {
...loginForm.value,
loginType: currentType.value
});
if (response.data.success) {
// TODO:
}
} catch (error) {
console.error('登录失败:', error);
}
}
// 使
return {
currentType, //
loginTypes, //
loginForm, //
inputType, //
placeholder, //
showVerifyCode, //
cooldown, //
sendCode, //
login //
};
}
}
</script>
<style scoped>
/* 登录表单容器样式 - 为整个登录表单提供白色背景和阴影效果 */
.form-container {
background: rgba(255, 255, 255, 0.90);
padding: 32px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
margin: auto;
}
/* 登录方式切换栏样式 - 包含用户名/学号/邮箱/手机登录选项 */
.login-type {
display: flex;
justify-content: space-around;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
/* 登录方式选项样式 - 每个登录方式的基本样式 */
.login-type span {
font-size: 14px;
padding: 6px 12px;
cursor: pointer;
position: relative;
color: #666;
}
/* 当前选中的登录方式样式 - 突出显示当前选中的登录方式 */
.login-type span.active {
color: #6FBD87;
background: #f5f5f5;
}
/* 当前选中登录方式的下划线样式 - 为选中项添加绿色下划线 */
.login-type span.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 1px;
background: #6FBD87;
}
/* 输入框组样式 - 包含所有输入框的容器 */
.input-group {
margin-bottom: 20px;
}
/* 通用输入框样式 - 适用于用户名/学号/邮箱/手机号/密码输入框 */
input {
width: 93%;
padding: 11px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
/* 输入框获得焦点时的样式 - 突出显示当前正在输入的框 */
input:focus {
border-color: #6FBD87;
outline: none;
}
/* 验证码输入区域样式 - 包含验证码输入框和获取验证码按钮 */
.verify-code {
display: flex;
gap: 10px;
}
/* 验证码输入框样式 */
.verify-code input {
flex: 1;
}
/* 获取验证码按钮样式 */
.verify-code button {
width: 88px;
height: 40px;
margin: 8px 0;
font-size: 12px;
}
/* 登录按钮样式 */
button {
width: 93%;
padding: 10px;
background: #6FBD87;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
/* 登录按钮悬停效果 */
button:hover {
background: #5aa76f;
}
/* 禁用状态的按钮样式(用于验证码倒计时) */
button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 自动登录选项样式 - 复选框和文字的容器 */
.remember-login {
display: flex;
align-items: center;
margin: 12px 0;
}
/* 自动登录复选框样式 */
.remember-login input[type="checkbox"] {
width: auto;
margin-left: 22px;
margin-right: 8px;
margin-top: 10px;
cursor: pointer;
}
/* 自动登录文字样式 */
.remember-login label {
color: #666;
cursor: pointer;
font-size: 15px;
}
/* 切换到注册的提示文字样式 */
.switch-form {
font-size: 15px;
margin-left: 138px;
margin-top: 16px;
text-align: center;
color: #666;
}
/* 切换到注册的链接按钮样式 */
.link-button {
background: none;
border: none;
color: #6FBD87;
text-decoration: underline;
padding: 0 2px;
width: auto;
font-size: 15px;
}
/* 切换到注册的链接按钮悬停效果 */
.link-button:hover {
color: #5aa76f;
background: none;
}
</style>

@ -0,0 +1,103 @@
<template>
<!-- 模态框遮罩层点击空白处关闭模态框 -->
<div class="modal-overlay" @click.self="close">
<!-- 模态框容器 -->
<div class="modal-container">
<!-- 关闭按钮 -->
<button class="close-button" @click="close">&times;</button>
<!-- 模态框内容区域 -->
<div class="modal-content">
<!-- 根据 isLogin 状态动态切换登录/注册组件 -->
<UserLogin v-if="isLogin" @toggleForm="toggleForm" />
<UserRegister v-else @toggleForm="toggleForm" />
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
//
import UserLogin from './Login.vue';
import UserRegister from './Register.vue';
export default {
name: 'LoginRegisterModal',
components: {
UserLogin, //
UserRegister //
},
setup(props, { emit }) {
//
const isLogin = ref(true);
// /
const toggleForm = () => {
isLogin.value = !isLogin.value;
};
return {
isLogin,
toggleForm,
close: () => emit('close') //
};
}
}
</script>
<style scoped>
/* 模态框遮罩层样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
/* 模态框容器样式 */
.modal-container {
background: white;
border-radius: 12px;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.15);
position: relative;
width: 90%;
max-width: 480px;
min-height: 500px;
}
/* 模态框内容区域样式 */
.modal-content {
padding: 25px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 关闭按钮样式 */
.close-button {
position: absolute;
top: 4px;
right: 17px;
background: none;
border: none;
font-size: 50px;
color: #666;
cursor: pointer;
padding: 4px;
width: 40px;
height: 40px;
z-index: 1;
}
/* 关闭按钮悬停效果 */
.close-button:hover {
color: #111;
}
</style>

@ -0,0 +1,370 @@
<template>
<!-- 注册表单容器 -->
<div class="form-container">
<h2>注册</h2>
<!-- 注册方式选择器(用户名/邮箱/手机) -->
<div class="register-type">
<span
v-for="type in registerTypes"
:key="type.value"
:class="{ active: currentType === type.value }"
@click="currentType = type.value">
{{ type.label }}
</span>
</div>
<!-- 注册表单,阻止默认提交行为 -->
<form @submit.prevent="register" class="register-form">
<!-- 输入框组 -->
<div class="input-group">
<!-- 动态输入框(根据注册方式显示相应的输入框) -->
<input
:type="inputType"
v-model="registerForm[currentType]"
:placeholder="placeholder"
required />
<!-- 验证码部分(邮箱/手机注册时显示) -->
<template v-if="showVerifyCode">
<div class="verify-code">
<!-- 验证码输入框 -->
<input
type="text"
v-model="registerForm.verifyCode"
placeholder="验证码"
required />
<!-- 发送验证码按钮(带倒计时功能) -->
<button
type="button"
@click="sendCode"
:disabled="cooldown > 0">
{{ cooldown > 0 ? `${cooldown}s` : '获取验证码' }}
</button>
</div>
</template>
<!-- 密码输入区域 -->
<div class="password-group">
<!-- 密码输入框 -->
<input
type="password"
v-model="registerForm.password"
placeholder="密码"
required />
<!-- 确认密码输入框 -->
<input
type="password"
v-model="registerForm.confirmPassword"
placeholder="确认密码"
required />
</div>
<!-- 图形验证码区域 -->
<div class="captcha-group">
<!-- 图形验证码输入框 -->
<input
type="text"
v-model="registerForm.captcha"
placeholder="图形验证码"
required />
<!-- 验证码图片(点击刷新) -->
<img
:src="captchaUrl"
@click="refreshCaptcha"
alt="验证码"
class="captcha-img" />
</div>
</div>
<!-- 注册按钮 -->
<button type="submit">注册</button>
</form>
<!-- 切换到登录表单的链接 -->
<p class="switch-form">
已有账号
<button @click="$emit('toggleForm')" class="link-button">登录</button>
</p>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import axios from 'axios';
export default {
name: 'UserRegister',
setup() {
//
const currentType = ref('username'); //
const cooldown = ref(0); //
//
const registerTypes = [
{ label: '用户名注册', value: 'username' },
{ label: '邮箱注册', value: 'email' },
{ label: '手机注册', value: 'phone' }
];
//
const registerForm = ref({
username: '', //
email: '', //
phone: '', //
password: '', //
confirmPassword: '', //
verifyCode: '', // (/)
captcha: '' //
});
// URL
const captchaUrl = ref('/user/captcha');
//
const inputType = computed(() => {
switch(currentType.value) {
case 'email': return 'email';
case 'phone': return 'tel';
default: return 'text';
}
});
//
const placeholder = computed(() => {
switch(currentType.value) {
case 'email': return '请输入邮箱';
case 'phone': return '请输入手机号';
default: return '请输入用户名';
}
});
//
const showVerifyCode = computed(() => {
return ['email', 'phone'].includes(currentType.value);
});
// /使
async function sendCode() {
if (cooldown.value > 0) return;
try {
//
const response = await axios.post(`/user/send-code`, {
type: currentType.value,
target: registerForm.value[currentType.value]
});
if (response.data.success) {
// 60
cooldown.value = 60;
const timer = setInterval(() => {
cooldown.value--;
if (cooldown.value <= 0) {
clearInterval(timer);
}
}, 1000);
}
} catch (error) {
console.error('发送验证码失败:', error);
}
}
//
function refreshCaptcha() {
//
captchaUrl.value = `/user/captcha?t=${new Date().getTime()}`;
}
//
async function register() {
try {
const response = await axios.post('/user/register', {
...registerForm.value,
registerType: currentType.value
});
if (response.data.success) {
//
this.$emit('toggleForm');
}
} catch (error) {
console.error('注册失败:', error);
}
}
// 使
return {
currentType, //
registerTypes, //
registerForm, //
captchaUrl, // URL
inputType, //
placeholder, //
showVerifyCode, //
cooldown, //
sendCode, //
refreshCaptcha, //
register //
};
}
}
</script>
<style scoped>
/* 注册表单容器样式 - 为整个注册表单提供白色背景和阴影效果 */
.form-container {
background: rgba(255, 255, 255, 0.90);
padding: 32px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
margin: auto;
}
/* 注册方式切换栏样式 - 包含用户名/邮箱/手机注册选项 */
.register-type {
display: flex;
justify-content: space-around;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
/* 注册方式选项样式 - 每个注册方式的基本样式 */
.register-type span {
font-size: 14px;
padding: 6px 12px;
cursor: pointer;
position: relative;
color: #666;
}
/* 当前选中的注册方式样式 - 突出显示当前选中的注册方式 */
.register-type span.active {
color: #6FBD87;
background: #f5f5f5;
}
/* 当前选中注册方式的下划线样式 - 为选中项添加绿色下划线 */
.register-type span.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background: #6FBD87;
}
/* 输入框组样式 - 包含所有输入框的容器 */
.input-group {
margin-bottom: 20px;
}
/* 通用输入框样式 - 适用于用户名/邮箱/手机号/密码输入框 */
input {
width: 93%;
padding: 11px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
/* 输入框获得焦点时的样式 - 突出显示当前正在输入的框 */
input:focus {
border-color: #6FBD87;
outline: none;
}
/* 验证码输入区域样式 - 包含验证码输入框和获取验证码按钮 */
.verify-code {
display: flex;
gap: 10px;
}
/* 验证码输入框样式 */
.verify-code input {
flex: 1;
}
/* 获取验证码按钮样式 */
.verify-code button {
width: 88px;
height: 40px;
margin: 8px 0;
font-size: 12px;
}
/* 密码输入区域样式 - 包含密码和确认密码输入框 */
.password-group {
display: flex;
flex-direction: column;
}
/* 图形验证码区域样式 - 包含验证码输入框和验证码图片 */
.captcha-group {
display: flex;
gap: 10px;
align-items: center;
}
/* 图形验证码图片样式 */
.captcha-img {
width: 180px;
height: 30px;
border-radius: 4px;
cursor: pointer;
}
/* 注册按钮样式 */
button {
width: 93%;
padding: 10px;
background: #6FBD87;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
/* 注册按钮悬停效果 */
button:hover {
background: #5aa76f;
}
/* 禁用状态的按钮样式(用于验证码倒计时) */
button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 切换到登录的提示文字样式 */
.switch-form {
margin-top: 20px;
text-align: center;
color: #666;
}
/* 切换到登录的链接按钮样式 */
.link-button {
background: none;
border: none;
color: #6FBD87;
text-decoration: underline;
padding: 0 2px;
width: auto;
font-size: inherit;
}
/* 切换到登录的链接按钮悬停效果 */
.link-button:hover {
color: #5aa76f;
background: none;
}
</style>

@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

@ -0,0 +1,29 @@
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import Login from '../components/Login.vue';
import Register from '../components/Register.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/user/login',
name: 'Login',
component: Login
},
{
path: '/user/register',
name: 'Register',
component: Register
}
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;

@ -0,0 +1,5 @@
<template>
<div>
<h1>欢迎来到珞珈岛</h1>
</div>
</template>

@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
Loading…
Cancel
Save