feat(auth): 实现基于阿里云短信验证码的登录注册功能
- 新增阿里云短信发送客户端配置及属性绑定类 - 集成阿里云短信服务实现验证码发送功能 - 基于 Sa-Token 完成登录状态管理和 token 生成 - 实现手机号验证码登录、密码登录及验证码注册支持 - 添加密码加密 Bean,使用 BCrypt 保障密码安全 - 新增 Redis 缓存验证码,实现验证码有效期和校验 - Vue 前端新增登录弹窗组件,支持三种登录模式切换 - 统一 Axios 请求添加 Token 请求头及响应错误提示 - 更新配置文件,加入 Sa-Token 相关配置项 - 调整后端数据库实体生成配置,新增用户表映射 - 添加前端依赖包 @vueuse/integrations 和 universal-cookie - 新增前端 Cookie 操作逻辑,用于 Token 的存取管理 - 优化 Header 组件,增加 Login 按钮触发登录弹窗
This commit is contained in:
@@ -105,6 +105,55 @@
|
||||
<artifactId>tess4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云短信发送 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-template</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 密码加密 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.yinlihupo.enlish.service.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ConfigurationProperties(prefix = "aliyun")
|
||||
@Component
|
||||
@Data
|
||||
public class AliyunAccessKeyProperties {
|
||||
private String accessKeyId;
|
||||
private String accessKeySecret;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.yinlihupo.enlish.service.config;
|
||||
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class AliyunSmsClientConfig {
|
||||
|
||||
@Resource
|
||||
private AliyunAccessKeyProperties aliyunAccessKeyProperties;
|
||||
|
||||
@Bean
|
||||
public Client smsClient() {
|
||||
try {
|
||||
Config config = new Config()
|
||||
// 必填
|
||||
.setAccessKeyId(aliyunAccessKeyProperties.getAccessKeyId())
|
||||
// 必填
|
||||
.setAccessKeySecret(aliyunAccessKeyProperties.getAccessKeySecret());
|
||||
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
|
||||
config.endpoint = "dysmsapi.aliyuncs.com";
|
||||
|
||||
return new Client(config);
|
||||
} catch (Exception e) {
|
||||
log.error("初始化阿里云短信发送客户端错误: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yinlihupo.enlish.service.config;
|
||||
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
@Component
|
||||
public class PasswordEncoderConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
// BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yinlihupo.enlish.service.constant;
|
||||
|
||||
public class UserRedisConstants {
|
||||
|
||||
public static final String USER_LOGIN_CODE = "user:login:code:";
|
||||
|
||||
public static String buildUserLoginCode(String phone) {
|
||||
return USER_LOGIN_CODE + phone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.yinlihupo.enlish.service.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.yinlihupo.enlish.service.model.vo.login.LoginReqVO;
|
||||
import com.yinlihupo.enlish.service.model.vo.login.VerificationCodeReqVO;
|
||||
import com.yinlihupo.enlish.service.service.LoginService;
|
||||
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||
import com.yinlihupo.framework.common.response.Response;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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;
|
||||
|
||||
@RequestMapping("/login/")
|
||||
@RestController
|
||||
@Slf4j
|
||||
public class LoginController {
|
||||
|
||||
@Resource
|
||||
private LoginService loginService;
|
||||
|
||||
@PostMapping("login")
|
||||
@ApiOperationLog(description = "登录")
|
||||
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
|
||||
try {
|
||||
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode());
|
||||
return Response.success(StpUtil.getTokenInfo().getTokenValue());
|
||||
} catch (Exception e) {
|
||||
log.error("注册或登录失败 {}", e.getMessage());
|
||||
return Response.fail("注册或登录失败 " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("sendVerificationCode")
|
||||
@ApiOperationLog(description = "发送验证码")
|
||||
public Response<Void> sendVerificationCode(@RequestBody VerificationCodeReqVO verificationCodeReqVO) {
|
||||
try {
|
||||
loginService.sendVerificationCode(verificationCodeReqVO.getPhone());
|
||||
return Response.success();
|
||||
} catch (Exception e) {
|
||||
log.error("发送验证码失败 {}", e.getMessage());
|
||||
return Response.fail("发送验证码失败 " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.yinlihupo.enlish.service.domain.dataobject;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class UserDO {
|
||||
private Long id;
|
||||
|
||||
private String password;
|
||||
|
||||
private String name;
|
||||
|
||||
private String openid;
|
||||
|
||||
private String phone;
|
||||
|
||||
private String email;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Integer isDeleted;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yinlihupo.enlish.service.domain.mapper;
|
||||
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||
|
||||
public interface UserDOMapper {
|
||||
|
||||
UserDO selectByPhone(String phone);
|
||||
|
||||
void insert(UserDO userDO);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.yinlihupo.enlish.service.model.vo.login;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class LoginReqVO {
|
||||
|
||||
private String phone;
|
||||
private String name;
|
||||
private String password;
|
||||
private String code;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yinlihupo.enlish.service.model.vo.login;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class VerificationCodeReqVO {
|
||||
|
||||
private String phone;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.yinlihupo.enlish.service.service;
|
||||
|
||||
public interface LoginService {
|
||||
|
||||
void login(String phone, String name, String reqPassword, String reqCode);
|
||||
|
||||
void sendVerificationCode(String phone);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.yinlihupo.enlish.service.service.login;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||
import com.aliyun.teautil.models.RuntimeOptions;
|
||||
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
||||
import com.yinlihupo.enlish.service.service.LoginService;
|
||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LoginServiceImpl implements LoginService {
|
||||
|
||||
@Resource
|
||||
private UserDOMapper userDOMapper;
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
@Resource
|
||||
private Client client;
|
||||
|
||||
@Override
|
||||
public void login(String phone, String name, String reqPassword, String reqCode) {
|
||||
UserDO userDO = userDOMapper.selectByPhone(phone);
|
||||
log.info("userDO:{}", userDO);
|
||||
|
||||
String code = stringRedisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone));
|
||||
if (userDO == null) {
|
||||
if (code == null || !code.equals(reqCode)) {
|
||||
throw new RuntimeException("验证码错误");
|
||||
}
|
||||
userDO = UserDO.builder()
|
||||
.phone(phone)
|
||||
.name(name)
|
||||
.password(passwordEncoder.encode(reqPassword))
|
||||
.build();
|
||||
userDOMapper.insert(userDO);
|
||||
StpUtil.login(userDO.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (code != null && code.equals(reqCode)) {
|
||||
StpUtil.login(userDO.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (reqPassword != null && passwordEncoder.matches(reqPassword, userDO.getPassword())) {
|
||||
StpUtil.login(userDO.getId());
|
||||
throw new RuntimeException("密码错误");
|
||||
|
||||
}
|
||||
|
||||
throw new RuntimeException("登录错误");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendVerificationCode(String phone) {
|
||||
String code = RandomUtil.randomNumbers(6);
|
||||
stringRedisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
||||
String signName = "短信测试";
|
||||
String templateCode = "SMS_154950909";
|
||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||
try {
|
||||
sendMessage(phone, signName, templateCode, templateParam);
|
||||
} catch (Exception e) {
|
||||
log.error("==> 短信发送失败, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initUserRole(UserDO userDO) {
|
||||
// todo
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*/
|
||||
public void sendMessage(String signName, String templateCode, String phone, String templateParam) throws Exception {
|
||||
SendSmsRequest sendSmsRequest = new SendSmsRequest()
|
||||
.setSignName(signName)
|
||||
.setTemplateCode(templateCode)
|
||||
.setPhoneNumbers(phone)
|
||||
.setTemplateParam(templateParam);
|
||||
RuntimeOptions runtime = new RuntimeOptions();
|
||||
|
||||
|
||||
log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);
|
||||
|
||||
// 发送短信
|
||||
SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||
|
||||
log.info("==> 短信发送成功, response: {}", JsonUtils.toJsonString(response));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -35,3 +35,7 @@ tmp:
|
||||
ai:
|
||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||
url: https://chat.cosonggle.com/v1/chat-messages
|
||||
|
||||
aliyun:
|
||||
accessKeyId:
|
||||
accessKeySecret:
|
||||
@@ -8,3 +8,22 @@ spring:
|
||||
mybatis:
|
||||
# MyBatis xml 配置文件路径
|
||||
mapper-locations: classpath:/mapper/**/*.xml
|
||||
|
||||
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
|
||||
sa-token:
|
||||
# token 名称(同时也是 cookie 名称)
|
||||
token-name: Authorization
|
||||
# token 前缀
|
||||
token-prefix: Bearer
|
||||
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
token-style: random-128
|
||||
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
timeout: 2592000
|
||||
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
active-timeout: -1
|
||||
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||
is-share: true
|
||||
# 是否输出操作日志
|
||||
is-log: true
|
||||
@@ -45,7 +45,7 @@
|
||||
targetProject="src/main/java"/>
|
||||
|
||||
<!-- 需要生成的表-实体类 -->
|
||||
<table tableName="student_lesson_plans" domainObjectName="StudentLessonPlansDO"
|
||||
<table tableName="user" domainObjectName="UserDO"
|
||||
enableCountByExample="false"
|
||||
enableUpdateByExample="false"
|
||||
enableDeleteByExample="false"
|
||||
|
||||
27
enlish-service/src/main/resources/mapper/UserDOMapper.xml
Normal file
27
enlish-service/src/main/resources/mapper/UserDOMapper.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.yinlihupo.enlish.service.domain.mapper.UserDOMapper">
|
||||
<resultMap id="BaseResultMap" type="com.yinlihupo.enlish.service.domain.dataobject.UserDO">
|
||||
<id column="id" jdbcType="BIGINT" property="id" />
|
||||
<result column="password" jdbcType="VARCHAR" property="password" />
|
||||
<result column="name" jdbcType="VARCHAR" property="name" />
|
||||
<result column="openid" jdbcType="VARCHAR" property="openid" />
|
||||
<result column="phone" jdbcType="VARCHAR" property="phone" />
|
||||
<result column="email" jdbcType="VARCHAR" property="email" />
|
||||
<result column="status" jdbcType="INTEGER" property="status" />
|
||||
<result column="is_deleted" jdbcType="INTEGER" property="isDeleted" />
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into user (password, phone, name)
|
||||
values (#{password}, #{phone}, #{name})
|
||||
</insert>
|
||||
|
||||
<select id="selectByPhone" resultMap="BaseResultMap">
|
||||
select *
|
||||
from user
|
||||
where phone = #{phone}
|
||||
and is_deleted = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
142
enlish-vue/package-lock.json
generated
142
enlish-vue/package-lock.json
generated
@@ -8,12 +8,14 @@
|
||||
"name": "enlish-vue",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@vueuse/integrations": "^14.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.12.0",
|
||||
"flowbite": "^1.8.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"universal-cookie": "^8.0.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -1200,6 +1202,116 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.1.0.tgz",
|
||||
"integrity": "sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "14.1.0",
|
||||
"@vueuse/shared": "14.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"async-validator": "^4",
|
||||
"axios": "^1",
|
||||
"change-case": "^5",
|
||||
"drauu": "^0.4",
|
||||
"focus-trap": "^7",
|
||||
"fuse.js": "^7",
|
||||
"idb-keyval": "^6",
|
||||
"jwt-decode": "^4",
|
||||
"nprogress": "^0.2",
|
||||
"qrcode": "^1.5",
|
||||
"sortablejs": "^1",
|
||||
"universal-cookie": "^7 || ^8",
|
||||
"vue": "^3.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"async-validator": {
|
||||
"optional": true
|
||||
},
|
||||
"axios": {
|
||||
"optional": true
|
||||
},
|
||||
"change-case": {
|
||||
"optional": true
|
||||
},
|
||||
"drauu": {
|
||||
"optional": true
|
||||
},
|
||||
"focus-trap": {
|
||||
"optional": true
|
||||
},
|
||||
"fuse.js": {
|
||||
"optional": true
|
||||
},
|
||||
"idb-keyval": {
|
||||
"optional": true
|
||||
},
|
||||
"jwt-decode": {
|
||||
"optional": true
|
||||
},
|
||||
"nprogress": {
|
||||
"optional": true
|
||||
},
|
||||
"qrcode": {
|
||||
"optional": true
|
||||
},
|
||||
"sortablejs": {
|
||||
"optional": true
|
||||
},
|
||||
"universal-cookie": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz",
|
||||
"integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.1.0",
|
||||
"@vueuse/shared": "14.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz",
|
||||
"integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz",
|
||||
"integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||
@@ -1305,7 +1417,8 @@
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -1356,6 +1469,7 @@
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
@@ -1553,6 +1667,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
@@ -2445,7 +2572,8 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -3148,6 +3276,16 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/universal-cookie": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz",
|
||||
"integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/integrations": "^14.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.12.0",
|
||||
"flowbite": "^1.8.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"universal-cookie": "^8.0.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
|
||||
9
enlish-vue/src/api/user.js
Normal file
9
enlish-vue/src/api/user.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from "@/axios";
|
||||
|
||||
export function login(data) {
|
||||
return axios.post("/login/login", data)
|
||||
}
|
||||
|
||||
export function getVerificationCode(data) {
|
||||
return axios.post("/login/sendVerificationCode", data)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "@/composables/auth";
|
||||
import { showMessage } from "@/composables/util.js";
|
||||
|
||||
// 创建 Axios 实例
|
||||
const instance = axios.create({
|
||||
@@ -6,5 +8,34 @@ const instance = axios.create({
|
||||
timeout: 7000, // 请求超时时间
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
instance.interceptors.request.use(function (config) {
|
||||
const token = getToken()
|
||||
console.log('统一添加请求头中的 Token:' + token)
|
||||
|
||||
// 当 token 不为空时
|
||||
if (token) {
|
||||
// 添加请求头, key 为 Authorization,value 值的前缀为 'Bearer '
|
||||
config.headers['Authorization'] = 'Bearer ' + token
|
||||
}
|
||||
return config;
|
||||
}, function (error) {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error)
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
instance.interceptors.response.use(function (response) {
|
||||
// 2xx 范围内的状态码都会触发该函数。
|
||||
// 对响应数据做点什么
|
||||
return response
|
||||
}, function (error) {
|
||||
// 若后台有错误提示就用提示文字,默认提示为 '请求失败'
|
||||
let errorMsg = error.response.data.message || '请求失败'
|
||||
// 弹错误提示
|
||||
showMessage(errorMsg, 'error')
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
// 暴露出去
|
||||
export default instance;
|
||||
|
||||
20
enlish-vue/src/composables/auth.js
Normal file
20
enlish-vue/src/composables/auth.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
|
||||
// 存储在 Cookie 中的 Token 的 key
|
||||
const TOKEN_KEY = 'Authorization'
|
||||
const cookie = useCookies()
|
||||
|
||||
// 获取 Token 值
|
||||
export function getToken() {
|
||||
return cookie.get(TOKEN_KEY)
|
||||
}
|
||||
|
||||
// 设置 Token 到 Cookie 中
|
||||
export function setToken(token, expires = 2592000) {
|
||||
return cookie.set(TOKEN_KEY, token, { expires })
|
||||
}
|
||||
|
||||
// 删除 Token
|
||||
export function removeToken() {
|
||||
return cookie.remove(TOKEN_KEY)
|
||||
}
|
||||
@@ -7,12 +7,14 @@
|
||||
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Flowbite</span>
|
||||
</a>
|
||||
<div class="flex items-center lg:order-2">
|
||||
<a href="#" @click.prevent="showLogin = true"
|
||||
class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">
|
||||
Login
|
||||
</a>
|
||||
<a href="#"
|
||||
class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Log
|
||||
in</a>
|
||||
<a href="#"
|
||||
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">Get
|
||||
started</a>
|
||||
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
|
||||
Get started
|
||||
</a>
|
||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||
@@ -38,22 +40,19 @@
|
||||
aria-current="page">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
to="/"
|
||||
<router-link to="/"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
||||
班级
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
to="/learningplan"
|
||||
<router-link to="/learningplan"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
||||
学案
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
to="/uploadpng"
|
||||
<router-link to="/uploadpng"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
||||
上传图片
|
||||
</router-link>
|
||||
@@ -62,8 +61,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<LoginDialog v-model="showLogin" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import LoginDialog from '@/layouts/components/LoginDialog.vue'
|
||||
const showLogin = ref(false)
|
||||
</script>
|
||||
|
||||
288
enlish-vue/src/layouts/components/LoginDialog.vue
Normal file
288
enlish-vue/src/layouts/components/LoginDialog.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="visible" class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50 p-4"
|
||||
@click.self="close">
|
||||
<div class="relative w-full max-w-md p-8 border border-gray-100 shadow-xl rounded-xl bg-white">
|
||||
<button @click="close" aria-label="关闭"
|
||||
class="absolute top-3 right-3 text-gray-500 hover:text-gray-700 focus:outline-none">✕</button>
|
||||
<div class="text-center px-2">
|
||||
<div class="mt-2 grid grid-cols-3 gap-2">
|
||||
<button type="button" @click="switchMode('password')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||
:class="mode === 'password' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||
账号密码登录
|
||||
</button>
|
||||
<button type="button" @click="switchMode('code')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||
:class="mode === 'code' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||
验证码登录
|
||||
</button>
|
||||
<button type="button" @click="switchMode('register')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||
:class="mode === 'register' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||
验证码注册
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-6 min-h-[300px]">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<div v-if="mode === 'password'" key="password">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<input type="text" v-model="form.phone" maxlength="20" placeholder="手机号"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<input type="password" v-model="form.password" maxlength="20" placeholder="密码"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button @click="userLogin"
|
||||
class="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-2.5 px-4 rounded-lg focus:outline-none focus:shadow-outline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="loading || !canSubmit">
|
||||
<span v-if="!loading">立即登录</span>
|
||||
<span v-else class="inline-flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
登录中...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="mode === 'code'" key="code">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<input type="text" v-model="form.phone" maxlength="20" placeholder="手机号"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" v-model="form.code" maxlength="6" placeholder="验证码"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
<button type="button" @click="sendCode"
|
||||
:disabled="codeDisabled || !form.phone"
|
||||
class="ml-auto bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm px-3 py-2 rounded focus:outline-none focus:shadow-outline whitespace-nowrap flex-shrink-0 inline-flex items-center justify-center min-w-[100px]">
|
||||
{{ codeBtnText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button @click="userLogin"
|
||||
class="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-2.5 px-4 rounded-lg focus:outline-none focus:shadow-outline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="loading || !canSubmit">
|
||||
<span v-if="!loading">立即登录</span>
|
||||
<span v-else class="inline-flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
处理中...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else key="register">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<input type="text" v-model="form.phone" maxlength="20" placeholder="手机号"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" v-model="form.name" maxlength="20" placeholder="姓名"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<input type="password" v-model="form.password" maxlength="20" placeholder="设置密码"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" v-model="form.code" maxlength="6" placeholder="验证码"
|
||||
class="appearance-none bg-transparent border border-gray-300 rounded-lg w-full px-3 py-2 text-gray-700 leading-tight focus:outline-none focus:border-blue-500">
|
||||
<button type="button" @click="sendCode"
|
||||
:disabled="codeDisabled || !form.phone"
|
||||
class="ml-auto bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm px-3 py-2 rounded focus:outline-none focus:shadow-outline whitespace-nowrap flex-shrink-0 inline-flex items-center justify-center min-w-[100px]">
|
||||
{{ codeBtnText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button @click="userLogin"
|
||||
class="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-2.5 px-4 rounded-lg focus:outline-none focus:shadow-outline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="loading || !canSubmit">
|
||||
<span v-if="!loading">验证码注册</span>
|
||||
<span v-else class="inline-flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
处理中...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { login, getVerificationCode,} from '@/api/user'
|
||||
import { setToken } from '../../composables/auth'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const mode = ref('password')
|
||||
const form = ref({
|
||||
phone: '',
|
||||
name: '',
|
||||
password: '',
|
||||
code: ''
|
||||
})
|
||||
const codeDisabled = ref(false)
|
||||
const codeBtnText = ref('发送验证码')
|
||||
let timer = null
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (mode.value === 'password') {
|
||||
return form.value.phone.trim() && form.value.password.trim()
|
||||
}
|
||||
if (mode.value === 'code') {
|
||||
return form.value.phone.trim() && form.value.code.trim()
|
||||
}
|
||||
return form.value.phone.trim() && form.value.name.trim() && form.value.password.trim() && form.value.code.trim()
|
||||
})
|
||||
|
||||
function switchMode(m) {
|
||||
mode.value = m
|
||||
}
|
||||
function close() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
async function sendCode() {
|
||||
if (!form.value.phone || codeDisabled.value) return
|
||||
codeDisabled.value = true
|
||||
try {
|
||||
await getVerificationCode({ phone: form.value.phone.trim() })
|
||||
ElMessage.success('验证码已发送')
|
||||
let count = 60
|
||||
codeBtnText.value = `${count}s`
|
||||
timer = setInterval(() => {
|
||||
count -= 1
|
||||
if (count <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
codeDisabled.value = false
|
||||
codeBtnText.value = '发送验证码'
|
||||
} else {
|
||||
codeBtnText.value = `${count}s`
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
codeDisabled.value = false
|
||||
codeBtnText.value = '发送验证码'
|
||||
}
|
||||
}
|
||||
async function userLogin() {
|
||||
if (!canSubmit.value || loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (mode.value === 'register') {
|
||||
const res = await login({
|
||||
phone: form.value.phone.trim(),
|
||||
name: form.value.name.trim(),
|
||||
password: form.value.password.trim(),
|
||||
code: form.value.code.trim()
|
||||
})
|
||||
const data = res.data
|
||||
if (data?.success) {
|
||||
ElMessage.success('注册成功')
|
||||
emit('success', res)
|
||||
emit('update:modelValue', false)
|
||||
} else {
|
||||
ElMessage.error(data?.message || '注册失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
const res = await login({
|
||||
phone: form.value.phone.trim(),
|
||||
name: mode.value === 'password' ? '' : form.value.name.trim(),
|
||||
password: mode.value === 'password' ? form.value.password : '',
|
||||
code: mode.value === 'code' ? form.value.code.trim() : ''
|
||||
})
|
||||
const data = res.data
|
||||
if (data?.success) {
|
||||
try { setToken(data.data) } catch { }
|
||||
ElMessage.success('登录成功')
|
||||
emit('success', res)
|
||||
emit('update:modelValue', false)
|
||||
} else {
|
||||
ElMessage.error(data?.message || '登录失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
if (v) {
|
||||
form.value = { phone: '', name: '', password: '', code: '' }
|
||||
mode.value = 'password'
|
||||
} else {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
codeDisabled.value = false
|
||||
codeBtnText.value = '发送验证码'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity .2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from,
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
</style>
|
||||
36
pom.xml
36
pom.xml
@@ -40,7 +40,7 @@
|
||||
<jackson.version>2.16.1</jackson.version>
|
||||
<mysql-connector-java.version>8.0.29</mysql-connector-java.version>
|
||||
<druid.version>1.2.23</druid.version>
|
||||
<sa-token.version>1.38.0</sa-token.version>
|
||||
<sa-token>1.44.0</sa-token>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
<hutool.version>5.8.26</hutool.version>
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
@@ -209,14 +209,14 @@
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
<version>${sa-token}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||
<!-- Sa-Token 整合 RedisTemplate -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
<artifactId>sa-token-redis-template</artifactId>
|
||||
<version>${sa-token}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 相关工具类 -->
|
||||
@@ -248,6 +248,32 @@
|
||||
<artifactId>activation</artifactId>
|
||||
<version>${activation.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云短信发送 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<version>${dysmsapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.15</version>
|
||||
</dependency>
|
||||
|
||||
<!-- no more than 2.3.3-->
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
|
||||
Reference in New Issue
Block a user