Procházet zdrojové kódy

学生端个人中心手机号绑定流程改造更新

dengshaobo před 1 měsícem
rodič
revize
b6bda41a58

+ 18 - 0
src/http/api/user.js

@@ -57,6 +57,24 @@ const user = {
     return get(base.prefix + '/api/v1/student_user/un_bind_wechat', data)
   },
 
+  // 个人中心-绑定手机号获取滑块验证
+  getBindPhoneSlider (data) {
+    return post(base.prefix + '/api/v1/student_user/obtain_slider_from_center', data)
+  },
+
+  //重新获取滑块验证码图片数据
+  obtainSliderImg(data)
+  {
+    return post(base.prefix + '/api/v1/student_user/re_obtain_slider',data)
+  },
+
+
+  //登录验证滑动验证码位置是否正确
+  checkSlideCaptcha(data)
+  {
+    return post(base.prefix + '/api/v1/student_user/validate_slider', data)
+  },
+
 
 }
 

+ 529 - 0
src/views/login/components/SlideCode.vue

@@ -0,0 +1,529 @@
+<template>
+  <transition name="zoom-center" appear>
+    <div v-if="visible" class="slide_code_modal" @click.self="HandleConfirm">
+        <div  class="slide_code_content">
+          <!-- <div class="modal_header">
+            <h3>{{ title }}</h3>
+            <i class="el-icon-close close_btn" @click="HandleConfirm"></i>
+          </div> -->
+          
+          <div class="modal_body">
+            <slot>
+              <div class="slider_container">
+                <div class="slider_text">
+                  请拖动滑块完成拼图
+                </div>
+                <div class="container_bg" :style="{ backgroundImage:  sliderBgUrl ? 'url(' + sliderBgUrl + ')' : '' }">
+                  <div class="slider_bg"  :style="{ left: sliderPositionX + 'px',top:sliderPositionY+'px', backgroundImage:  sliderImgUrl ? 'url(' + sliderImgUrl + ')' : '' }">
+
+                  </div>
+                  <!-- 验证成功的样式 -->
+                  <transition name="slide-up">
+                    <div class="slider_sucess" v-if="isSuccess">
+                      {{sliderTime }}秒的速度超过{{sliderPrent}}%的用户
+                    </div>
+                    <div class="slider_failed" v-if="isFailed">
+                      验证失败,请重新拖动滑块尝试
+                    </div>
+                  </transition>
+                </div>
+                <div class="slider_track">
+                  
+                  <div  class="slider_button"  :style="{ left: sliderPositionX + 'px' }"  @mousedown="startSlide"  @touchstart="startSlide">
+                    <i class="el-icon-d-arrow-right"></i>
+                  </div>
+                 
+                </div>
+              </div>
+            </slot>
+          </div>
+        </div>
+    </div>
+  </transition>
+</template>
+
+<script>
+import user from "@/http/api/user";
+
+export default {
+  name: 'SlideCode',
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    },//关闭打开
+
+    sliderUUID:{
+      type:String,
+      default:''
+    },//图片唯一凭证
+
+    sliderBgUrl:{
+      type:String,
+      default:''
+    },//背景图片地址
+
+    sliderImgUrl:{
+      type:String,
+      default:''
+    },//滑块图片地址
+
+    sliderPositionY:{
+      type:[Number,String],
+      default:0
+    },//滑块y轴位置
+
+    title: {
+      type: String,
+      default: '安全验证'
+    },
+
+  },
+  data() {
+    return {
+      visible: false,
+
+
+      sliderPositionX: 0,//滑块的x轴位置
+      sliderWidth: 0,
+      sliderTime: 0,//滑动耗时(秒)
+      sliderPrent:'98',//百分比
+      startTime: 0,//开始滑动的时间戳
+      sliderVerified:0,//滑动验证码验证状态 0 未开始  1 验证成功  -1 验证失败
+      isSliding: false,//是否开始滑动
+      isSuccess:false,//是否验证成功
+      isFailed:false,//是否验证失败
+      startX: 0,//这里将用于存储鼠标相对于轨道的初始偏移
+
+      sliderText: '按住滑块拖动',
+
+    };
+  },
+  watch: {
+    value: {
+      handler(newVal) {
+         this.visible = newVal;
+         this.ResetSlider();//重置滑块位置
+      },
+      immediate: true
+    },
+  },
+  methods: {
+
+
+    //验证滑动验证码位置是否正确
+    VerifySliderCaptcha()
+    {
+      let param={
+        sliderUUID: this.sliderUUID,//图片唯一凭证
+        x: Math.round(this.sliderPositionX),//滑块位置
+      };
+     
+      user.checkSlideCaptcha(param).then(res =>
+      {
+        console.log("验证滑动验证码返回结果",res);
+       
+        if(res.code==200)
+        {
+          //验证成功
+          this.isSuccess = true;
+          this.sliderVerified = 1;
+          this.sliderPhone=res.data;
+          setTimeout(() => {
+            this.HandleConfirm();//关闭弹窗
+          }, 1000);
+          
+          // 验证成功后,发送请求获取验证码
+          // this.sliderPhone=res.data;
+          
+          // const timer = setInterval(() => {
+          //   this.count--;
+          //   this.codeText = `${this.count} 秒`;
+          //   if (this.count <= 0) {
+          //     clearInterval(timer);
+          //     this.isCount = false;
+          //     this.count = 300;
+          //     this.codeText = '重新获取'
+          //   }
+          // }, 1000);
+
+        }
+        else if(res.code==601)
+        {
+          //验证成功
+          this.sliderVerified = 1;
+          this.isSuccess = true;
+          // 验证成功后,发送请求获取验证码
+          this.sliderPhone=res.data;
+          this.$message.warning("滑块验证通过,您上次的验证码还在有效期,请输入上次发送的验证码进行登录!");
+          setTimeout(() => {
+            this.HandleConfirm();//关闭弹窗
+          }, 1000);
+        }
+        else if(res.code==602)
+        {
+          //滑动验证码过期 重新获取验证码
+          this.$message.error("滑块验证码已过期,已为您重新获取!");
+          // this.ObtainSliderImg();
+          this.$emit('refresh');//重新获取验证码
+        }
+        else if(res.code==400)
+        {
+          this.isFailed=true;
+          //滑动验证码过期 重新获取验证码
+          // this.$message.error(res.msg);
+          this.sliderVerified = -1;
+          this.sliderPositionX=0;
+        }
+        else{
+          this.isFailed=true;
+          //验证失败
+          this.sliderVerified = -1;
+          //重置滑块位置
+          this.sliderPositionX=0;
+        }
+
+        //延迟2秒关闭失败层
+        setTimeout(() => {
+          this.isFailed = false;
+
+        }, 2000);
+      })
+
+    },
+
+    
+
+
+    //关闭弹窗
+    HandleConfirm() {
+      this.visible = false;
+
+      this.$emit('close',this.sliderVerified,this.sliderPhone);
+    },
+
+  
+    //重置滑块位置
+    ResetSlider() {
+      this.sliderPositionX = 0;
+      this.sliderVerified = 0;
+      this.sliderWidth = 0;
+      this.sliderText = '按住滑块拖动';
+      this.isSuccess = false;
+      this.isFailed=false;
+    },
+
+    //开始滑动事件
+    startSlide(event) {
+      event.preventDefault();
+      event.stopPropagation();
+      this.isSliding = true;//是否滑动
+
+      // 记录开始时间
+      this.startTime = Date.now();
+      // 获取轨道元素
+      const track = this.$el.querySelector('.slider_track');
+      const rect = track.getBoundingClientRect();
+      
+      // 计算鼠标点击位置距离轨道左边缘的距离
+      const clientX = event.type.includes('mouse') ? event.clientX : event.touches[0].clientX;
+      this.startX = clientX - rect.left - this.sliderPositionX;
+      
+      document.addEventListener('mousemove', this.onSlide);
+      document.addEventListener('mouseup', this.stopSlide);
+      document.addEventListener('touchmove', this.onSlide, { passive: false });
+      document.addEventListener('touchend', this.stopSlide);
+    },
+    
+    onSlide(event) 
+    {
+      if (!this.isSliding || this.isSuccess) return;
+      
+      const track = this.$el.querySelector('.slider_track');
+      const rect = track.getBoundingClientRect();
+      
+      // 计算当前鼠标距离轨道左边缘的距离
+      const currentClientX = event.type.includes('mouse') ? event.clientX : event.touches[0].clientX;
+      const currentRelativeX = currentClientX - rect.left;
+      
+      // 新的滑块位置 = 当前鼠标相对位置 - 初始点击时的相对偏移
+      // 这样能保证滑块跟随鼠标移动,且不会发生跳变
+      let newPixelPosition = currentRelativeX - this.startX;
+
+      // 计算最大可移动距离
+      const maxPosition = 300 - 50;
+      
+      // 限制范围                              
+      newPixelPosition = Math.max(0, Math.min(maxPosition, newPixelPosition));
+      
+      this.sliderPositionX = newPixelPosition;
+      
+
+      
+     
+    },
+    
+    stopSlide() {
+      if (!this.isSliding) return;
+      // 计算滑动耗时
+      const endTime = Date.now();
+      this.sliderTime = parseFloat(((endTime - this.startTime) / 1000).toFixed(2));
+      
+      console.log('滑动耗时:', this.sliderTime, '秒');
+       // 根据滑动时间计算百分比
+      if (this.sliderTime <= 1) {
+        this.sliderPrent = 99;
+      } else if (this.sliderTime <= 2) {
+        this.sliderPrent = 98;
+      } else if (this.sliderTime <= 3) {
+        this.sliderPrent = 97;
+      } else {
+        this.sliderPrent = Math.max(0, 96 - Math.floor(this.sliderTime - 3));
+      }
+      this.isSliding = false;
+      document.removeEventListener('mousemove', this.onSlide);
+      document.removeEventListener('mouseup', this.stopSlide);
+      document.removeEventListener('touchmove', this.onSlide);
+      document.removeEventListener('touchend', this.stopSlide);
+      // 松开鼠标后,发送请求验证滑块位置
+      this.VerifySliderCaptcha();
+      
+    },
+    
+
+    
+
+  },
+  
+  beforeDestroy() {
+    document.removeEventListener('mousemove', this.onSlide);
+    document.removeEventListener('mouseup', this.stopSlide);
+    document.removeEventListener('touchmove', this.onSlide);
+    document.removeEventListener('touchend', this.stopSlide);
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.slide_code_modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  // background-color: rgba(0, 0, 0, 0.5);//不要遮罩层
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+}
+
+.slide_code_content {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  min-width: 300px;
+  max-width: 600px;
+}
+
+.modal_header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #e4e7ed;
+  
+  h3 {
+    margin: 0;
+    font-size: 16px;
+    color: #303133;
+  }
+  
+  .close_btn {
+    cursor: pointer;
+    font-size: 18px;
+    color: #909399;
+    transition: color 0.3s;
+    
+    &:hover {
+      color: #f56c6c;
+    }
+  }
+}
+
+.modal_body {
+  padding: 20px;
+  min-height: 100px;
+}
+
+.modal_footer {
+  padding: 12px 20px;
+  border-top: 1px solid #e4e7ed;
+  text-align: right;
+  
+  .el-button {
+    margin-left: 10px;
+  }
+}
+
+.slider_container {
+
+  .slider_text
+  {
+    text-align: center;
+    height: 36px;
+    font-size: 16px;
+  }
+  //背景的图片
+  .container_bg
+  {
+    width: 300px;
+    height: 180px;
+    background-size: 100% 100%;
+    background-position: 0 0;
+    background-repeat: no-repeat;
+    position: relative;
+    overflow: hidden;
+    .slider_bg
+    {
+      width: 50px;
+      height: 50px;
+      background-position: 0 0;
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      position: absolute;
+      left: 0;
+      top: 35px;
+      border-radius: 5px;
+      border:1px solid red;
+    }
+
+    .slider_sucess
+    {
+      width: 100%;
+      height: 40px;
+      background-color: #2BC644;
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      z-index: 88;
+      line-height: 40px;
+      color:#fff;
+      font-size: 12px;
+      text-align: center;
+    }
+
+    .slider_failed
+    {
+      width: 100%;
+      height: 40px;
+      background-color: #ff5d39;
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      z-index: 88;
+      line-height: 40px;
+      color:#fff;
+      font-size: 12px;
+      text-align: center;
+    }
+  }
+}
+
+.slider_track {
+  position: relative;
+  height: 40px;
+  background-color: #e4e7ed;
+  border-radius: 20px;
+  overflow: hidden;
+  user-select: none;
+  cursor: pointer;
+  margin-top: 20px;
+}
+
+.slider_button {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 50px;
+  height: 36px;
+  background: #fff;
+  border: 2px solid #409eff;
+  border-radius: 25px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+  
+  i {
+    color: #409eff;
+    font-size: 16px;
+  }
+  
+  &:active {
+    cursor: grabbing;
+  }
+}
+
+// 从底部升起的动画
+.slide-up-enter-active {
+  animation: slideUpIn 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.slide-up-leave-active {
+  animation: slideUpOut 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+@keyframes slideUpIn {
+  from {
+    transform: translateY(100%);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+@keyframes slideUpOut {
+  from {
+    transform: translateY(0);
+    opacity: 1;
+  }
+  to {
+    transform: translateY(100%);
+    opacity: 0;
+  }
+}
+
+.zoom-center-enter-active {
+  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.zoom-center-leave-active {
+  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
+}
+.zoom-center-enter,
+.zoom-center-leave-to {
+  opacity: 0;
+  transform: scale(0.5);
+}
+
+.zoom-center-enter-to {
+  opacity: 1;
+  transform: scale(1);
+}
+
+.zoom-center-leave {
+  opacity: 1;
+  transform: scale(1);
+}
+
+.zoom-center-leave-to {
+  opacity: 0;
+  transform: scale(0.5);
+}
+                                   
+</style>

+ 114 - 19
src/views/userInfo/personInfo.vue

@@ -66,7 +66,7 @@
             <div class="bingding_input">
               <el-input v-model="bindingPhoneData.code" placeholder="请输入验证码" prefix-icon="iconfont icon_yanzhengma">
                 <template slot="suffix">
-                  <span class="get_code" @click="GetCode">{{ codeText }}</span>
+                  <span :class="isCount?'code_gray':'get_code'" @click="GetCode">{{ codeText }}</span>
                 </template>
               </el-input>
             </div>
@@ -85,6 +85,7 @@
           
         </div>
       </el-dialog>
+      <SlideCode  v-model="showSlideCode"  title="安全验证" :sliderUUID="sliderUUID" :sliderBgUrl="sliderBgUrl" :sliderPositionY="sliderPositionY" :sliderImgUrl="sliderImgUrl" @close="CloseSlideCode" @refresh="ObtainSliderImg" />
     </div>
 
   </div>
@@ -93,13 +94,14 @@
 import { mapGetters } from "vuex";
 
 import heada_Image from "../../assets/user_img.png";
+import SlideCode from "@/views/login/components/SlideCode.vue";//滑动验证码组件
 
 export default {
   computed: {
     ...mapGetters(["userInfo"]),
   },
   components: {
-
+    SlideCode
   },
   data() {
     return {
@@ -131,6 +133,13 @@ export default {
       wechatUrl: '', // 获取微信二维码返回地址
       qrLoading: false, // 二维码加载loading
       wechatCode: '', // 扫码获取code
+
+      timer:null,//定时器
+      showSlideCode:false,//是否显示滑动验证码
+      sliderUUID:'',//图片唯一凭证
+      sliderBgUrl:'',//画框背景图片
+      sliderImgUrl:'',//滑块图片
+      sliderPositionY:0,//滑块y轴位置
     };
   },
   mounted() {
@@ -158,6 +167,22 @@ export default {
   },
   beforeDestroy() {
     window.removeEventListener('message', this.WechatBind);
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
+
+    // 【新增】清理微信二维码定时器
+    if (this.resizeTimer) {
+      clearInterval(this.resizeTimer);
+      this.resizeTimer = null;
+    }
+    
+    // 清理微信 DOM
+    const container = document.getElementById("wechat-qr-container");
+    if (container) {
+      container.innerHTML = '';
+    }
   },
   methods: {
 
@@ -178,11 +203,11 @@ export default {
 
       console.log("绑定手机号",this.bindingPhoneData);
       if(!this.bindingPhoneData.phone) {
-        this.$message.warning('手机号不能为空!')
+        this.$message.warning('请输入手机号!')
         return
       }
       if(!this.bindingPhoneData.code){
-        this.$message.warning('验证码不能为空!')
+        this.$message.warning('请输入验证码!')
         return
       }
       try {
@@ -219,14 +244,86 @@ export default {
         this.bindPhoneLoading = false
       }
     },
+
+    //关闭滑块验证码弹窗
+    CloseSlideCode(sliderVerified,phoneNumber)
+    {
+      console.log("关闭滑动验证码弹窗",sliderVerified);
+      this.sliderVerified=sliderVerified;
+      
+      if(sliderVerified==1)
+      {
+        //绑定手机验证
+        this.StartCountdown();//启动倒计时
+      }
+      this.showSlideCode=false;
+    },
+
+    //h获取滑动验证码返回
+    ObtainSliderImg()
+    {
+      let param={
+        sliderUUID:this.sliderUUID,//图片唯一凭证
+      };
+      this.$api.user.obtainSliderImg(param).then(res => {
+        console.log("打印重新获取滑动验证码结果", res);
+        if(res.code == 200) 
+        {
+          this.sliderUUID = res.data.sliderUUID
+          this.sliderBgUrl = res.data.backgroundBase64;
+          this.sliderImgUrl = res.data.blockBase64;
+          this.sliderPositionY = res.data.y;//验证码的y坐标
+        }
+        else
+        {
+          this.$message.error(res.msg);
+        }
+      })
+    },
+
+    //启动全局倒计时定时器
+    StartCountdown() 
+    {
+      // 先清除已存在的定时器
+      if (this.timer) {
+        clearInterval(this.timer);
+        this.timer = null;
+      }
+      
+      this.isCount = true;
+      this.count = 300;
+      this.codeText = '300 秒';
+      this.timer = setInterval(() => {
+        this.count--;
+        this.codeText = `${this.count} 秒`;
+        if (this.count <= 0) {
+          this.StopCountdown();
+        }
+      }, 1000);
+    },
+    //停止全局倒计时定时器
+    StopCountdown() 
+    {
+      if (this.timer) {
+        clearInterval(this.timer);
+        this.timer = null;
+      }
+      this.isCount = false;
+      this.count = 300;
+      this.codeText = '重新获取';
+    },
     // 获取手机验证码
     GetCode() 
     {
+      // 如果已经有定时器在运行,先清除它,防止叠加
+      if (this.timer) {
+        clearInterval(this.timer);
+        this.timer = null;
+      }
       if(!this.bindingPhoneData.phone) {
         this.$message.warning('请输入手机号码');
         return
       }
-
       // 验证手机号格式和位数
       const phoneRegex = /^1[3-9]\d{9}$/;
       if (!phoneRegex.test(this.bindingPhoneData.phone)) {
@@ -234,27 +331,25 @@ export default {
         return;
       }
       if(this.isCount) return
-      this.$api.user.getBindPhoneValidCode({
+      this.$api.user.getBindPhoneSlider({
         phoneNumber: this.bindingPhoneData.phone
       }).then(res => {
-        if(res.code == 200) {
-          this.isCount = true
-          this.codeText = `${this.count} 秒`
-          const timer = setInterval(() => {
-            this.count--;
-            this.codeText = `${this.count} 秒`;
-            if (this.count <= 0) {
-              clearInterval(timer);
-              this.isCount = false;
-              this.count = 60;
-              this.codeText = '重新获取'
-            }
-          }, 1000);
+        if(res.code == 200) 
+        {
+          this.sliderUUID = res.data.sliderUUID;
+          this.sliderBgUrl = res.data.backgroundBase64;
+          this.sliderImgUrl = res.data.blockBase64;
+          this.sliderPositionY=res.data.y;
+          this.showSlideCode=true;
+          
         }else {
           this.$message.error(res.msg)
         }
       })
     },
+
+
+
     //打开绑定微信弹窗
     OpenWeChartDialog()
     {

+ 1 - 1
version.json

@@ -1,5 +1,5 @@
 {
-  "version": "0.1.3_2026_4_2_0",
+  "version": "0.1.3_2026_5_6_0",
   "content": [
     {
       "time": "2024-11-1",