提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
1. 竞赛删除
1.1 后端
@DeleteMapping("/delete")
public R<Void> delete(Long examId){
log.info("删除竞赛examId:{}",examId);
return toR(iExamService.delete(examId));
}
@Override
public int delete(Long examId) {
Exam exam = getExamById(examId);
if(LocalDateTime.now().isAfter(exam.getStartTime())){
throw new ServiceException(ResultCode.EXAM_HAVE_STARED);
}
examQuestionMapper.delete(new LambdaQueryWrapper<ExamQuestion>().eq(ExamQuestion::getExamId,examId));
return examMapper.deleteById(exam);
}
因为没有题目的时候examQuestionMapper.delete会返回0,所以返回examMapper.deleteById
这样就OK了
1.2 前端
export function deleteExamService(examId) {
return service({
url: "/exam/delete",
method: "delete",
params: { examId },
});
}
async function onDelete(examId){
await deleteExamService(examId)
ElMessage.success("删除竞赛成功")
params.pageNum = 1;
getExamList();
}
这样就OK了
2. 竞赛发布
2.1 竞赛发布后端
竞赛发布,在添加竞赛,编辑竞赛,还有竞赛列表中都可以发布
发布竞赛,第一要存在这个竞赛,第二要有题目才可以发布竞赛
只有发布了的竞赛才可以显示在c端的竞赛列表中
@PutMapping("/publish")
public R<Void> publish(Long examId){
log.info("点击了竞赛的发布按钮examId:{}",examId);
return toR(iExamService.publish(examId));
}
public static final int TRUE = 1;
public static final int FALSE = 0;
@Override
public int publish(Long examId) {
Exam exam = getExamById(examId);
//查看这个竞赛中是否有题目
Long count = examQuestionMapper.selectCount(new LambdaQueryWrapper<ExamQuestion>()
.eq(ExamQuestion::getExamId, examId));
if(count==null|| count<=0){
throw new ServiceException(ResultCode.EXAM_NOT_HAVE_QUESTION);
}
exam.setStatus(Constants.TRUE);
return examMapper.updateById(exam);
}
这样就可以了,然后就是注意一下,就死已经到了开始时间的竞赛,可以中途发布,让c端用户看到,或者去练习竞赛,不用参加竞赛
2.2 竞赛撤销发布-后端
前提:竞赛存在,竞赛还没开始,已经开始的竞赛,不能撤销发布
但是没有发布的竞赛,到了开始时间,可以发布
@Override
public int publish(Long examId) {
Exam exam = getExamById(examId);
//查看这个竞赛中是否有题目
Long count = examQuestionMapper.selectCount(new LambdaQueryWrapper<ExamQuestion>()
.eq(ExamQuestion::getExamId, examId));
if(count==null|| count<=0){
throw new ServiceException(ResultCode.EXAM_NOT_HAVE_QUESTION);
}
exam.setStatus(Constants.TRUE);
return examMapper.updateById(exam);
}
@Override
public int cancelPublish(Long examId) {
Exam exam = getExamById(examId);
if(LocalDateTime.now().isAfter(exam.getStartTime())){
throw new ServiceException(ResultCode.EXAM_HAVE_STARED);
}
exam.setStatus(Constants.FALSE);
return examMapper.updateById(exam);
}
2.3 竞赛发布-撤销发布-前端
export function publishExamService(examId) {
return service({
url: "/exam/publish",
method: "put",
params: { examId },
});
}
export function cancelPublishExamService(examId) {
return service({
url: "/exam/cancelPublish",
method: "put",
params: { examId },
});
}
在添加和编辑中
//发布竞赛
async function publishExam(){
await publishExamService(formExam.examId)
ElMessage.success("竞赛发布成功")
router.push("/oj/layout/exam")
}
在题目列表中
async function publishExam(examId){
await publishExamService(examId)
ElMessage.success("发布竞赛成功")
getExamList();
}
async function cancelPublishExam(examId){
await cancelPublishExamService(examId)
ElMessage.success("取消发布竞赛成功")
getExamList();
}
这样就成功了
3. C端用户管理
定时任务我们最后来设计
3.1 C端用户表结构设计
B端:C端用户列表功能
拉黑用户操作
C端:登录注册,修改个人信息,退出登录
create table tb_user(
user_id bigint unsigned NOT NULL COMMENT '用户id(主键)',
nick_name varchar(20) comment '用户昵称',
head_image varchar(100) comment '用户头像',
sex tinyint comment '用户状态1: 男 2:女',
phone char(11) not null comment '手机号',
code char(6) comment '验证码',
email varchar(20) comment '邮箱',
wechat varchar(20) comment '微信号',
school_name varchar(20) comment '学校',
major_name varchar(20) comment '专业',
introduce varchar(100) comment '个人介绍',
status tinyint not null comment '用户状态0: 拉黑 1:正常',
create_by bigint unsigned not null comment '创建人',
create_time datetime not null comment '创建时间',
update_by bigint unsigned comment '更新人',
update_time datetime comment '更新时间',
primary key(`user_id`)
)
注意就是登录和注册都是一个接口
验证码我们存入数据库中,但是这个是不太合适的,先这样处理
手机号作为唯一标识
登录的时候,有头像和昵称,点击了退出登录的话,头像和昵称就没了
head_image 是存储的图片地址
用户的修改人,创建人都是自己
3.2 用户列表功能-后端
用户id不能模糊查询,但是用户昵称可以模糊查询
@JsonSerialize(using = ToStringSerializer.class) 是 Jackson 框架中的一个注解,主要作用是将 Java 对象序列化为 JSON 时,强制将指定字段的值转换为字符串(String)类型。
@Getter
@Setter
@TableName("tb_user")
public class User extends BaseEntity {
@JsonSerialize(using = ToStringSerializer.class)
@TableId(value = "USER_ID", type = IdType.ASSIGN_ID)
private Long userId;
private String nickName;
private String headImage;
private Integer sex;
private String phone;
private String code;
private String email;
private String wechat;
private String schoolName;
private String majorName;
private String introduce;
private Integer status;
}
@Data
public class UserQueryDTO extends PageQueryDTO {
private Long userId;
private String nickName;
}
@Getter
@Setter
public class UserVO {
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
private String nickName;
private Integer sex;
private String phone;
private String email;
private String wechat;
private String schoolName;
private String majorName;
private String introduce;
private Integer status;
}
<?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.ck.system.mapper.user.UserMapper">
<select id="selectUserList" resultType="com.ck.system.domain.user.vo.UserVO">
SELECT
user_id,
nick_name,
sex,
phone,
email,
wechat,
school_name,
major_name,
introduce,
status
FROM
tb_user
<where>
<if test="userId !=null ">
user_id = #{userId}
</if>
<if test="nickName !=null and nickName != ''">
nick_name like concat('%', #{nickName}, '%')
</if>
</where>
ORDER BY
create_time DESC
</select>
</mapper>
@Override
public List<UserVO> list(UserQueryDTO userQueryDTO) {
PageHelper.startPage(userQueryDTO.getPageNum(),userQueryDTO.getPageSize());
return userMapper.selectUserList(userQueryDTO);
}
3.3 mybatis-plus打印sql语句
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
加上这个配置, mybatis-plus就可以打印sql语句了
3.4 拉黑功能-后端
拉黑,第一需要被拉黑的用户id,然后是status,是拉黑还是解禁
被拉黑的用户,就不能提交代码,就不能报名参赛了
@Data
public class UserUpdateStatusDTO {
private Long userId;
private Integer status;
}
@Override
public int updateStatus(UserUpdateStatusDTO userUpdateStatusDTO) {
User user = userMapper.selectById(userUpdateStatusDTO.getUserId());
if(user == null){
throw new ServiceException(ResultCode.FAILED_USER_NOT_EXISTS);
}
user.setStatus(userUpdateStatusDTO.getStatus());
return userMapper.updateById(user);
}
@PutMapping("/updateStatus")
public R<Void> updateStatus(@RequestBody UserUpdateStatusDTO userUpdateStatusDTO){
log.info("修改用户状态,拉黑或者解禁userUpdateStatusDTO:{}", userUpdateStatusDTO);
return toR(userService.updateStatus(userUpdateStatusDTO));
}
3.5 用户列表-拉黑-前端
<el-tag type="error" v-else>拉黑</el-tag>
tag 标签表示状态
import service from '@/utils/request'
export function getUserListService(params) {
return service({
url: "/user/list",
method: "get",
params,
});
}
export function updateStatusService(params = {}) {
return service({
url: "/user/updateStatus",
method: "put",
data: params,
});
}
有data对应post或者put
直接params,或者,params+{参数}对应get
<template>
<!-- 表单 -->
<el-form inline="true">
<el-form-item label="用户id">
<el-input v-model="params.userId" placeholder="请您输入要搜索的用户id" />
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="params.nickName" placeholder="请您输入要搜索的用户昵称" />
</el-form-item>
<el-form-item>
<el-button @click="onSearch" plain>搜索</el-button>
<el-button @click="onReset" plain type="info">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table height="526px" :data="userList">
<el-table-column prop="userId" label="用户id" width="180px" />
<el-table-column prop="nickName" label="用户昵称" />
<el-table-column prop="sex" label="用户性别">
<template #default="{ row }">
<div v-if="row.sex === 1" style="color:#3EC8FF;">男</div>
<div v-if="row.sex === 2" style="color:#FD4C40;">女</div>
</template>
</el-table-column>
<el-table-column prop="phone" width="120px" label="手机号" />
<el-table-column prop="email" width="120px" label="邮箱" />
<el-table-column prop="wechat" width="120px" label="微信号" />
<el-table-column label="学校/专业" width="150px">
<template #default="{ row }">
<span class="block-span"> 学校: {{ row.schoolName }}</span>
<span class="block-span"> 专业: {{ row.majorName }}</span>
</template>
</el-table-column>
<el-table-column prop="introduce" label="个人介绍" />
<el-table-column prop="status" width="90px" label="用户状态">
<template #default="{ row }">
<el-tag type="success" v-if="row.status">正常</el-tag>
<el-tag type="error" v-else>拉黑</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80px" fixed="right">
<template #default="{ row }">
<el-button class="red" v-if="row.status === 1" type="text" plain
@click="onUpdateUserStatus(row.userId, 0)">拉黑</el-button>
<el-button v-if="row.status === 0" type="text" plain
@click="onUpdateUserStatus(row.userId, 1)">解禁</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<el-pagination background size="small" layout="total, sizes, prev, pager, next, jumper" :total="total"
v-model:current-page="params.pageNum" v-model:page-size="params.pageSize" :page-sizes="[5, 10, 15, 20]"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</template>
<script setup>
import { reactive, ref } from 'vue';
import { getUserListService, updateStatusService } from '@/apis/cuser'
const params = reactive({
pageNum: 1,
pageSize: 10,
userId: '',
nickName: '',
})
const userList = ref([])
const total = ref(0)
async function getUserList() {
const ref = await getUserListService(params)
userList.value = ref.rows
total.value = ref.total
}
getUserList()
function onSearch() {
params.pageNum = 1
getUserList()
}
function onReset() {
params.pageNum = 1
params.pageSize = 10
params.userId = ''
params.nickName = ''
getUserList()
}
function handleSizeChange(newSize) {
params.pageNum = 1
getUserList()
}
function handleCurrentChange(newPage) {
getUserList()
}
const updateStatusParams = reactive({
userId: '',
status: '',
})
async function onUpdateUserStatus(userId, status) {
console.log("userId:",userId)
updateStatusParams.userId = userId
updateStatusParams.status = status
console.log("updateStatusParams:",updateStatusParams)
await updateStatusService(updateStatusParams)
getUserList()
}
</script>
这样就OK了,很简单
4. C端用户登录注册
4.1 业务分析
采用的是手机号验证码的登录方式
然后就是登录注册两个逻辑合在一起
老用户的手机号,就是直接登录
新用户的手机号的话,就是直接注册了 ,然后自动登录了
现在我们要在friend下面写代码了
@Data
public class UserSendCodeDTO {
private String phone;
}
@PostMapping("/sendCode")
public R<Void> sendCode(@RequestBody UserSendCodeDTO userSendCodeDTO) {
log.info("发送验证码,UserSendCodeDTO:{}",userSendCodeDTO);
return toR(userService.sendCode(userSendCodeDTO));
}
因为手机号比较隐私,所以不用get请求来暴露,post比较隐秘
@Override
public int sendCode(UserSendCodeDTO userSendCodeDTO) {
//先校验手机号格式对不对
if(!checkPhone(userSendCodeDTO.getPhone())){
throw new ServiceException(ResultCode.PHONE_STYLE_ERR);
}
//生成六位随机数
String code = RandomUtil.randomNumbers(6);
return 0;
}
public static boolean checkPhone(String phone) {
Pattern regex = Pattern.compile("^1[2|3|4|5|6|7|8|9][0-9]\\d{8}$");
Matcher m = regex.matcher(phone);
return m.matches();
}
RandomUtil.randomNumbers也是hutool生成随机数的方法
checkPhone是用来检查手机号格式的
然后就是该如何发送手机号验证码的问题了
4.2 集成阿里云短信服务
但是现在短信服务个人都无法使用了,所以我们这里就不做文档操作了,可以根据上面的官网来操作
我们就不采用发送手机验证码给用户的操作了
现在开始写代码了
先给friend增加nacos配置文件
server:
port: 9202
spring:
data:
redis:
host: localhost
password: 123456
datasource:
url: jdbc:mysql://localhost:3306/ckoj_dev?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
username: ojtest
password: 123456
hikari:
minimum-idle: 5 # 最⼩空闲连接数
maximum-pool-size: 20 # 最⼤连接数
idle-timeout: 30000 # 空闲连接存活时间(毫秒)
connection-timeout: 30000 # 连接超时时间(毫秒)
jwt:
secret: zxcvbnmasdfghjkl
然后就是网关的nacos配置文件
- id: oj-friend
uri: lb://oj-friend
predicates:
- Path=/friend/**
filters:
- StripPrefix=1
反正friend的配置和system差不多
4.3 验证码发送
第一获取验证码比较频繁,而且每次获取验证码都不一样,第三就是验证码有有效时间的
所以我们可以在redis中存储验证码
前缀可以是phone:code:手机号
每天限发50次,记录发送次数,第二天清零,还是用redis
key是phone:code:times:手机号
发送次数超过50次就不能发送了
然后还有就是发送之后,60s内不能再次发送
@Service
public class UserServiceImpl implements IUserService {
@Value("${sms.phone-code-time-minute:5}")
private Long phoneCodeTime;//验证码过期时间
@Value("${sms.phone-code-limit-times:3}")
private Long codeLimitTimes;//每天发送限制次数
@Autowired
private RedisService redisService;
@Override
public boolean sendCode(UserSendCodeDTO dto) {
//先校验手机号格式对不对
if(!checkPhone(dto.getPhone())){
throw new ServiceException(ResultCode.PHONE_STYLE_ERR);
}
//生成六位随机数
String code = RandomUtil.randomNumbers(6);
log.info("手机号发送验证码为,code:{}",code);
String phoneCodeKey = getPhoneCodeKey(dto.getPhone());
//获取上一次发送的剩余缓存时间,发送时间相差60s的话,就不能发送来了
Long codeExpiredTime = redisService.getExpired(phoneCodeKey, TimeUnit.SECONDS);
if(codeExpiredTime!=null && phoneCodeTime * 60 - codeExpiredTime <60){
//expiredTime!=null说明不是第一次发送, <60说明发送太快
throw new ServiceException(ResultCode.PHONE_CODE_NOT_SEND_QUICKLY);
}else{
//要么第一次发送,要么是过了很长时间了
redisService.setCacheObject(phoneCodeKey,code,phoneCodeTime, TimeUnit.MINUTES);
}
String phoneCodeTimesKey = getPhoneCodeTimesKey(dto.getPhone());
//获取redis中存储的次数
Long sendTimes = redisService.getCacheObject(phoneCodeTimesKey, Long.class);
if(sendTimes!=null && sendTimes >= codeLimitTimes){
throw new ServiceException(ResultCode.PHONE_CODE_SEND_TIMES_LIMITED);
}
//要么第一次发送,要么没有超过次数限制
if(sendTimes == null){
//第一次发送的话,那么就设置过期时间为到零点
//先获取过期时间
Long timesExpiredTime = ChronoUnit.SECONDS.between(LocalDateTime.now()
,LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0));
redisService.setCacheObject(phoneCodeTimesKey,0,timesExpiredTime,TimeUnit.SECONDS);
}
//走到这里就是没有超过限制,如果超过了,肯定不为null,肯定走不到这里
redisService.increment(phoneCodeTimesKey);
return true;
}
private String getPhoneCodeTimesKey(String phone) {
return CacheConstants.PHONE_CODE_TIMES_KEY + phone;
}
private String getPhoneCodeKey(String phone) {
return CacheConstants.PHONE_CODE_KEY + phone;
}
public static boolean checkPhone(String phone) {
Pattern regex = Pattern.compile("^1[2|3|4|5|6|7|8|9][0-9]\\d{8}$");
Matcher m = regex.matcher(phone);
return m.matches();
}
}
这样就成功了
public Long increment(final String key){
return redisTemplate.opsForValue().increment(key);
}
这个increment就是对value进行加1,然后返回加1后的value的值
public static final String PHONE_CODE_KEY = "phone:code:key:";
public static final String PHONE_CODE_TIMES_KEY = "phone:code:times:key:";
sms:
phone-code-time-minute: 5
phone-code-limit-times: 3
还有就是网关要对这个发送验证码的接口进行过滤,不要登录拦截了
security:
ignore:
whites:
- /**/login
- /friend/user/sendCode
我们把手机号打印出来,就当做是发送了手机号验证码了
这样我们就成功了
@Value(“${sms.phone-code-time-minute:5}”)
这里写个5的原因就是如果nacos没有配置的话,就会使用默认值5
4.4 登录注册-后端开发
判断验证码是否正确
删除redis验证码
注册
返回token
@PostMapping("/loginOrRegister")
public R<String> loginOrRegiter(@RequestBody LoginOrRegisterDTO dto){
log.info("用户登录或注册LoginOrRegisterDTO:{}",dto);
return R.ok(userService.loginOrRegister(dto));
}
@Data
public class LoginOrRegisterDTO {
private String phone;
private String code;
}
@AllArgsConstructor
@Getter
public enum UserStatus {
Block(0),
Normal(1);
private Integer status;
}
@Override
public String loginOrRegister(LoginOrRegisterDTO dto) {
//先检查验证码对不对
String phone =dto.getPhone();
String code =dto.getCode();
checkPhoneCode(phone,code);
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
if(user==null){
//说明是注册
user = new User();
user.setPhone(phone);
user.setStatus(UserStatus.Normal.getStatus());
userMapper.insert(user);
}
//然后是创建token
return tokenService.createToken(user.getUserId(), secret, UserIdentity.ORDINARY.getValue(),user.getNickName() );
}
private void checkPhoneCode(String phone, String code) {
if(!checkPhone(phone)){
throw new ServiceException(ResultCode.PHONE_STYLE_ERR);
}
String cacheCode = redisService.getCacheObject(getPhoneCodeKey(phone), String.class);
if(cacheCode == null){
throw new ServiceException(ResultCode.PHONE_CODE_NO_SEND_OR_EXPIRED);
}
if(!code.equals(cacheCode)){
throw new ServiceException(ResultCode.PHONE_CODE_ERR);
}
redisService.deleteObject(getPhoneCodeKey(phone));
}
这样就可以了,因为为null的user不能set,然后就是插入数据之后,会自动返回id的
其中创建token的函数中是可以缓存数据的,设置过期时间
注意nacos上修改了配置,服务可以不用重新启动
然后我们在设置一个开关,什么意思呢,意思就是开关打开正常发送,开关关闭,验证码就一直为123456
@Value("${sms.is-send:true}")
private Boolean isSend;
public static final String DEFAULT_PHONE_CODE = "123456";
String code = isSend ? RandomUtil.randomNumbers(6) : Constants.DEFAULT_PHONE_CODE;
sms:
phone-code-time-minute: 5
phone-code-limit-times: 3
is-send: false
这样就OK了
4.4 前端项目初始化
创建项目oj-fe-c
项目和oj-fe-b是差不多的
比如拷贝utils
# 使用 npm 安装 安装element-plus
npm install element-plus --save
# 安装elementplus的按需导入
npm install -D unplugin-vue-components unplugin-auto-import
# 使用 npm 安装
npm install -D sass-embedded
npm install axios
npm install js-cookie@3.0.5
还有代理服务器配置
在vite.vonfig.js里面
server: {
proxy: {
"/dev-api": {
target: "http://127.0.0.1:19090/friend",
rewrite: (p) => p.replace(/^\/dev-api/, ""),
},
},
}
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
这个是elementplus的按需导入的配置,在vite.vonfig.js里面
然后main.js也和oj-fe-b一样的
4.5 前端
首页点击登录按钮,就可以跳转过来到登录页
{
path: '/c-oj/home',
name: 'home',
component: () => import('@/views/Home.vue')
},
{
path: '/c-oj/login',
name: 'login',
component: () => import('@/views/Login.vue')
},
{
path: '/',
redirect: '/c-oj/home'
},
先配置两个路由,还有默认路由
<template>
<div class="login-page">
<div class="orange"> </div>
<div class="blue"></div>
<div class="blue small"></div>
<div class="login-box">
<div class="logo-box">
<img src="@/assets/logo.png">
<div>
<div class="sys-name">CK-OJ</div>
<div class="sys-sub-name">帮助ZL学习</div>
</div>
</div>
<div class="form-box-title">
<span>验证码登录</span>
</div>
<div class="form-box">
<div class="form-item">
<img src="@/assets/images/shouji.png">
<el-input v-model="mobileForm.phone" type="text" placeholder="请输入手机号" />
</div>
<div class="form-item">
<img src="@/assets/images/yanzhengma.png">
<el-input style="width:134px" v-model="mobileForm.code" type="text" placeholder="请输入验证码" />
<div class="code-btn-box" @click="getCode">
<span>{{ txt }}</span>
</div>
</div>
<div class="submit-box" @click="loginFun">
登录/注册
</div>
</div>
<div class="gray-bot">
<p>注册或点击登录代表您同意 <span>服务条款</span> 和 <span>隐私协议</span></p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { setToken } from '@/utils/cookie'
import { sendCodeService, codeLoginService } from '@/apis/user'
import router from '@/router'
// 验证码登录表单
let mobileForm = reactive({
phone: '',
code: ''
})
let txt = ref('获取验证码')
let timer = null
async function getCode() {
await sendCodeService(mobileForm)
txt.value = '59s'
let num = 59
timer = setInterval(() => {
num--
if (num < 1) {
txt.value = '重新获取验证码'
clearInterval(timer)
} else {
txt.value = num + 's'
}
}, 1000)
}
async function loginFun() {
const loginRef = await codeLoginService(mobileForm)
setToken(loginRef.data)
router.push('/c-oj/home/question')
}
</script>
<style lang="scss" scoped>
.login-page {
width: 100vw;
height: 100vh;
position: relative;
margin-top: -60px;
margin-left: -20px;
overflow: hidden;
.login-box {
width: 600px;
height: 604px;
background: #FFFFFF;
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);
border-radius: 10px;
opacity: 0.9;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
padding: 0 72px;
padding-top: 50px;
overflow: hidden;
.logo-box {
display: flex;
align-items: center;
&.refister-logo {
margin-bottom: 56px;
}
img {
width: 68px;
height: 68px;
margin-right: 16px;
}
.sys-name {
height: 33px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: #222222;
line-height: 33px;
margin-bottom: 13px;
}
.sys-sub-name {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #222222;
line-height: 22px;
}
}
.form-box-title {
height: 116px;
display: flex;
align-items: center;
span {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
line-height: 33px;
display: block;
height: 33px;
margin-right: 40px;
position: relative;
letter-spacing: 1px;
cursor: pointer;
&.active {
font-weight: bold;
&::before {
position: absolute;
content: '';
bottom: -13px;
left: 0;
width: 100%;
height: 5px;
background: #32C5FF;
border-radius: 10px;
}
}
}
}
.gray-bot {
position: absolute;
left: 0;
text-align: center;
margin-top: 56px;
width: 100%;
height: 50px;
background: #FAFAFA;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #666666;
line-height: 50px;
p {
margin: 0;
}
span {
color: #32C5FF;
cursor: pointer;
}
}
:deep(.form-box) {
.submit-box {
margin-top: 90px;
width: 456px;
height: 48px;
background: #96E1FE;
border-radius: 8px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 16px;
color: #FFFFFF;
letter-spacing: 1px;
&.refister-submit {
margin-top: 72px;
}
&:hover {
background: #32C5FF;
}
}
.form-item {
display: flex;
align-items: center;
width: 456px;
height: 48px;
background: #F8F8F8;
border-radius: 8px;
margin-bottom: 30px;
position: relative;
.code-btn-box {
position: absolute;
right: 0;
width: 151px;
height: 48px;
background: #32C5FF;
border-radius: 8px;
top: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
span {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
}
}
.error-tip {
position: absolute;
width: 140px;
text-align: right;
padding-right: 12px;
height: 20px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #FD4C40;
line-height: 20px;
right: 0;
&.bottom {
right: 157px;
}
}
.el-input {
width: 380px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #222222;
}
.el-input__wrapper {
border: none;
box-shadow: none;
background: transparent;
width: 230px;
padding-left: 0;
}
img {
width: 24px;
height: 24px;
margin: 0 18px;
}
}
}
}
&::after {
position: absolute;
top: 0;
left: 0;
height: 100vh;
bottom: 0;
right: 0;
background: rgba(255, 255, 255, .8);
z-index: 1;
content: '';
}
.orange {
background: #F0714A;
width: 498px;
height: 498px;
border-radius: 50%;
background: #F0714A;
opacity: 0.67;
filter: blur(50px);
left: 14.2%;
top: 41%;
position: absolute;
}
.blue {
width: 334px;
height: 334px;
background: #32C5FF;
opacity: 0.67;
filter: blur(50px);
left: 14.2%;
top: 42%;
position: absolute;
top: 16.3%;
left: 80.7%;
&.small {
width: 186px;
height: 186px;
top: 8.2%;
left: 58.2%;
}
}
}
</style>
然后是Login.vue
然后是图片资源,就是asset目录下的,我们直接全部拷贝就可以了
直接拷贝整个项目需要的assets
main.scss是公共样式,在main.js中引入
import service from "@/utils/request";
export function sendCodeService(params = {}) {
return service({
url: "/user/sendCode",
method: "post",
data: params,
});
}
export function codeLoginService(params = {}) {
return service({
url: "/user/loginOrRegister",
method: "post",
data: params,
});
}
然后是在apis下面创建user.js
然后就成功了
但是我们这里启动失败了,因为elementplus的按需导入,有些东西没有import,可以去看官网
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
这样就成功了
<div class="code-btn-box" @click="getCode">
<span>{{ txt }}</span>
</div>
这个就是获取验证码的按钮
let txt = ref('获取验证码')
是一个响应式数据
async function getCode() {
await sendCodeService(mobileForm)
txt.value = '59s'
let num = 59
timer = setInterval(() => {
num--
if (num < 1) {
txt.value = '重新获取验证码'
clearInterval(timer)
} else {
txt.value = num + 's'
}
}, 1000)
}
txt.value = '59s’这里改了,前端也会改了
setInterval是一个定时函数,每隔一秒就会执行一次
这里进行了限制,第一sendCodeService在60s之内不会发两次,所以再次点击不会重置倒计时的
const TokenKey = "Oj-c-Token";
然后是在cookie.js里面要改一下cookie的名字,不然会把cookie覆盖的
不然就会一边登录,另一边就不能登录了