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>
|
<artifactId>tess4j</artifactId>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<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));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,4 +34,8 @@ tmp:
|
|||||||
|
|
||||||
ai:
|
ai:
|
||||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
url: https://chat.cosonggle.com/v1/chat-messages
|
url: https://chat.cosonggle.com/v1/chat-messages
|
||||||
|
|
||||||
|
aliyun:
|
||||||
|
accessKeyId:
|
||||||
|
accessKeySecret:
|
||||||
@@ -7,4 +7,23 @@ spring:
|
|||||||
|
|
||||||
mybatis:
|
mybatis:
|
||||||
# MyBatis xml 配置文件路径
|
# MyBatis xml 配置文件路径
|
||||||
mapper-locations: classpath:/mapper/**/*.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"/>
|
targetProject="src/main/java"/>
|
||||||
|
|
||||||
<!-- 需要生成的表-实体类 -->
|
<!-- 需要生成的表-实体类 -->
|
||||||
<table tableName="student_lesson_plans" domainObjectName="StudentLessonPlansDO"
|
<table tableName="user" domainObjectName="UserDO"
|
||||||
enableCountByExample="false"
|
enableCountByExample="false"
|
||||||
enableUpdateByExample="false"
|
enableUpdateByExample="false"
|
||||||
enableDeleteByExample="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",
|
"name": "enlish-vue",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/integrations": "^14.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.12.0",
|
"element-plus": "^2.12.0",
|
||||||
"flowbite": "^1.8.1",
|
"flowbite": "^1.8.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"universal-cookie": "^8.0.1",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.4"
|
"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": {
|
"node_modules/@vueuse/metadata": {
|
||||||
"version": "9.13.0",
|
"version": "9.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||||
@@ -1305,7 +1417,8 @@
|
|||||||
"version": "4.2.5",
|
"version": "4.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
@@ -1356,6 +1469,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.4",
|
||||||
@@ -1553,6 +1667,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/copy-anything": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||||
@@ -2445,7 +2572,8 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -3148,6 +3276,16 @@
|
|||||||
"@types/estree": "^1.0.0"
|
"@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": {
|
"node_modules/unplugin": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/integrations": "^14.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.12.0",
|
"element-plus": "^2.12.0",
|
||||||
"flowbite": "^1.8.1",
|
"flowbite": "^1.8.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"universal-cookie": "^8.0.1",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.4"
|
"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 axios from "axios";
|
||||||
|
import { getToken } from "@/composables/auth";
|
||||||
|
import { showMessage } from "@/composables/util.js";
|
||||||
|
|
||||||
// 创建 Axios 实例
|
// 创建 Axios 实例
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
@@ -6,5 +8,34 @@ const instance = axios.create({
|
|||||||
timeout: 7000, // 请求超时时间
|
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;
|
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>
|
<span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Flowbite</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center lg:order-2">
|
<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="#"
|
<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
|
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">
|
||||||
in</a>
|
Get started
|
||||||
<a href="#"
|
</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"
|
<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"
|
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">
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
@@ -38,22 +40,19 @@
|
|||||||
aria-current="page">Home</a>
|
aria-current="page">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link
|
<router-link to="/"
|
||||||
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">
|
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>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link
|
<router-link to="/learningplan"
|
||||||
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">
|
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>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link
|
<router-link to="/uploadpng"
|
||||||
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">
|
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>
|
</router-link>
|
||||||
@@ -62,8 +61,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<LoginDialog v-model="showLogin" />
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import LoginDialog from '@/layouts/components/LoginDialog.vue'
|
||||||
|
const showLogin = ref(false)
|
||||||
</script>
|
</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>
|
<jackson.version>2.16.1</jackson.version>
|
||||||
<mysql-connector-java.version>8.0.29</mysql-connector-java.version>
|
<mysql-connector-java.version>8.0.29</mysql-connector-java.version>
|
||||||
<druid.version>1.2.23</druid.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>
|
<guava.version>33.0.0-jre</guava.version>
|
||||||
<hutool.version>5.8.26</hutool.version>
|
<hutool.version>5.8.26</hutool.version>
|
||||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||||
@@ -209,14 +209,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.dev33</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||||
<version>${sa-token.version}</version>
|
<version>${sa-token}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
<!-- Sa-Token 整合 RedisTemplate -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.dev33</groupId>
|
<groupId>cn.dev33</groupId>
|
||||||
<artifactId>sa-token-redis-jackson</artifactId>
|
<artifactId>sa-token-redis-template</artifactId>
|
||||||
<version>${sa-token.version}</version>
|
<version>${sa-token}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 相关工具类 -->
|
<!-- 相关工具类 -->
|
||||||
@@ -248,6 +248,32 @@
|
|||||||
<artifactId>activation</artifactId>
|
<artifactId>activation</artifactId>
|
||||||
<version>${activation.version}</version>
|
<version>${activation.version}</version>
|
||||||
</dependency>
|
</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-->
|
<!-- no more than 2.3.3-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.glassfish.jaxb</groupId>
|
<groupId>org.glassfish.jaxb</groupId>
|
||||||
|
|||||||
Reference in New Issue
Block a user