微服务的编程测评系统10-竞赛删除发布-用户管理-登录注册

发布于:2025-08-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

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覆盖的
不然就会一边登录,另一边就不能登录了

总结


网站公告

今日签到

点亮在社区的每一天
去签到