Selaa lähdekoodia

Merge branch 'dev-ljx' into 'dev'

登录加滑块

See merge request product-group-fe/sfy-safety-group/sfy-safety!296
ai0197(吴云丰) 3 kuukautta sitten
vanhempi
commit
aee4fbedb6
24 muutettua tiedostoa jossa 3281 lisäystä ja 183 poistoa
  1. 2 0
      package.json
  2. 0 165
      src/components/Login.vue
  3. 48 11
      src/components/Login/components/BaseLogin.vue
  4. 435 0
      src/components/Login/components/slider-captcha/Verify.vue
  5. 279 0
      src/components/Login/components/slider-captcha/Verify/VerifyPoints.vue
  6. 409 0
      src/components/Login/components/slider-captcha/Verify/VerifySlide.vue
  7. 27 0
      src/components/Login/components/slider-captcha/api/index.ts
  8. 12 0
      src/components/Login/components/slider-captcha/types/index.ts
  9. 11 0
      src/components/Login/components/slider-captcha/utils/ase.ts
  10. 97 0
      src/components/Login/components/slider-captcha/utils/util.ts
  11. 434 0
      src/components/Login/components/verifition/Verify.vue
  12. 260 0
      src/components/Login/components/verifition/Verify/VerifyPoints.vue
  13. 467 0
      src/components/Login/components/verifition/Verify/VerifySlide.vue
  14. 27 0
      src/components/Login/components/verifition/api/index.js
  15. 11 0
      src/components/Login/components/verifition/utils/ase.js
  16. 30 0
      src/components/Login/components/verifition/utils/axios.js
  17. 35 0
      src/components/Login/components/verifition/utils/util.js
  18. 509 0
      src/components/Login/components/verify/VerifySlide.vue
  19. 21 0
      src/components/Login/components/verify/utils/aes.ts
  20. 41 0
      src/components/Login/components/verify/utils/api.ts
  21. 46 0
      src/components/Login/components/verify/utils/axios.ts
  22. 72 0
      src/components/Login/components/verify/utils/util.ts
  23. 1 1
      src/hooks/setting/index.ts
  24. 7 6
      utils/devProxy/staff/proxy.ts

+ 2 - 0
package.json

@@ -75,6 +75,7 @@
   "devDependencies": {
     "@commitlint/cli": "17.0.3",
     "@commitlint/config-conventional": "17.0.3",
+    "@types/crypto-js":"4.2.0",
     "@types/element-resize-detector": "1.1.3",
     "@types/intro.js": "3.0.2",
     "@types/lodash-es": "4.17.6",
@@ -91,6 +92,7 @@
     "commitizen": "4.2.5",
     "core-js": "3.23.5",
     "cross-env": "7.0.3",
+    "crypto-js": "^4.2.0",
     "dotenv": "16.0.1",
     "eslint": "8.20.0",
     "eslint-config-prettier": "8.5.0",

+ 0 - 165
src/components/Login.vue

@@ -1,165 +0,0 @@
-<template>
-  <div class="login-container">
-    <div class="login-form">
-      <img :src="exitIcon" alt="关闭" class="exit-icon" @click="emit('close')" />
-      <header class="login-form__header">
-        <span>登录</span>
-      </header>
-      <main class="login-form__main">
-        <el-form
-          ref="formRef"
-          :model="formValue"
-          label-width="auto"
-          class="login-form__form"
-          @keydown.enter="handleLogin"
-        >
-          <el-form-item prop="username" :rules="[{ required: true, message: '账号不能为空' }]">
-            <el-input
-              placeholder="请输入您的账号"
-              v-model="formValue.username"
-              type="text"
-              autocomplete="off"
-              clearable
-              class="el-input--default"
-            />
-          </el-form-item>
-          <el-form-item prop="password" :rules="[{ required: true, message: '密码不能为空' }]">
-            <el-input
-              :placeholder="'请输入您的密码'"
-              v-model="formValue.password"
-              type="password"
-              autocomplete="off"
-              show-password
-              clearable
-              class="el-input--default"
-            />
-          </el-form-item>
-
-          <!-- <el-form-item
-            prop="code"
-            :rules="[{ required: true, message: '验证码不能为空' }]"
-            v-if="type !== 'modifyPassword'"
-          >
-            <el-input placeholder="验证码" v-model="formValue.code" clearable class="el-input--default" />
-          </el-form-item> -->
-        </el-form>
-      </main>
-      <footer class="login-form__footer">
-        <el-button class="login-form__button" @click="handleLogin">登录</el-button>
-      </footer>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-  import { ref, reactive } from 'vue';
-  import { ElMessage } from 'element-plus';
-  import type { FormInstance } from 'element-plus';
-  import exitIcon from 'assets/svg/exit.svg';
-  import { useUserStore } from '@/store/modules/user';
-
-  const userStore = useUserStore();
-
-  const formValue = reactive({
-    username: userStore.info.username,
-    password: '',
-  });
-  const emit = defineEmits(['close']);
-  const formRef = ref<FormInstance>();
-  const handleLogin = () => {
-    if (!formRef.value) return;
-    formRef.value.validate((valid: boolean) => {
-      if (valid) {
-        console.log('valid', formValue);
-        userStore
-          .login(formValue)
-          .then(() => {
-            window.location.reload();
-          })
-          .catch((err) => {
-            ElMessage.error(err.message || '登录失败');
-          });
-      }
-    });
-  };
-</script>
-
-<style lang="scss" scoped>
-  .login-container {
-    @include flex-center;
-    position: fixed;
-    left: 0;
-    top: 0;
-    width: 100%;
-    height: 100%;
-    background-color: rgba(0, 0, 0, 0.42);
-    z-index: 1000;
-  }
-  .login-form {
-    position: relative;
-    width: 420px;
-    padding: 0 20px;
-    background: $white-color;
-    border-radius: 14px;
-    &__header {
-      width: 100%;
-      height: 80px;
-      font-size: 16px;
-      font-weight: 550;
-      color: #333;
-      text-align: center;
-      line-height: 80px;
-      letter-spacing: 5px;
-    }
-    &__main {
-      @include flex-center;
-      flex-direction: column;
-      gap: 20px;
-    }
-    &__code {
-      @include flex-center;
-      justify-content: space-between;
-      width: 100%;
-      height: 44px;
-      .el-input--default {
-        width: 50%;
-      }
-    }
-    &__footer {
-      width: 100%;
-      height: 66px;
-      margin-top: 18px;
-      .login-form__button {
-        width: 100%;
-        height: 44px;
-        font-size: 16px;
-        color: $white-color;
-        background-color: $primary-color;
-        border-radius: 5px;
-        cursor: pointer;
-      }
-    }
-    .exit-icon {
-      position: absolute;
-      top: 15px;
-      right: 16px;
-      width: 24px;
-      height: 24px;
-      cursor: pointer;
-    }
-  }
-  .login-form__form {
-    display: flex;
-    flex-direction: column;
-    gap: 2px;
-    width: 100%;
-  }
-  .el-input--default {
-    width: 100%;
-    height: 44px;
-    :deep(.el-input__inner) {
-      font-size: 14px;
-      color: #999;
-    }
-  }
-</style>

+ 48 - 11
src/components/Login/components/BaseLogin.vue

@@ -48,6 +48,7 @@
         <el-button class="login-form__button" @click="handleLogin">登录</el-button>
       </footer>
     </div>
+    <SliderCaptcha captchaType="blockPuzzle" mode="pop" @success="handleCaptchaPassed" ref="sliderCaptchaRef" />
   </div>
 </template>
 
@@ -59,6 +60,7 @@
   import exitIcon from 'assets/svg/exit.svg';
   import { useUserStore } from '@/store/modules/user';
   import { PWD_KEY } from '@/utils/pwd';
+  import SliderCaptcha from '@/components/Login/components/slider-captcha/Verify.vue';
 
   const props = defineProps<{ title: string }>();
 
@@ -67,28 +69,63 @@
   const formValue = reactive({
     username: userStore.info.username,
     password: '',
+    captchaVerification:'',
+    
   });
+  const sliderCaptchaRef = ref<InstanceType<typeof SliderCaptcha>>();
+
   const emit = defineEmits<{ (e: 'close'): unknown; (e: 'success'): unknown }>();
   const formRef = ref<FormInstance>();
 
+  // const handleLogin = () => {
+  //   if (!formRef.value) return;
+  //   formRef.value.validate((valid: boolean) => {
+  //     if (valid) {
+  //       console.log('valid', formValue);
+  //       formValue.password = md5(PWD_KEY + formValue.password);
+  //       userStore
+  //         .login(formValue)
+  //         .then(() => {
+  //           emit('success');
+  //           // window.location.reload();
+  //         })
+  //         .catch((err) => {
+  //           ElMessage.error(err.message || '登录失败');
+  //         });
+  //     }
+  //   });
+  // };
+
   const handleLogin = () => {
     if (!formRef.value) return;
     formRef.value.validate((valid: boolean) => {
       if (valid) {
-        console.log('valid', formValue);
-        formValue.password = md5(PWD_KEY + formValue.password);
-        userStore
-          .login(formValue)
-          .then(() => {
-            emit('success');
-            // window.location.reload();
-          })
-          .catch((err) => {
-            ElMessage.error(err.message || '登录失败');
-          });
+          sliderCaptchaRef.value?.show();
       }
     });
   };
+
+  const handleCaptchaPassed = ({ captchaVerification }: { captchaVerification: string }) => {
+    onLogin(captchaVerification);
+  };
+
+  const onLogin = (vification: string) => {
+    console.log('valid', formValue);
+    formValue.captchaVerification=vification
+    const encryptedPwd = md5(PWD_KEY + formValue.password);
+    const loginParams = {
+      ...formValue,
+      password: encryptedPwd, // 替换为加密后的密码
+    }; 
+    userStore
+      .login(loginParams)
+      .then(() => {
+        emit('success');
+      })
+      .catch((err) => {
+        ElMessage.error(err.message || '登录失败');
+      });
+  };
 </script>
 
 <style lang="scss" scoped>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 435 - 0
src/components/Login/components/slider-captcha/Verify.vue


+ 279 - 0
src/components/Login/components/slider-captcha/Verify/VerifyPoints.vue

@@ -0,0 +1,279 @@
+<template>
+  <div style="position: relative">
+    <div class="verify-img-out">
+      <div
+        class="verify-img-panel"
+        :style="{
+          width: setSize.imgWidth,
+          height: setSize.imgHeight,
+          'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+          'margin-bottom': vSpace + 'px',
+        }"
+      >
+        <div class="verify-refresh" style="z-index: 3" @click="refresh" v-show="showRefresh">
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <img
+          :src="'data:image/png;base64,' + pointBackImgBase"
+          ref="canvas"
+          alt=""
+          style="width: 100%; height: 100%; display: block"
+          @click="bindingClick ? canvasClick($event) : undefined"
+        />
+
+        <div
+          v-for="(tempPoint, index) in tempPoints"
+          :key="index"
+          class="point-area"
+          :style="{
+            'background-color': '#1abd6c',
+            color: '#fff',
+            'z-index': 9999,
+            width: '20px',
+            height: '20px',
+            'text-align': 'center',
+            'line-height': '20px',
+            'border-radius': '50%',
+            position: 'absolute',
+            top: parseInt(tempPoint.y - 10) + 'px',
+            left: parseInt(tempPoint.x - 10) + 'px',
+          }"
+        >
+          {{ index + 1 }}
+        </div>
+      </div>
+    </div>
+    <!-- 'height': this.barSize.height, -->
+    <div
+      class="verify-bar-area"
+      :style="{
+        width: setSize.imgWidth,
+        color: this.barAreaColor,
+        'border-color': this.barAreaBorderColor,
+        'line-height': this.barSize.height,
+      }"
+    >
+      <span class="verify-msg">{{ text }}</span>
+    </div>
+  </div>
+</template>
+<script type="text/babel">
+/**
+ * VerifyPoints
+ * @description 点选
+ * */
+import { resetSize, _code_chars, _code_color1, _code_color2 } from '../utils/util';
+import { aesEncrypt } from '../utils/ase';
+import { reqGet, reqCheck } from '../api/index';
+import { computed, onMounted, reactive, ref, watch, nextTick, toRefs, watchEffect, getCurrentInstance } from 'vue';
+export default {
+  name: 'VerifyPoints',
+  props: {
+    //弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: 'fixed',
+    },
+    captchaType: {
+      type: String,
+    },
+    //间隔
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '155px',
+        };
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '40px',
+        };
+      },
+    },
+  },
+  setup(props, context) {
+    const { mode, captchaType, vSpace, imgSize, barSize } = toRefs(props);
+    const { proxy } = getCurrentInstance();
+    let secretKey = ref(''), //后端返回的ase加密秘钥
+      checkNum = ref(3), //默认需要点击的字数
+      fontPos = reactive([]), //选中的坐标信息
+      checkPosArr = reactive([]), //用户点击的坐标
+      num = ref(1), //点击的记数
+      pointBackImgBase = ref(''), //后端获取到的背景图片
+      poinTextList = reactive([]), //后端返回的点击字体顺序
+      backToken = ref(''), //后端返回的token值
+      setSize = reactive({
+        imgHeight: 0,
+        imgWidth: 0,
+        barHeight: 0,
+        barWidth: 0,
+      }),
+      tempPoints = reactive([]),
+      text = ref(''),
+      barAreaColor = ref(undefined),
+      barAreaBorderColor = ref(undefined),
+      showRefresh = ref(true),
+      bindingClick = ref(true);
+
+    const init = () => {
+      //加载页面
+      fontPos.splice(0, fontPos.length);
+      checkPosArr.splice(0, checkPosArr.length);
+      num.value = 1;
+      getPictrue();
+      nextTick(() => {
+        let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy);
+        setSize.imgHeight = imgHeight;
+        setSize.imgWidth = imgWidth;
+        setSize.barHeight = barHeight;
+        setSize.barWidth = barWidth;
+        proxy.$parent.$emit('ready', proxy);
+      });
+    };
+    onMounted(() => {
+      // 禁止拖拽
+      init();
+      proxy.$el.onselectstart = function () {
+        return false;
+      };
+    });
+    const canvas = ref(null);
+    const canvasClick = (e) => {
+      checkPosArr.push(getMousePos(canvas, e));
+      if (num.value == checkNum.value) {
+        num.value = createPoint(getMousePos(canvas, e));
+        //按比例转换坐标值
+        let arr = pointTransfrom(checkPosArr, setSize);
+        checkPosArr.length = 0;
+        checkPosArr.push(...arr);
+        //等创建坐标执行完
+        setTimeout(() => {
+          // var flag = this.comparePos(this.fontPos, this.checkPosArr);
+          //发送后端请求
+          var captchaVerification = secretKey.value
+            ? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value)
+            : backToken.value + '---' + JSON.stringify(checkPosArr);
+          let data = {
+            captchaType: captchaType.value,
+            pointJson: secretKey.value
+              ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
+              : JSON.stringify(checkPosArr),
+            token: backToken.value,
+          };
+          reqCheck(data).then((res) => {
+            if (res.repCode == '0000') {
+              barAreaColor.value = '#4cae4c';
+              barAreaBorderColor.value = '#5cb85c';
+              text.value = '验证成功';
+              bindingClick.value = false;
+              if (mode.value == 'pop') {
+                setTimeout(() => {
+                  proxy.$parent.clickShow = false;
+                  refresh();
+                }, 1500);
+              }
+              proxy.$parent.$emit('success', { captchaVerification });
+            } else {
+              proxy.$parent.$emit('error', proxy);
+              barAreaColor.value = '#d9534f';
+              barAreaBorderColor.value = '#d9534f';
+              text.value = '验证失败';
+              setTimeout(() => {
+                refresh();
+              }, 700);
+            }
+          });
+        }, 400);
+      }
+      if (num.value < checkNum.value) {
+        num.value = createPoint(getMousePos(canvas, e));
+      }
+    };
+    //获取坐标
+    const getMousePos = function (obj, e) {
+      var x = e.offsetX;
+      var y = e.offsetY;
+      return { x, y };
+    };
+    //创建坐标点
+    const createPoint = function (pos) {
+      tempPoints.push(Object.assign({}, pos));
+      return num.value + 1;
+    };
+    const refresh = function () {
+      tempPoints.splice(0, tempPoints.length);
+      barAreaColor.value = '#000';
+      barAreaBorderColor.value = '#ddd';
+      bindingClick.value = true;
+      fontPos.splice(0, fontPos.length);
+      checkPosArr.splice(0, checkPosArr.length);
+      num.value = 1;
+      getPictrue();
+      text.value = '验证失败';
+      showRefresh.value = true;
+    };
+
+    // 请求背景图片和验证图片
+    function getPictrue() {
+      let data = {
+        captchaType: captchaType.value,
+      };
+      reqGet(data).then((res) => {
+        if (res.repCode == '0000') {
+          pointBackImgBase.value = res.repData.originalImageBase64;
+          backToken.value = res.repData.token;
+          secretKey.value = res.repData.secretKey;
+          poinTextList.value = res.repData.wordList;
+          text.value = '请依次点击【' + poinTextList.value.join(',') + '】';
+        } else {
+          text.value = res.repMsg;
+        }
+      });
+    }
+    //坐标转换函数
+    const pointTransfrom = function (pointArr, imgSize) {
+      var newPointArr = pointArr.map((p) => {
+        let x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth));
+        let y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight));
+        return { x, y };
+      });
+      return newPointArr;
+    };
+    return {
+      secretKey,
+      checkNum,
+      fontPos,
+      checkPosArr,
+      num,
+      pointBackImgBase,
+      poinTextList,
+      backToken,
+      setSize,
+      tempPoints,
+      text,
+      barAreaColor,
+      barAreaBorderColor,
+      showRefresh,
+      bindingClick,
+      init,
+      canvas,
+      canvasClick,
+      getMousePos,
+      createPoint,
+      refresh,
+      getPictrue,
+      pointTransfrom,
+    };
+  },
+};
+</script>

+ 409 - 0
src/components/Login/components/slider-captcha/Verify/VerifySlide.vue

@@ -0,0 +1,409 @@
+<template>
+  <div style="position: relative">
+    <div v-if="type === '2'" class="verify-img-out" :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }">
+      <div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
+        <img :src="'data:image/png;base64,' + backImgBase" alt="" style="width: 100%; height: 100%; display: block" />
+        <div class="verify-refresh" @click="refresh" v-show="showRefresh"><i class="iconfont icon-refresh"></i> </div>
+        <transition name="tips">
+          <span class="verify-tips" v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'">{{ tipWords }}</span>
+        </transition>
+      </div>
+    </div>
+    <!-- 公共部分 -->
+    <div
+      class="verify-bar-area"
+      :style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"
+    >
+      <span class="verify-msg" v-text="text"></span>
+      <div
+        class="verify-left-bar"
+        :style="{
+          width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+          height: barSize.height,
+          'border-color': leftBarBorderColor,
+          transition: transitionWidth,
+        }"
+      >
+        <span class="verify-msg" v-text="finishText"></span>
+        <div
+          class="verify-move-block"
+          @touchstart="start"
+          @mousedown="start"
+          :style="{
+            width: barSize.height,
+            height: barSize.height,
+            'background-color': moveBlockBackgroundColor,
+            left: moveBlockLeft,
+            transition: transitionLeft,
+          }"
+        >
+          <i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
+          <div
+            v-if="type === '2'"
+            class="verify-sub-block"
+            :style="{
+              width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
+              height: setSize.imgHeight,
+              top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
+              'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+            }"
+          >
+            <img
+              :src="'data:image/png;base64,' + blockBackImgBase"
+              alt=""
+              style="width: 100%; height: 100%; display: block; -webkit-user-drag: none"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script type="text/babel">
+/**
+ * VerifySlide
+ * @description 滑块
+ * */
+import { aesEncrypt } from '../utils/ase';
+import { resetSize } from '../utils/util';
+import { reqGet, reqCheck } from '../api/index';
+import { computed, onMounted, reactive, ref, watch, nextTick, toRefs, watchEffect, getCurrentInstance } from 'vue';
+//  "captchaType":"blockPuzzle",
+export default {
+  name: 'VerifySlide',
+  props: {
+    captchaType: {
+      type: String,
+    },
+    type: {
+      type: String,
+      default: '1',
+    },
+    //弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: 'fixed',
+    },
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    explain: {
+      type: String,
+      default: '向右滑动完成验证',
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '155px',
+        };
+      },
+    },
+    blockSize: {
+      type: Object,
+      default() {
+        return {
+          width: '50px',
+          height: '50px',
+        };
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '40px',
+        };
+      },
+    },
+  },
+  setup(props, context) {
+    const { mode, captchaType, vSpace, imgSize, barSize, type, blockSize, explain } = toRefs(props);
+    const { proxy } = getCurrentInstance();
+    let secretKey = ref(''), //后端返回的ase加密秘钥
+      passFlag = ref(''), //是否通过的标识
+      backImgBase = ref(''), //验证码背景图片
+      blockBackImgBase = ref(''), //验证滑块的背景图片
+      backToken = ref(''), //后端返回的唯一token值
+      startMoveTime = ref(''), //移动开始的时间
+      endMovetime = ref(''), //移动结束的时间
+      tipsBackColor = ref(''), //提示词的背景颜色
+      tipWords = ref(''),
+      text = ref(''),
+      finishText = ref(''),
+      setSize = reactive({
+        imgHeight: 0,
+        imgWidth: 0,
+        barHeight: 0,
+        barWidth: 0,
+      }),
+      top = ref(0),
+      left = ref(0),
+      moveBlockLeft = ref(undefined),
+      leftBarWidth = ref(undefined),
+      // 移动中样式
+      moveBlockBackgroundColor = ref(undefined),
+      leftBarBorderColor = ref('#ddd'),
+      iconColor = ref(undefined),
+      iconClass = ref('icon-right'),
+      status = ref(false), //鼠标状态
+      isEnd = ref(false), //是够验证完成
+      showRefresh = ref(true),
+      transitionLeft = ref(''),
+      transitionWidth = ref(''),
+      startLeft = ref(0);
+
+    const barArea = computed(() => {
+      return proxy.$el.querySelector('.verify-bar-area');
+    });
+    function init() {
+      text.value = explain.value;
+      getPictrue();
+      nextTick(() => {
+        let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy);
+        setSize.imgHeight = imgHeight;
+        setSize.imgWidth = imgWidth;
+        setSize.barHeight = barHeight;
+        setSize.barWidth = barWidth;
+        proxy.$parent.$emit('ready', proxy);
+      });
+
+      window.removeEventListener('touchmove', function (e) {
+        move(e);
+      });
+      window.removeEventListener('mousemove', function (e) {
+        move(e);
+      });
+
+      //鼠标松开
+      window.removeEventListener('touchend', function () {
+        end();
+      });
+      window.removeEventListener('mouseup', function () {
+        end();
+      });
+
+      window.addEventListener('touchmove', function (e) {
+        move(e);
+      });
+      window.addEventListener('mousemove', function (e) {
+        move(e);
+      });
+
+      //鼠标松开
+      window.addEventListener('touchend', function () {
+        end();
+      });
+      window.addEventListener('mouseup', function () {
+        end();
+      });
+    }
+    watch(type, () => {
+      init();
+    });
+    onMounted(() => {
+      // 禁止拖拽
+      init();
+      proxy.$el.onselectstart = function () {
+        return false;
+      };
+    });
+    //鼠标按下
+    function start(e) {
+      e = e || window.event;
+      if (!e.touches) {
+        //兼容PC端
+        var x = e.clientX;
+      } else {
+        //兼容移动端
+        var x = e.touches[0].pageX;
+      }
+      console.log(barArea);
+      startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left);
+      startMoveTime.value = +new Date(); //开始滑动的时间
+      if (isEnd.value == false) {
+        text.value = '';
+        moveBlockBackgroundColor.value = '#337ab7';
+        leftBarBorderColor.value = '#337AB7';
+        iconColor.value = '#fff';
+        e.stopPropagation();
+        status.value = true;
+      }
+    }
+    //鼠标移动
+    function move(e) {
+      e = e || window.event;
+      if (status.value && isEnd.value == false) {
+        if (!e.touches) {
+          //兼容PC端
+          var x = e.clientX;
+        } else {
+          //兼容移动端
+          var x = e.touches[0].pageX;
+        }
+        var bar_area_left = barArea.value.getBoundingClientRect().left;
+        var move_block_left = x - bar_area_left; //小方块相对于父元素的left值
+        if (move_block_left >= barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2) {
+          move_block_left = barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2;
+        }
+        if (move_block_left <= 0) {
+          move_block_left = parseInt(parseInt(blockSize.value.width) / 2);
+        }
+        //拖动后小方块的left值
+        moveBlockLeft.value = move_block_left - startLeft.value + 'px';
+        leftBarWidth.value = move_block_left - startLeft.value + 'px';
+      }
+    }
+
+    //鼠标松开
+    function end() {
+      endMovetime.value = +new Date();
+      //判断是否重合
+      if (status.value && isEnd.value == false) {
+        var moveLeftDistance = parseInt((moveBlockLeft.value || '').replace('px', ''));
+        moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth);
+        let data = {
+          captchaType: captchaType.value,
+          secretKey: secretKey.value,
+          token: backToken.value,
+          pointJson: secretKey.value
+            ? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
+            : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+        };
+        reqCheck(data).then((res) => {
+          if (res.code===200) {
+            moveBlockBackgroundColor.value = '#5cb85c';
+            leftBarBorderColor.value = '#5cb85c';
+            iconColor.value = '#fff';
+            iconClass.value = 'icon-check';
+            showRefresh.value = false;
+            isEnd.value = true;
+            passFlag.value = true;
+            tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s验证成功`;
+
+            const captchaVerification = secretKey.value
+              ? aesEncrypt(backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
+              : backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 });
+
+            // 原来代码
+            // if (mode.value == 'pop') {
+            //   setTimeout(() => {
+            //     proxy.$parent.clickShow = false;
+            //     refresh();
+            //   }, 3000);
+            // }
+
+            if (mode.value == 'pop') {
+              setTimeout(() => {
+                tipWords.value = '';
+                proxy.$parent.closeBox();
+                refresh();
+              }, 3000);
+            }
+
+            // 原来代码
+            // setTimeout(() => {
+            //   tipWords.value = '';
+            //   proxy.$parent.closeBox();
+            //   proxy.$parent.$emit('success', { captchaVerification });
+            // }, 1000);
+
+            proxy.$parent.$emit('success', { captchaVerification });
+          } else {
+            moveBlockBackgroundColor.value = '#d9534f';
+            leftBarBorderColor.value = '#d9534f';
+            iconColor.value = '#fff';
+            iconClass.value = 'icon-close';
+            passFlag.value = false;
+            setTimeout(function () {
+              refresh();
+            }, 1000);
+            proxy.$parent.$emit('error', proxy);
+            tipWords.value = '验证失败';
+            setTimeout(() => {
+              tipWords.value = '';
+            }, 1000);
+          }
+        });
+        status.value = false;
+      }
+    }
+
+    const refresh = () => {
+      showRefresh.value = true;
+      finishText.value = '';
+
+      transitionLeft.value = 'left .3s';
+      moveBlockLeft.value = 0;
+
+      leftBarWidth.value = undefined;
+      transitionWidth.value = 'width .3s';
+
+      leftBarBorderColor.value = '#ddd';
+      moveBlockBackgroundColor.value = '#fff';
+      iconColor.value = '#000';
+      iconClass.value = 'icon-right';
+      isEnd.value = false;
+
+      getPictrue();
+      setTimeout(() => {
+        transitionWidth.value = '';
+        transitionLeft.value = '';
+        text.value = explain.value;
+      }, 300);
+    };
+
+    // 请求背景图片和验证图片
+    function getPictrue() {
+      // let data = {
+      //   captchaType: captchaType.value,
+      // };
+      reqGet().then((res) => {
+        if (res.repCode == '0000') {
+          backImgBase.value = res.repData.originalImageBase64;
+          blockBackImgBase.value = res.repData.jigsawImageBase64;
+          backToken.value = res.repData.token;
+          secretKey.value = res.repData.secretKey;
+        } else {
+          tipWords.value = res.repMsg;
+        }
+      });
+    }
+    return {
+      secretKey, //后端返回的ase加密秘钥
+      passFlag, //是否通过的标识
+      backImgBase, //验证码背景图片
+      blockBackImgBase, //验证滑块的背景图片
+      backToken, //后端返回的唯一token值
+      startMoveTime, //移动开始的时间
+      endMovetime, //移动结束的时间
+      tipsBackColor, //提示词的背景颜色
+      tipWords,
+      text,
+      finishText,
+      setSize,
+      top,
+      left,
+      moveBlockLeft,
+      leftBarWidth,
+      // 移动中样式
+      moveBlockBackgroundColor,
+      leftBarBorderColor,
+      iconColor,
+      iconClass,
+      status, //鼠标状态
+      isEnd, //是够验证完成
+      showRefresh,
+      transitionLeft,
+      transitionWidth,
+      barArea,
+      refresh,
+      start,
+    };
+  },
+};
+</script>
+

+ 27 - 0
src/components/Login/components/slider-captcha/api/index.ts

@@ -0,0 +1,27 @@
+/**
+ * 此处可直接引用自己项目封装好的 axios 配合后端联调
+ */
+import { http } from '@/utils/http/axios';
+import { Captcha } from '../types';
+
+//获取验证图片  以及token
+export function reqGet() {
+  return http.request({
+    url: '/common/sliderCaptcha/get',
+    method: 'GET',
+  });
+}
+
+//滑动或者点选验证
+export function reqCheck(data) {
+  return http.request(
+    {
+      url: '/common/sliderCaptcha/check',
+      method: 'POST',
+      data,
+    },
+    {
+      isTransformResponse: false,
+    },
+  );
+}

+ 12 - 0
src/components/Login/components/slider-captcha/types/index.ts

@@ -0,0 +1,12 @@
+export interface Captcha {
+  repCode: string;
+  repData: {
+    backImageBase: string; // 验证码背景图片,base64
+    blockBackImageBase: string; // 验证滑块的背景图片,base64
+    secretKey: string; // 后端返回的 ASE 加密密钥
+    token: string; // 后端返回的唯一token值
+  };
+  success: boolean;
+  repMsg?: string;
+  repCodeEnum?: string;
+}

+ 11 - 0
src/components/Login/components/slider-captcha/utils/ase.ts

@@ -0,0 +1,11 @@
+import CryptoJS from 'crypto-js';
+/**
+ * @word 要加密的内容
+ * @keyWord String  服务器随机返回的关键字
+ *  */
+export function aesEncrypt(word: string, keyWord = 'XwKsGlMcdPMEhR1B'): string {
+  var key = CryptoJS.enc.Utf8.parse(keyWord);
+  var srcs = CryptoJS.enc.Utf8.parse(word);
+  var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
+  return encrypted.toString();
+}

+ 97 - 0
src/components/Login/components/slider-captcha/utils/util.ts

@@ -0,0 +1,97 @@
+export function resetSize(vm) {
+  var img_width, img_height, bar_width, bar_height; //图片的宽度、高度,移动条的宽度、高度
+
+  var parentWidth = vm.$el.parentNode.offsetWidth || window.innerWidth;
+  var parentHeight = vm.$el.parentNode.offsetHeight || window.innerHeight;
+  if (vm.imgSize.width.indexOf('%') != -1) {
+    img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px';
+  } else {
+    img_width = vm.imgSize.width;
+  }
+
+  if (vm.imgSize.height.indexOf('%') != -1) {
+    img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px';
+  } else {
+    img_height = vm.imgSize.height;
+  }
+
+  if (vm.barSize.width.indexOf('%') != -1) {
+    bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px';
+  } else {
+    bar_width = vm.barSize.width;
+  }
+
+  if (vm.barSize.height.indexOf('%') != -1) {
+    bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px';
+  } else {
+    bar_height = vm.barSize.height;
+  }
+
+  return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height };
+}
+
+export const _code_chars = [
+  1,
+  2,
+  3,
+  4,
+  5,
+  6,
+  7,
+  8,
+  9,
+  'a',
+  'b',
+  'c',
+  'd',
+  'e',
+  'f',
+  'g',
+  'h',
+  'i',
+  'j',
+  'k',
+  'l',
+  'm',
+  'n',
+  'o',
+  'p',
+  'q',
+  'r',
+  's',
+  't',
+  'u',
+  'v',
+  'w',
+  'x',
+  'y',
+  'z',
+  'A',
+  'B',
+  'C',
+  'D',
+  'E',
+  'F',
+  'G',
+  'H',
+  'I',
+  'J',
+  'K',
+  'L',
+  'M',
+  'N',
+  'O',
+  'P',
+  'Q',
+  'R',
+  'S',
+  'T',
+  'U',
+  'V',
+  'W',
+  'X',
+  'Y',
+  'Z',
+];
+export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'];
+export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'];

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 434 - 0
src/components/Login/components/verifition/Verify.vue


+ 260 - 0
src/components/Login/components/verifition/Verify/VerifyPoints.vue

@@ -0,0 +1,260 @@
+<template>
+    <div style="position: relative"
+        >
+        <div class="verify-img-out">
+            <div class="verify-img-panel" :style="{'width': setSize.imgWidth,
+                                                   'height': setSize.imgHeight,
+                                                   'background-size' : setSize.imgWidth + ' '+ setSize.imgHeight,
+                                                   'margin-bottom': vSpace + 'px'}"
+                                                    >
+                <div class="verify-refresh" style="z-index:3" @click="refresh" v-show="showRefresh">
+                    <i class="iconfont icon-refresh"></i>
+                </div>
+                <img :src="'data:image/png;base64,'+pointBackImgBase" 
+                ref="canvas"
+                alt=""  style="width:100%;height:100%;display:block"
+                @click="bindingClick?canvasClick($event):undefined">
+
+                <div v-for="(tempPoint, index) in tempPoints" :key="index" class="point-area"
+                     :style="{
+                        'background-color':'#1abd6c',
+                        color:'#fff',
+                        'z-index':9999,
+                        width:'20px',
+                        height:'20px',
+                        'text-align':'center',
+                        'line-height':'20px',
+                        'border-radius': '50%',
+                        position:'absolute',
+                        top:parseInt(tempPoint.y-10) + 'px',
+                        left:parseInt(tempPoint.x-10) + 'px'
+                     }">
+                    {{index + 1}}
+                </div>
+            </div>
+        </div>
+        <!-- 'height': this.barSize.height, -->
+        <div class="verify-bar-area"
+             :style="{'width': setSize.imgWidth,
+                      'color': this.barAreaColor,
+                      'border-color': this.barAreaBorderColor,
+                      'line-height':this.barSize.height}">
+            <span class="verify-msg">{{text}}</span>
+        </div>
+    </div>
+</template>
+<script type="text/babel">
+    /**
+     * VerifyPoints
+     * @description 点选
+     * */
+    import {resetSize, _code_chars, _code_color1, _code_color2} from './../utils/util'
+    import {aesEncrypt} from "./../utils/ase"
+    import {reqGet,reqCheck}  from "./../api/index"
+    import { computed, onMounted, reactive, ref,watch,nextTick,toRefs, watchEffect,getCurrentInstance} from 'vue';
+    export default {
+        name: 'VerifyPoints',
+        props: {
+            //弹出式pop,固定fixed
+            mode: {
+                type: String,
+                default: 'fixed'
+            },
+            captchaType:{
+                type:String,
+            },
+            //间隔
+            vSpace: {
+                type: Number,
+                default: 5
+            },
+            imgSize: {
+                type: Object,
+                default() {
+                    return {
+                        width: '310px',
+                        height: '155px'
+                    }
+                }
+            },
+            barSize: {
+                type: Object,
+                default() {
+                    return {
+                        width: '310px',
+                        height: '40px'
+                    }
+                }
+            }
+        },
+        setup(props,context){
+            const {mode,captchaType,vSpace,imgSize,barSize} = toRefs(props)
+            const { proxy } = getCurrentInstance();
+            let secretKey = ref(''),           //后端返回的ase加密秘钥
+                checkNum = ref(3),             //默认需要点击的字数
+                fontPos = reactive([]),            //选中的坐标信息
+                checkPosArr = reactive([]),        //用户点击的坐标
+                num = ref(1),                 //点击的记数
+                pointBackImgBase = ref(''),    //后端获取到的背景图片
+                poinTextList = reactive([]),        //后端返回的点击字体顺序
+                backToken = ref(''),           //后端返回的token值
+                setSize = reactive({
+                    imgHeight: 0,
+                    imgWidth: 0,
+                    barHeight: 0,
+                    barWidth: 0
+                }),
+                tempPoints = reactive([]),
+                text = ref(''),
+                barAreaColor = ref(undefined),
+                barAreaBorderColor = ref(undefined),
+                showRefresh = ref(true),
+                bindingClick = ref(true)
+
+                
+               
+
+                const init = ()=>{
+                    //加载页面
+                    fontPos.splice(0, fontPos.length)
+                    checkPosArr.splice(0, checkPosArr.length)
+                    num.value = 1
+                    getPictrue();
+                    nextTick(() => {
+                        let {imgHeight,imgWidth,barHeight,barWidth} = resetSize(proxy)
+                        setSize.imgHeight = imgHeight
+                        setSize.imgWidth = imgWidth
+                        setSize.barHeight = barHeight
+                        setSize.barWidth = barWidth
+                        proxy.$parent.$emit('ready', proxy)
+                    })
+                }
+                 onMounted(()=>{
+                    // 禁止拖拽
+                    init()
+                    proxy.$el.onselectstart = function () {
+                        return false
+                    }
+                })
+                const canvas = ref(null)
+                const canvasClick = (e)=>{
+                    checkPosArr.push(getMousePos(canvas, e));
+                    if (num.value == checkNum.value) {
+                        num.value = createPoint(getMousePos(canvas, e));
+                        //按比例转换坐标值
+                        let arr = pointTransfrom(checkPosArr,setSize)
+                        checkPosArr.length = 0
+                        checkPosArr.push(...arr);
+                        //等创建坐标执行完
+                        setTimeout(() => {
+                            // var flag = this.comparePos(this.fontPos, this.checkPosArr);
+                            //发送后端请求
+                            var captchaVerification = secretKey.value? aesEncrypt(backToken.value+'---'+JSON.stringify(checkPosArr),secretKey.value):backToken.value+'---'+JSON.stringify(checkPosArr)
+                            let data = {
+                                captchaType:captchaType.value,
+                                "pointJson":secretKey.value? aesEncrypt(JSON.stringify(checkPosArr),secretKey.value):JSON.stringify(checkPosArr),
+                                "token":backToken.value
+                            }
+                            reqCheck(data).then(res=>{
+                                if (res.repCode == "0000") {
+                                    barAreaColor.value = '#4cae4c'
+                                    barAreaBorderColor.value = '#5cb85c'
+                                    text.value = '验证成功'
+                                    bindingClick.value = false
+                                    if (mode.value=='pop') {
+                                        setTimeout(()=>{
+                                            proxy.$parent.clickShow = false;
+                                            refresh();
+                                        },1500)
+                                    }
+                                    proxy.$parent.$emit('success', {captchaVerification})
+                                }else{
+                                    proxy.$parent.$emit('error', proxy)
+                                    barAreaColor.value = '#d9534f'
+                                    barAreaBorderColor.value = '#d9534f'
+                                    text.value = '验证失败'
+                                    setTimeout(() => {
+                                        refresh();
+                                    }, 700);
+                                }
+                            })
+                        }, 400);
+                    }
+                    if (num.value < checkNum.value) {
+                        num.value = createPoint(getMousePos(canvas, e));
+                    }
+                }
+                 //获取坐标
+                const getMousePos = function (obj, e) {
+                    var x = e.offsetX 
+                    var y = e.offsetY 
+                    return {x, y}
+                }
+                //创建坐标点
+                const createPoint = function (pos) {
+                    tempPoints.push(Object.assign({}, pos))
+                    return num.value+1;
+                }
+                const refresh = function () {
+                    tempPoints.splice(0, tempPoints.length)
+                    barAreaColor.value = '#000'
+                    barAreaBorderColor.value = '#ddd'
+                    bindingClick.value = true
+                    fontPos.splice(0, fontPos.length)
+                    checkPosArr.splice(0, checkPosArr.length)
+                    num.value = 1
+                    getPictrue();
+                    text.value = '验证失败'
+                    showRefresh.value = true
+                }
+
+                // 请求背景图片和验证图片
+                function getPictrue() {
+                    let data = {
+                        captchaType:captchaType.value
+                    }
+                    reqGet(data).then(res=>{
+                        if (res.repCode == "0000") {
+                            pointBackImgBase.value = res.repData.originalImageBase64
+                            backToken.value = res.repData.token
+                            secretKey.value = res.repData.secretKey
+                            poinTextList.value = res.repData.wordList
+                            text.value = '请依次点击【' + poinTextList.value.join(",") + '】'
+                        }else{
+                            text.value = res.repMsg;
+                        }
+                    })
+                }
+                //坐标转换函数
+                const pointTransfrom = function(pointArr,imgSize){
+                    var newPointArr = pointArr.map(p=>{
+                        let x = Math.round(310 * p.x/parseInt(imgSize.imgWidth)) 
+                        let y =Math.round(155 * p.y/parseInt(imgSize.imgHeight)) 
+                        return {x,y}
+                    })
+                    return newPointArr
+                }
+                return {
+                    secretKey,
+                    checkNum,
+                    fontPos,
+                    checkPosArr,
+                    num,
+                    pointBackImgBase,
+                    poinTextList,
+                    backToken,
+                    setSize,
+                    tempPoints,
+                    text,
+                    barAreaColor,
+                    barAreaBorderColor,
+                    showRefresh,
+                    bindingClick,
+                    init,
+                    canvas,
+                    canvasClick,
+                    getMousePos,createPoint,refresh,getPictrue,pointTransfrom
+                }
+        },
+    }
+</script>

+ 467 - 0
src/components/Login/components/verifition/Verify/VerifySlide.vue

@@ -0,0 +1,467 @@
+<template>
+  <div style="position: relative">
+    <div
+      v-if="type === '2'"
+      class="verify-img-out"
+      :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"
+    >
+      <div
+        class="verify-img-panel"
+        :style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
+      >
+        <img
+          :src="'data:image/png;base64,' + backImgBase"
+          alt=""
+          style="width: 100%; height: 100%; display: block"
+        />
+        <div class="verify-refresh" @click="refresh" v-show="showRefresh">
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <transition name="tips">
+          <span
+            class="verify-tips"
+            v-if="tipWords"
+            :class="passFlag ? 'suc-bg' : 'err-bg'"
+            >{{ tipWords }}</span
+          >
+        </transition>
+      </div>
+    </div>
+    <!-- 公共部分 -->
+    <div
+      class="verify-bar-area"
+      :style="{
+        width: setSize.imgWidth,
+        height: barSize.height,
+        'line-height': barSize.height,
+      }"
+    >
+      <span class="verify-msg" v-text="text"></span>
+      <div
+        class="verify-left-bar"
+        :style="{
+          width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+          height: barSize.height,
+          'border-color': leftBarBorderColor,
+          transaction: transitionWidth,
+        }"
+      >
+        <span class="verify-msg" v-text="finishText"></span>
+        <div
+          class="verify-move-block"
+          @touchstart="start"
+          @mousedown="start"
+          :style="{
+            width: barSize.height,
+            height: barSize.height,
+            'background-color': moveBlockBackgroundColor,
+            left: moveBlockLeft,
+            transition: transitionLeft,
+          }"
+        >
+          <i
+            :class="['verify-icon iconfont', iconClass]"
+            :style="{ color: iconColor }"
+          ></i>
+          <div
+            v-if="type === '2'"
+            class="verify-sub-block"
+            :style="{
+              width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
+              height: setSize.imgHeight,
+              top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
+              'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+            }"
+          >
+            <img
+              :src="'data:image/png;base64,' + blockBackImgBase"
+              alt=""
+              style="
+                width: 100%;
+                height: 100%;
+                display: block;
+                -webkit-user-drag: none;
+              "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script type="text/babel">
+/**
+ * VerifySlide
+ * @description 滑块
+ * */
+import { aesEncrypt } from "./../utils/ase";
+import { resetSize } from "./../utils/util";
+import { reqGet, reqCheck } from "./../api/index";
+import {
+  computed,
+  onMounted,
+  reactive,
+  ref,
+  watch,
+  nextTick,
+  toRefs,
+  watchEffect,
+  getCurrentInstance,
+} from "vue";
+//  "captchaType":"blockPuzzle",
+export default {
+  name: "VerifySlide",
+  props: {
+    captchaType: {
+      type: String,
+    },
+    type: {
+      type: String,
+      default: "1",
+    },
+    //弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: "fixed",
+    },
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    explain: {
+      type: String,
+      default: "向右滑动完成验证",
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: "310px",
+          height: "155px",
+        };
+      },
+    },
+    blockSize: {
+      type: Object,
+      default() {
+        return {
+          width: "50px",
+          height: "50px",
+        };
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: "310px",
+          height: "40px",
+        };
+      },
+    },
+  },
+  setup(props, context) {
+    const {
+      mode,
+      captchaType,
+      vSpace,
+      imgSize,
+      barSize,
+      type,
+      blockSize,
+      explain,
+    } = toRefs(props);
+    const { proxy } = getCurrentInstance();
+    let secretKey = ref(""), //后端返回的ase加密秘钥
+      passFlag = ref(""), //是否通过的标识
+      backImgBase = ref(""), //验证码背景图片
+      blockBackImgBase = ref(""), //验证滑块的背景图片
+      backToken = ref(""), //后端返回的唯一token值
+      startMoveTime = ref(""), //移动开始的时间
+      endMovetime = ref(""), //移动结束的时间
+      tipsBackColor = ref(""), //提示词的背景颜色
+      tipWords = ref(""),
+      text = ref(""),
+      finishText = ref(""),
+      setSize = reactive({
+        imgHeight: 0,
+        imgWidth: 0,
+        barHeight: 0,
+        barWidth: 0,
+      }),
+      top = ref(0),
+      left = ref(0),
+      moveBlockLeft = ref(undefined),
+      leftBarWidth = ref(undefined),
+      // 移动中样式
+      moveBlockBackgroundColor = ref(undefined),
+      leftBarBorderColor = ref("#ddd"),
+      iconColor = ref(undefined),
+      iconClass = ref("icon-right"),
+      status = ref(false), //鼠标状态
+      isEnd = ref(false), //是够验证完成
+      showRefresh = ref(true),
+      transitionLeft = ref(""),
+      transitionWidth = ref(""),
+      startLeft = ref(0);
+
+    const barArea = computed(() => {
+      return proxy.$el.querySelector(".verify-bar-area");
+    });
+    function init() {
+      text.value = explain.value;
+      getPictrue();
+      nextTick(() => {
+        let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy);
+        setSize.imgHeight = imgHeight;
+        setSize.imgWidth = imgWidth;
+        setSize.barHeight = barHeight;
+        setSize.barWidth = barWidth;
+        proxy.$parent.$emit("ready", proxy);
+      });
+
+      window.removeEventListener("touchmove", function (e) {
+        move(e);
+      });
+      window.removeEventListener("mousemove", function (e) {
+        move(e);
+      });
+
+      //鼠标松开
+      window.removeEventListener("touchend", function () {
+        end();
+      });
+      window.removeEventListener("mouseup", function () {
+        end();
+      });
+
+      window.addEventListener("touchmove", function (e) {
+        move(e);
+      });
+      window.addEventListener("mousemove", function (e) {
+        move(e);
+      });
+
+      //鼠标松开
+      window.addEventListener("touchend", function () {
+        end();
+      });
+      window.addEventListener("mouseup", function () {
+        end();
+      });
+    }
+    watch(type, () => {
+      init();
+    });
+    onMounted(() => {
+      // 禁止拖拽
+      init();
+      proxy.$el.onselectstart = function () {
+        return false;
+      };
+    });
+    //鼠标按下
+    function start(e) {
+      e = e || window.event;
+      if (!e.touches) {
+        //兼容PC端
+        var x = e.clientX;
+      } else {
+        //兼容移动端
+        var x = e.touches[0].pageX;
+      }
+      console.log(barArea);
+      startLeft.value = Math.floor(
+        x - barArea.value.getBoundingClientRect().left
+      );
+      startMoveTime.value = +new Date(); //开始滑动的时间
+      if (isEnd.value == false) {
+        text.value = "";
+        moveBlockBackgroundColor.value = "#337ab7";
+        leftBarBorderColor.value = "#337AB7";
+        iconColor.value = "#fff";
+        e.stopPropagation();
+        status.value = true;
+      }
+    }
+    //鼠标移动
+    function move(e) {
+      e = e || window.event;
+      if (status.value && isEnd.value == false) {
+        if (!e.touches) {
+          //兼容PC端
+          var x = e.clientX;
+        } else {
+          //兼容移动端
+          var x = e.touches[0].pageX;
+        }
+        var bar_area_left = barArea.value.getBoundingClientRect().left;
+        var move_block_left = x - bar_area_left; //小方块相对于父元素的left值
+        if (
+          move_block_left >=
+          barArea.value.offsetWidth -
+            parseInt(parseInt(blockSize.value.width) / 2) -
+            2
+        ) {
+          move_block_left =
+            barArea.value.offsetWidth -
+            parseInt(parseInt(blockSize.value.width) / 2) -
+            2;
+        }
+        if (move_block_left <= 0) {
+          move_block_left = parseInt(parseInt(blockSize.value.width) / 2);
+        }
+        //拖动后小方块的left值
+        moveBlockLeft.value = move_block_left - startLeft.value + "px";
+        leftBarWidth.value = move_block_left - startLeft.value + "px";
+      }
+    }
+
+    //鼠标松开
+    function end() {
+      endMovetime.value = +new Date();
+      //判断是否重合
+      if (status.value && isEnd.value == false) {
+        var moveLeftDistance = parseInt(
+          (moveBlockLeft.value || "").replace("px", "")
+        );
+        moveLeftDistance =
+          (moveLeftDistance * 310) / parseInt(setSize.imgWidth);
+        let data = {
+          captchaType: captchaType.value,
+          pointJson: secretKey.value
+            ? aesEncrypt(
+                JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+                secretKey.value
+              )
+            : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+          token: backToken.value,
+        };
+        reqCheck(data).then((res) => {
+          if (res.repCode == "0000") {
+            moveBlockBackgroundColor.value = "#5cb85c";
+            leftBarBorderColor.value = "#5cb85c";
+            iconColor.value = "#fff";
+            iconClass.value = "icon-check";
+            showRefresh.value = false;
+            isEnd.value = true;
+            if (mode.value == "pop") {
+              setTimeout(() => {
+                proxy.$parent.clickShow = false;
+                refresh();
+              }, 1500);
+            }
+            passFlag.value = true;
+            tipWords.value = `${(
+              (endMovetime.value - startMoveTime.value) /
+              1000
+            ).toFixed(2)}s验证成功`;
+            var captchaVerification = secretKey.value
+              ? aesEncrypt(
+                  backToken.value +
+                    "---" +
+                    JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+                  secretKey.value
+                )
+              : backToken.value +
+                "---" +
+                JSON.stringify({ x: moveLeftDistance, y: 5.0 });
+            setTimeout(() => {
+              tipWords.value = "";
+              proxy.$parent.closeBox();
+              proxy.$parent.$emit("success", { captchaVerification });
+            }, 1000);
+          } else {
+            moveBlockBackgroundColor.value = "#d9534f";
+            leftBarBorderColor.value = "#d9534f";
+            iconColor.value = "#fff";
+            iconClass.value = "icon-close";
+            passFlag.value = false;
+            setTimeout(function () {
+              refresh();
+            }, 1000);
+            proxy.$parent.$emit("error", proxy);
+            tipWords.value = "验证失败";
+            setTimeout(() => {
+              tipWords.value = "";
+            }, 1000);
+          }
+        });
+        status.value = false;
+      }
+    }
+
+    const refresh = () => {
+      showRefresh.value = true;
+      finishText.value = "";
+
+      transitionLeft.value = "left .3s";
+      moveBlockLeft.value = 0;
+
+      leftBarWidth.value = undefined;
+      transitionWidth.value = "width .3s";
+
+      leftBarBorderColor.value = "#ddd";
+      moveBlockBackgroundColor.value = "#fff";
+      iconColor.value = "#000";
+      iconClass.value = "icon-right";
+      isEnd.value = false;
+
+      getPictrue();
+      setTimeout(() => {
+        transitionWidth.value = "";
+        transitionLeft.value = "";
+        text.value = explain.value;
+      }, 300);
+    };
+
+    // 请求背景图片和验证图片
+    function getPictrue() {
+      let data = {
+        captchaType: captchaType.value,
+      };
+      reqGet(data).then((res) => {
+        if (res.repCode == "0000") {
+          backImgBase.value = res.repData.originalImageBase64;
+          blockBackImgBase.value = res.repData.jigsawImageBase64;
+          backToken.value = res.repData.token;
+          secretKey.value = res.repData.secretKey;
+        } else {
+          tipWords.value = res.repMsg;
+        }
+      });
+    }
+    return {
+      secretKey, //后端返回的ase加密秘钥
+      passFlag, //是否通过的标识
+      backImgBase, //验证码背景图片
+      blockBackImgBase, //验证滑块的背景图片
+      backToken, //后端返回的唯一token值
+      startMoveTime, //移动开始的时间
+      endMovetime, //移动结束的时间
+      tipsBackColor, //提示词的背景颜色
+      tipWords,
+      text,
+      finishText,
+      setSize,
+      top,
+      left,
+      moveBlockLeft,
+      leftBarWidth,
+      // 移动中样式
+      moveBlockBackgroundColor,
+      leftBarBorderColor,
+      iconColor,
+      iconClass,
+      status, //鼠标状态
+      isEnd, //是够验证完成
+      showRefresh,
+      transitionLeft,
+      transitionWidth,
+      barArea,
+      refresh,
+      start,
+    };
+  },
+};
+</script>

+ 27 - 0
src/components/Login/components/verifition/api/index.js

@@ -0,0 +1,27 @@
+/**
+ * 此处可直接引用自己项目封装好的 axios 配合后端联调
+ */
+
+
+import request from "./../utils/axios"  //组件内部封装的axios
+// import request from "@/api/axios.js"       //调用项目封装的axios
+
+//获取验证图片  以及token
+export function reqGet(data) {
+	return  request({
+        url: '/captcha/get',
+        method: 'post',
+        data
+    })
+}
+
+//滑动或者点选验证
+export function reqCheck(data) {
+	return  request({
+        url: '/captcha/check',
+        method: 'post',
+        data
+    })
+}
+
+

+ 11 - 0
src/components/Login/components/verifition/utils/ase.js

@@ -0,0 +1,11 @@
+import CryptoJS from 'crypto-js'
+/**
+ * @word 要加密的内容
+ * @keyWord String  服务器随机返回的关键字
+ *  */
+export function aesEncrypt(word,keyWord="XwKsGlMcdPMEhR1B"){
+  var key = CryptoJS.enc.Utf8.parse(keyWord);
+  var srcs = CryptoJS.enc.Utf8.parse(word);
+  var encrypted = CryptoJS.AES.encrypt(srcs, key, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
+  return encrypted.toString();
+}

+ 30 - 0
src/components/Login/components/verifition/utils/axios.js

@@ -0,0 +1,30 @@
+import axios from 'axios';
+
+axios.defaults.baseURL = 'http://192.168.22.121';
+
+const service = axios.create({
+  timeout: 40000,
+  headers: {
+    'X-Requested-With': 'XMLHttpRequest',
+    'Content-Type': 'application/json; charset=UTF-8'
+  },
+})
+service.interceptors.request.use(
+  config => {
+    return config
+  },
+  error => {
+    Promise.reject(error)
+  }
+)
+
+// response interceptor
+service.interceptors.response.use(
+  response => {
+    const res = response.data;
+    return res
+  },
+  error => {
+  }
+)
+export default service

+ 35 - 0
src/components/Login/components/verifition/utils/util.js

@@ -0,0 +1,35 @@
+export function resetSize(vm) {
+    var img_width, img_height, bar_width, bar_height;	//图片的宽度、高度,移动条的宽度、高度
+
+    var parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
+    var parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
+    if (vm.imgSize.width.indexOf('%') != -1) {
+        img_width = parseInt(vm.imgSize.width) / 100 * parentWidth + 'px'
+    } else {
+        img_width = vm.imgSize.width;
+    }
+
+    if (vm.imgSize.height.indexOf('%') != -1) {
+        img_height = parseInt(vm.imgSize.height) / 100 * parentHeight + 'px'
+    } else {
+        img_height = vm.imgSize.height
+    }
+
+    if (vm.barSize.width.indexOf('%') != -1) {
+        bar_width = parseInt(vm.barSize.width) / 100 * parentWidth + 'px'
+    } else {
+        bar_width = vm.barSize.width
+    }
+
+    if (vm.barSize.height.indexOf('%') != -1) {
+        bar_height = parseInt(vm.barSize.height) / 100 * parentHeight + 'px'
+    } else {
+        bar_height = vm.barSize.height
+    }
+
+    return {imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height}
+}
+
+export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
+export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
+export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']

+ 509 - 0
src/components/Login/components/verify/VerifySlide.vue

@@ -0,0 +1,509 @@
+<template>
+  <div style="position: relative">
+    <div v-if="type === '2'" class="verify-img-out" :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }">
+      <div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
+        <img
+          :src="'data:image/png;base64,' + backImgBase"
+          alt="验证背景图"
+          style="width: 100%; height: 100%; display: block"
+        />
+        <div class="verify-refresh" @click="refresh" v-show="showRefresh">
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <transition name="tips">
+          <span class="verify-tips" v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'">{{ tipWords }}</span>
+        </transition>
+      </div>
+    </div>
+    <!-- 公共部分 -->
+    <div
+      class="verify-bar-area"
+      :style="{
+        width: setSize.imgWidth,
+        height: barSize.height,
+        'line-height': barSize.height,
+      }"
+    >
+      <span class="verify-msg" v-text="text"></span>
+      <div
+        class="verify-left-bar"
+        :style="{
+          width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+          height: barSize.height,
+          'border-color': leftBarBorderColor,
+          transition: transitionWidth, // 原单词拼写错误:transaction -> transition
+        }"
+      >
+        <span class="verify-msg" v-text="finishText"></span>
+        <div
+          class="verify-move-block"
+          @touchstart="start"
+          @mousedown="start"
+          :style="{
+            width: barSize.height,
+            height: barSize.height,
+            'background-color': moveBlockBackgroundColor,
+            left: moveBlockLeft,
+            transition: transitionLeft,
+          }"
+        >
+          <i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
+          <div
+            v-if="type === '2'"
+            class="verify-sub-block"
+            :style="{
+              width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
+              height: setSize.imgHeight,
+              top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
+              'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+            }"
+          >
+            <img
+              :src="'data:image/png;base64,' + blockBackImgBase"
+              alt="滑块背景图"
+              style="width: 100%; height: 100%; display: block; -webkit-user-drag: none"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue';
+  import { aesEncrypt } from './utils/aes';
+  import { resetSize } from './utils/util';
+  import { reqGet, reqCheck } from './utils/api';
+
+  // -------------------------- 1. 定义 Props 类型及默认值 --------------------------
+  interface ImgSize {
+    width: string;
+    height: string;
+  }
+
+  interface BlockSize {
+    width: string;
+    height: string;
+  }
+
+  interface BarSize {
+    width: string;
+    height: string;
+  }
+
+  const props = withDefaults(
+    defineProps<{
+      captchaType: string;
+      type: string;
+      mode: string;
+      vSpace: number;
+      explain: string;
+      imgSize: ImgSize;
+      blockSize: BlockSize;
+      barSize: BarSize;
+    }>(),
+    {
+      type: '1',
+      mode: 'fixed',
+      vSpace: 5,
+      explain: '向右滑动完成验证',
+      imgSize: () => ({
+        width: '360px',
+        height: '180px',
+      }),
+      blockSize: () => ({
+        width: '50px',
+        height: '50px',
+      }),
+      barSize: () => ({
+        width: '310px',
+        height: '40px',
+      }),
+    },
+  );
+
+  // -------------------------- 2. 定义 Emits 事件 --------------------------
+  const emit = defineEmits<{
+    (e: 'success', data: { captchaVerification: string }): void;
+    (e: 'error', instance: HTMLElement): void;
+  }>();
+
+  // -------------------------- 3. 响应式数据定义 --------------------------
+  // 后端返回相关
+  const secretKey = ref(''); // ase加密秘钥
+  const backImgBase = ref(''); // 验证码背景图片base64
+  const blockBackImgBase = ref(''); // 滑块背景图片base64
+  const backToken = ref(''); // 唯一token
+  // 验证状态相关
+  const passFlag = ref(false); // 是否验证通过
+  const isEnd = ref(false); // 验证是否完成
+  const status = ref(false); // 鼠标/触摸是否按下
+  const showRefresh = ref(true); // 是否显示刷新按钮
+  // 时间相关
+  const startMoveTime = ref(0); // 移动开始时间
+  const endMovetime = ref(0); // 移动结束时间
+  // 文本相关
+  const tipWords = ref(''); // 提示文字
+  const text = ref(props.explain); // 滑动提示文字
+  const finishText = ref(''); // 完成提示文字
+  // 样式相关
+  const setSize = reactive({
+    imgHeight: 0,
+    imgWidth: 0,
+    barHeight: 0,
+    barWidth: 0,
+  });
+  const moveBlockLeft = ref<string | undefined>(undefined); // 滑块左侧距离
+  const leftBarWidth = ref<string | undefined>(undefined); // 左侧进度条宽度
+  const moveBlockBackgroundColor = ref<string | undefined>(undefined); // 滑块背景色
+  const leftBarBorderColor = ref('#ddd'); // 左侧进度条边框色
+  const iconColor = ref<string | undefined>(undefined); // 图标颜色
+  const iconClass = ref('icon-right'); // 图标类名
+  const transitionLeft = ref(''); // 滑块过渡样式
+  const transitionWidth = ref(''); // 进度条过渡样式
+  const startLeft = ref(0); // 滑动起始偏移量
+
+  // -------------------------- 4. 计算属性 --------------------------
+  // 获取滑块容器元素
+  const barArea = computed(() => {
+    const el = document.querySelector('.verify-bar-area');
+    return el as HTMLElement | null;
+  });
+
+  // -------------------------- 5. 核心方法 --------------------------
+  /**
+   * 初始化组件
+   */
+  const init = () => {
+    text.value = props.explain;
+    getPictrue(); // 请求验证码图片
+
+    nextTick(() => {
+      // @ts-ignore 兼容原 resetSize 方法(需确保 resetSize 支持传入组件实例)
+      const sizeInfo = resetSize({ $el: document.querySelector('.verify-img-out')?.parentElement });
+      if (sizeInfo) {
+        setSize.imgHeight = sizeInfo.imgHeight;
+        setSize.imgWidth = sizeInfo.imgWidth;
+        setSize.barHeight = sizeInfo.barHeight;
+        setSize.barWidth = sizeInfo.barWidth;
+      }
+      // 移除原有事件监听(防止重复绑定)
+      window.removeEventListener('touchmove', handleMove);
+      window.removeEventListener('mousemove', handleMove);
+      window.removeEventListener('touchend', handleEnd);
+      window.removeEventListener('mouseup', handleEnd);
+      // 重新绑定事件
+      window.addEventListener('touchmove', handleMove);
+      window.addEventListener('mousemove', handleMove);
+      window.addEventListener('touchend', handleEnd);
+      window.addEventListener('mouseup', handleEnd);
+    });
+
+    // 禁止元素选中
+    const rootEl = document.querySelector('.verify-img-out')?.parentElement;
+    if (rootEl) {
+      rootEl.onselectstart = () => false;
+    }
+  };
+
+  /**
+   * 鼠标/触摸按下事件(开始滑动)
+   */
+  const start = (e: MouseEvent | TouchEvent) => {
+    e = e || window.event;
+    let x = 0;
+
+    // 兼容PC端和移动端
+    if ('touches' in e && e.touches.length > 0) {
+      x = e.touches[0].pageX; // 移动端
+    } else {
+      x = (e as MouseEvent).clientX; // PC端
+    }
+
+    // 修正:计算鼠标按下点相对滑块左侧的偏移(而非拖动条)
+    // 先获取滑块当前位置(未滑动时为0)
+    const currentLeft = moveBlockLeft.value ? parseInt(moveBlockLeft.value) : 0;
+    startLeft.value = x - (barArea.value?.getBoundingClientRect().left || 0) - currentLeft;
+
+    startMoveTime.value = +new Date(); // 记录开始时间
+
+    if (!isEnd.value) {
+      text.value = '';
+      moveBlockBackgroundColor.value = '#337ab7';
+      leftBarBorderColor.value = '#337AB7';
+      iconColor.value = '#fff';
+      e.stopPropagation();
+      e.preventDefault(); // 新增:禁止默认行为(避免滑动时选中文本)
+      status.value = true;
+    }
+  };
+
+  /**
+   * 鼠标/触摸移动事件(滑动中)
+   */
+  const handleMove = (e: MouseEvent | TouchEvent) => {
+    e = e || window.event;
+    if (!status.value || isEnd.value) return;
+
+    let x = 0;
+    // 兼容PC端和移动端
+    if ('touches' in e && e.touches.length > 0) {
+      x = e.touches[0].pageX; // 移动端
+    } else {
+      x = (e as MouseEvent).clientX; // PC端
+    }
+
+    if (!barArea.value) return;
+    const bar_area_rect = barArea.value.getBoundingClientRect();
+    const bar_area_left = bar_area_rect.left;
+    const bar_width = bar_area_rect.width; // 拖动条实际宽度(适配响应式)
+    const block_width = parseInt(props.blockSize.width); // 滑块宽度
+
+    // 1. 计算滑块绝对左偏移(相对拖动条左侧)
+    let move_block_left = x - bar_area_left - startLeft.value;
+
+    // 2. 正确的边界限制:0 ≤ 偏移 ≤ 拖动条宽度 - 滑块宽度
+    const minLeft = 0; // 最小偏移(滑块左边缘对齐拖动条)
+    const maxLeft = bar_width - block_width; // 最大偏移(滑块右边缘对齐拖动条)
+
+    // 限制偏移在合法范围
+    move_block_left = Math.max(minLeft, Math.min(move_block_left, maxLeft));
+
+    // 3. 更新滑块和进度条位置(直接用绝对偏移,无多余计算)
+    moveBlockLeft.value = move_block_left + 'px';
+    leftBarWidth.value = move_block_left + 'px';
+  };
+
+  /**
+   * 鼠标/触摸松开事件(滑动结束,验证判断)
+   */
+  const handleEnd = () => {
+    endMovetime.value = +new Date();
+    if (!status.value || isEnd.value) return;
+
+    // 计算滑动距离(适配不同尺寸)
+    let moveLeftDistance = 0;
+    if (moveBlockLeft.value) {
+      moveLeftDistance = parseInt(moveBlockLeft.value.replace('px', ''));
+    }
+    moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth);
+
+    // 构造验证参数
+    const checkData = {
+      captchaType: props.captchaType,
+      pointJson: secretKey.value
+        ? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
+        : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+      token: backToken.value,
+    };
+
+    // 发起验证请求
+    reqCheck(checkData).then((res: any) => {
+      if (res.repCode === '0000') {
+        // 验证成功
+        moveBlockBackgroundColor.value = '#5cb85c';
+        leftBarBorderColor.value = '#5cb85c';
+        iconColor.value = '#fff';
+        iconClass.value = 'icon-check';
+        showRefresh.value = false;
+        isEnd.value = true;
+        passFlag.value = true;
+        tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s验证成功`;
+
+        // 构造验证成功返回参数
+        const captchaVerification = secretKey.value
+          ? aesEncrypt(backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
+          : backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 });
+
+        // 弹窗模式下自动关闭并刷新
+        if (props.mode === 'pop') {
+          setTimeout(() => {
+            refresh();
+          }, 1500);
+        }
+
+        // 延迟触发成功事件并清空提示
+        setTimeout(() => {
+          tipWords.value = '';
+          emit('success', { captchaVerification });
+        }, 1000);
+      } else {
+        // 验证失败
+        moveBlockBackgroundColor.value = '#d9534f';
+        leftBarBorderColor.value = '#d9534f';
+        iconColor.value = '#fff';
+        iconClass.value = 'icon-close';
+        passFlag.value = false;
+        tipWords.value = '验证失败';
+        emit('error', document.querySelector('.verify-img-out') as HTMLElement);
+
+        // 延迟刷新组件并清空提示
+        setTimeout(() => {
+          refresh();
+          tipWords.value = '';
+        }, 1000);
+      }
+    });
+
+    status.value = false;
+  };
+
+  /**
+   * 刷新验证码(重置状态+重新请求图片)
+   */
+  const refresh = () => {
+    showRefresh.value = true;
+    finishText.value = '';
+    transitionLeft.value = 'left .3s';
+    moveBlockLeft.value = undefined;
+    leftBarWidth.value = undefined;
+    transitionWidth.value = 'width .3s';
+    leftBarBorderColor.value = '#ddd';
+    moveBlockBackgroundColor.value = '#fff';
+    iconColor.value = '#000';
+    iconClass.value = 'icon-right';
+    isEnd.value = false;
+
+    // 重新请求验证码图片
+    getPictrue();
+
+    // 延迟重置过渡样式和提示文字
+    setTimeout(() => {
+      transitionWidth.value = '';
+      transitionLeft.value = '';
+      text.value = props.explain;
+    }, 300);
+  };
+
+  /**
+   * 请求验证码背景图和滑块图
+   */
+  const getPictrue = () => {
+    const reqData = {
+      captchaType: props.captchaType,
+    };
+
+    reqGet(reqData).then((res: any) => {
+      if (res.repCode === '0000') {
+        backImgBase.value = res.repData.originalImageBase64;
+        blockBackImgBase.value = res.repData.jigsawImageBase64;
+        backToken.value = res.repData.token;
+        secretKey.value = res.repData.secretKey;
+      } else {
+        tipWords.value = res.repMsg || '获取验证码失败';
+      }
+    });
+  };
+
+  // -------------------------- 6. 生命周期 & 监听 --------------------------
+  // 监听 type 变化,重新初始化
+  watch(
+    () => props.type,
+    () => {
+      init();
+    },
+  );
+
+  // 组件挂载后初始化
+  onMounted(() => {
+    init();
+  });
+
+  // -------------------------- 7. 暴露模板使用的变量/方法 --------------------------
+  defineExpose({
+    refresh, // 暴露刷新方法供父组件调用
+  });
+</script>
+
+<style scoped>
+  /* 可保留原有样式,此处仅为占位 */
+  .verify-img-out {
+    position: relative;
+    margin-bottom: 8px;
+  }
+  .verify-img-panel {
+    position: relative;
+    overflow: hidden;
+    border-radius: 4px;
+    background: #f5f5f5;
+  }
+  .verify-refresh {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    width: 30px;
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+    cursor: pointer;
+    background: rgba(255, 255, 255, 0.8);
+    border-radius: 50%;
+    z-index: 10;
+  }
+  .verify-tips {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    padding: 4px 12px;
+    border-radius: 4px;
+    color: #fff;
+    font-size: 14px;
+    z-index: 10;
+  }
+  .suc-bg {
+    background: #5cb85c;
+  }
+  .err-bg {
+    background: #d9534f;
+  }
+  .verify-bar-area {
+    position: relative;
+    background: #f8f8f8;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    overflow: hidden;
+    text-align: center;
+    font-size: 14px;
+    color: #666;
+  }
+  .verify-left-bar {
+    position: absolute;
+    top: 0;
+    left: 0;
+    background: #e6f4ff;
+    border-right: 1px solid #337ab7;
+    box-sizing: border-box;
+    z-index: 5;
+  }
+  .verify-move-block {
+    position: absolute;
+    top: 0;
+    cursor: pointer;
+    background: #fff;
+    border: 1px solid #ddd;
+    box-sizing: border-box;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .verify-icon {
+    font-size: 18px;
+  }
+  .verify-sub-block {
+    position: absolute;
+    left: 0;
+    overflow: hidden;
+    z-index: 10;
+  }
+  .tips-enter-active,
+  .tips-leave-active {
+    transition: opacity 0.3s ease;
+  }
+  .tips-enter-from,
+  .tips-leave-to {
+    opacity: 0;
+  }
+</style>

+ 21 - 0
src/components/Login/components/verify/utils/aes.ts

@@ -0,0 +1,21 @@
+import CryptoJS from 'crypto-js';
+
+/**
+ * AES 加密函数(ECB 模式 + Pkcs7 填充)
+ * @param word 要加密的字符串内容
+ * @param keyWord 加密密钥(默认值:XwKsGlMcdPMEhR1B)
+ * @returns 加密后的 Base64 格式字符串
+ */
+export function aesEncrypt(word: string, keyWord: string = "XwKsGlMcdPMEhR1B"): string {
+  // 把密钥转换为 Utf8 编码的 CryptoJS 数据格式
+  const key = CryptoJS.enc.Utf8.parse(keyWord);
+  // 把待加密内容转换为 Utf8 编码的 CryptoJS 数据格式
+  const srcs = CryptoJS.enc.Utf8.parse(word);
+  // 执行 AES 加密(ECB 模式,Pkcs7 填充)
+  const encrypted = CryptoJS.AES.encrypt(srcs, key, {
+    mode: CryptoJS.mode.ECB,
+    padding: CryptoJS.pad.Pkcs7
+  });
+  // 返回加密后的字符串(默认输出 Base64 格式)
+  return encrypted.toString();
+}

+ 41 - 0
src/components/Login/components/verify/utils/api.ts

@@ -0,0 +1,41 @@
+/**
+ * 此处可直接引用自己项目封装好的 axios 配合后端联调
+ */
+import { request } from "./axios"; // 导入泛型 request 函数
+
+// 1. 定义请求参数类型
+interface ReqGetParams {
+  captchaType: string;
+  [key: string]: any;
+}
+
+interface ReqCheckParams {
+  captchaType: string;
+  pointJson: string;
+  token: string;
+  [key: string]: any;
+}
+
+// 2. 定义通用响应数据类型
+interface ApiResponse<T = any> {
+  repCode: string;
+  repMsg: string;
+  repData: T;
+}
+
+// 3. 利用泛型直接指定返回值类型,无需额外断言
+export function reqGet(data: ReqGetParams): Promise<ApiResponse> {
+  return request<ApiResponse>({ // 明确指定泛型类型为 ApiResponse
+    url: '/captcha/get',
+    method: 'post',
+    data
+  });
+}
+
+export function reqCheck(data: ReqCheckParams): Promise<ApiResponse> {
+  return request<ApiResponse>({ // 明确指定泛型类型为 ApiResponse
+    url: '/captcha/check',
+    method: 'post',
+    data
+  });
+}

+ 46 - 0
src/components/Login/components/verify/utils/axios.ts

@@ -0,0 +1,46 @@
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
+
+// 设置 axios 默认基础路径
+axios.defaults.baseURL = 'https://captcha.anji-plus.com/captcha-api';
+
+// 定义 axios 实例配置(继承 AxiosRequestConfig 类型,可自定义额外配置)
+const serviceConfig: AxiosRequestConfig = {
+  timeout: 40000,
+  headers: {
+    'X-Requested-With': 'XMLHttpRequest',
+    'Content-Type': 'application/json; charset=UTF-8'
+  },
+};
+
+// 创建 axios 实例并明确类型 AxiosInstance
+const service: AxiosInstance = axios.create(serviceConfig);
+
+// 请求拦截器:明确 config 类型为 AxiosRequestConfig,返回值类型为 AxiosRequestConfig | Promise<AxiosRequestConfig>
+service.interceptors.request.use(
+  (config: AxiosRequestConfig): AxiosRequestConfig => {
+    return config;
+  },
+  (error: AxiosError): Promise<never> => {
+    // 明确返回 Promise.reject,补充返回值类型
+    return Promise.reject(error);
+  }
+);
+
+// 响应拦截器:明确 response 类型为 AxiosResponse,error 类型为 AxiosError
+service.interceptors.response.use(
+  (response: AxiosResponse): any => {
+    // 提取响应数据,保留原有逻辑
+    const res = response.data;
+    return res;
+  },
+  (error: AxiosError): Promise<never> => {
+    // 可在这里添加错误处理逻辑(如提示网络错误等)
+    // 明确返回 Promise.reject,补充返回值类型
+    return Promise.reject(error);
+  }
+);
+
+// export default service;
+export function request<T = any>(config: AxiosRequestConfig): Promise<T> {
+  return service(config) as Promise<T>;
+}

+ 72 - 0
src/components/Login/components/verify/utils/util.ts

@@ -0,0 +1,72 @@
+// util.ts 完整修复后的 resetSize 函数
+interface ResetSizeVm {
+  imgSize?: { // 改为可选属性(避免传入的 vm 无 imgSize 时报错)
+    width: string;
+    height: string;
+  };
+  barSize?: { // 改为可选属性
+    width: string;
+    height: string;
+  };
+  $el: {
+    parentNode: {
+      offsetWidth: number;
+      offsetHeight: number;
+    };
+  };
+}
+
+interface ResetSizeResult {
+  imgWidth: string;
+  imgHeight: string;
+  barWidth: string;
+  barHeight: string;
+}
+
+export function resetSize(vm: ResetSizeVm): ResetSizeResult {
+  let img_width: string;
+  let img_height: string;
+  let bar_width: string;
+  let bar_height: string;
+
+  // 1. 父容器尺寸兜底(避免 parentNode 不存在)
+  const parentWidth = vm.$el?.parentNode?.offsetWidth || window.innerWidth || 360;
+  const parentHeight = vm.$el?.parentNode?.offsetHeight || window.innerHeight || 180;
+
+  // 2. imgSize 空值兜底(核心修复:避免访问 undefined.width)
+  const imgSize = vm.imgSize || { width: '360px', height: '180px' }; // 默认360*180
+  // 处理图片宽度
+  if (imgSize.width.indexOf('%') !== -1) {
+    img_width = (parseInt(imgSize.width) / 100 * parentWidth) + 'px';
+  } else {
+    img_width = imgSize.width || '360px'; // 兜底
+  }
+  // 处理图片高度
+  if (imgSize.height.indexOf('%') !== -1) {
+    img_height = (parseInt(imgSize.height) / 100 * parentHeight) + 'px';
+  } else {
+    img_height = imgSize.height || '180px'; // 兜底
+  }
+
+  // 3. barSize 空值兜底
+  const barSize = vm.barSize || { width: img_width, height: '40px' }; // 默认宽度和图片一致
+  // 处理滑块条宽度
+  if (barSize.width.indexOf('%') !== -1) {
+    bar_width = (parseInt(barSize.width) / 100 * parentWidth) + 'px';
+  } else {
+    bar_width = barSize.width || img_width; // 兜底为图片宽度
+  }
+  // 处理滑块条高度
+  if (barSize.height.indexOf('%') !== -1) {
+    bar_height = (parseInt(barSize.height) / 100 * parentHeight) + 'px';
+  } else {
+    bar_height = barSize.height || '40px'; // 兜底
+  }
+
+  return {
+    imgWidth: img_width,
+    imgHeight: img_height,
+    barWidth: bar_width,
+    barHeight: bar_height
+  };
+}

+ 1 - 1
src/hooks/setting/index.ts

@@ -1,6 +1,6 @@
 import type { GlobConfig } from '/#/config';
 
-import { warn } from '@/utils/log';
+// import { warn } from '@/utils/log';
 import { getAppEnvConfig } from '@/utils/env';
 
 export const useGlobSetting = (): Readonly<GlobConfig> => {

+ 7 - 6
utils/devProxy/staff/proxy.ts

@@ -3,15 +3,16 @@ import path from 'path';
 
 // staff环境
 const proxyStaff: PROXY_TYPE = {
-  serverHost: 'http://192.168.13.68:8802/',
-  skyeyeLoginHost: 'http://192.168.13.68:7000/skyeye-login/#/',
-  skyeyePlatformHost: 'http://192.168.13.68:7000/skyeye-pc/#/',
-  skyeyeAdminHost: 'http://192.168.13.68:7000/skyeye-admin/#/',
-  skyeyeH5Host: 'http://192.168.13.68:7000/skyeye-h5/#/',
+  serverHost: 'http://192.168.6.42:8802/',
+  // serverHost: 'http://192.168.20.4:8802/',
+  skyeyeLoginHost: 'http://192.168.6.42:7000/skyeye-login/#/',
+  skyeyePlatformHost: 'http://192.168.6.42:7000/skyeye-pc/#/',
+  skyeyeAdminHost: 'http://192.168.6.42:7000/skyeye-admin/#/',
+  skyeyeH5Host: 'http://192.168.6.42:7000/skyeye-h5/#/',
   tiansuoHost: 'http://192.168.6.33:91/',
   fileUploadHost: 'http://192.168.13.102:9000/',
   violation_src: 'http://192.168.13.102:62/violation_src',
-  push_stream_host: 'http://192.168.13.68:7000/skyeye-admin/push_stream_host/',
+  push_stream_host: 'http://192.168.6.42:7000/skyeye-admin/push_stream_host/',
 };
 
 // 对外导出的代理