Просмотр исходного кода

企业修改密码安全验证

lifanagju_citu 6 месяцев назад
Родитель
Сommit
27e3161c6e

+ 5 - 0
components.d.ts

@@ -60,11 +60,16 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SimilarPositions: typeof import('./src/components/Position/similarPositions.vue')['default']
+    Src: typeof import('./src/components/Verifition/src/index.vue')['default']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
     TextArea: typeof import('./src/components/FormUI/textArea/index.vue')['default']
     TextInput: typeof import('./src/components/FormUI/TextInput/index.vue')['default']
     TipDialog: typeof import('./src/components/CtDialog/tipDialog.vue')['default']
     VerificationCode: typeof import('./src/components/VerificationCode/index.vue')['default']
+    Verifition: typeof import('./src/components/Verifition/index.vue')['default']
+    Verify: typeof import('./src/components/Verifition/src/Verify.vue')['default']
+    VerifyPoints: typeof import('./src/components/Verifition/Verify/VerifyPoints.vue')['default']
+    VerifySlide: typeof import('./src/components/Verifition/Verify/VerifySlide.vue')['default']
     WangEditor: typeof import('./src/components/FormUI/wangEditor/index.vue')['default']
   }
 }

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "pinia-plugin-persistedstate": "^3.2.1",
     "pnpm": "^9.1.0",
     "qrcode": "^1.5.4",
+    "crypto-js": "^4.2.0",
     "qs": "^6.12.1",
     "roboto-fontface": "*",
     "v-clipboard": "^3.0.0-next.1",

+ 29 - 0
src/api/Verifition.js

@@ -0,0 +1,29 @@
+import request from '@/config/axios'
+
+// import { getCodeTestData } from '@/components/Verifition/Verify/getCodeTestData'
+// import { reqCheckFalseData, reqCheckTrueData } from '@/components/Verifition/Verify/reqCheckTestData'
+// console.log(1, '23456', reqCheckFalseData)
+// // 获取验证图片以及 token
+// export const getCode = async () => {
+//   return getCodeTestData
+// }
+// // 滑动或者点选验证
+// export const reqCheck = async () => {
+//   return reqCheckTrueData
+// }
+
+// 获取验证图片以及 token
+export const getCode = async (data) => {
+  return await request.postOriginal({
+    url: '/app-api/system/captcha/get',
+    data
+  })
+}
+
+// 滑动或者点选验证
+export const reqCheck = async (data) => {
+  return await request.postOriginal({
+    url: '/app-api/system/captcha/check',
+    data
+  })
+}

+ 251 - 0
src/components/Verifition/Verify/VerifyPoints.vue

@@ -0,0 +1,251 @@
+<template>
+  <div style="position: relative">
+    <div class="verify-img-out">
+      <div
+        :style="{
+          width: setSize.imgWidth,
+          height: setSize.imgHeight,
+          'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+          'margin-bottom': vSpace + 'px'
+        }"
+        class="verify-img-panel"
+      >
+        <div v-show="showRefresh" class="verify-refresh" style="z-index: 3" @click="refresh">
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <img
+          ref="canvas"
+          :src="'data:image/png;base64,' + pointBackImgBase"
+          alt=""
+          style="display: block; width: 100%; height: 100%"
+          @click="bindingClick ? canvasClick($event) : undefined"
+        />
+
+        <div
+          v-for="(tempPoint, index) in tempPoints"
+          :key="index"
+          :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'
+          }"
+          class="point-area"
+        >
+          {{ index + 1 }}
+        </div>
+      </div>
+    </div>
+    <!-- 'height': this.barSize.height, -->
+    <div
+      :style="{
+        width: setSize.imgWidth,
+        color: barAreaColor,
+        'border-color': barAreaBorderColor,
+        'line-height': barSize.height
+      }"
+      class="verify-bar-area"
+    >
+      <span class="verify-msg">{{ text }}</span>
+    </div>
+  </div>
+</template>
+<script setup type="text/babel">
+/**
+ * VerifyPoints
+ * @description 点选
+ * */
+import { resetSize } from './../utils/util'
+import { aesEncrypt } from './../utils/ase'
+import { getCode, reqCheck } from '@/api/Verifition'
+import { getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+
+const props = defineProps({
+  //弹出式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'
+      }
+    }
+  }
+})
+
+const { t } = useI18n()
+const { mode, captchaType } = 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 = t('captcha.success')
+          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 = t('captcha.fail')
+          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 = async 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
+  await getPictrue()
+  showRefresh.value = true
+}
+
+// 请求背景图片和验证图片
+const getPictrue = async () => {
+  let data = {
+    captchaType: captchaType.value
+  }
+  const res = await getCode(data)
+  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 = t('captcha.point') + '【' + 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
+}
+</script>

+ 380 - 0
src/components/Verifition/Verify/VerifySlide.vue

@@ -0,0 +1,380 @@
+<template>
+  <div style="position: relative">
+    <div
+      v-if="type === '2'"
+      :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"
+      class="verify-img-out"
+    >
+      <div :style="{ width: setSize.imgWidth, height: setSize.imgHeight }" class="verify-img-panel">
+        <img
+          :src="'data:image/png;base64,' + backImgBase"
+          alt=""
+          style="display: block; width: 100%; height: 100%"
+        />
+        <div v-show="showRefresh" class="verify-refresh" @click="refresh">
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <transition name="tips">
+          <span v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'" class="verify-tips">
+            {{ tipWords }}
+          </span>
+        </transition>
+      </div>
+    </div>
+    <!-- 公共部分 -->
+    <div
+      :style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"
+      class="verify-bar-area"
+    >
+      <span class="verify-msg" v-text="text"></span>
+      <div
+        :style="{
+          width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+          height: barSize.height,
+          'border-color': leftBarBorderColor,
+          transaction: transitionWidth
+        }"
+        class="verify-left-bar"
+      >
+        <span class="verify-msg" v-text="finishText"></span>
+        <div
+          :style="{
+            width: barSize.height,
+            height: barSize.height,
+            'background-color': moveBlockBackgroundColor,
+            left: moveBlockLeft,
+            transition: transitionLeft
+          }"
+          class="verify-move-block"
+          @mousedown="start"
+          @touchstart="start"
+        >
+          <i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
+          <div
+            v-if="type === '2'"
+            :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
+            }"
+            class="verify-sub-block"
+          >
+            <img
+              :src="'data:image/png;base64,' + blockBackImgBase"
+              alt=""
+              style="display: block; width: 100%; height: 100%; -webkit-user-drag: none"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup type="text/babel">
+/**
+ * VerifySlide
+ * @description 滑块
+ * */
+import { aesEncrypt } from './../utils/ase'
+import { resetSize } from './../utils/util'
+import { getCode, reqCheck } from '@/api/Verifition'
+import { useI18n } from '@/hooks/web/useI18n'
+import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue'
+
+const props = defineProps({
+  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: '30px'
+      }
+    }
+  }
+})
+
+const { t } = useI18n()
+const { mode, captchaType, 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(''), //移动结束的时间
+  tipWords = ref(''),
+  text = ref(''),
+  finishText = ref(''),
+  setSize = reactive({
+    imgHeight: 0,
+    imgWidth: 0,
+    barHeight: 0,
+    barWidth: 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')
+})
+const init = () => {
+  if (explain.value === '') {
+    text.value = t('captcha.slide')
+  } else {
+    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
+  }
+})
+//鼠标按下
+const start = (e) => {
+  e = e || window.event
+  let x
+  if (!e.touches) {
+    //兼容PC端
+    x = e.clientX
+  } else {
+    //兼容移动端
+    x = e.touches[0].pageX
+  }
+  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
+  }
+}
+//鼠标移动
+const move = (e) => {
+  e = e || window.event
+  if (status.value && isEnd.value == false) {
+    let x
+    if (!e.touches) {
+      //兼容PC端
+      x = e.clientX
+    } else {
+      //兼容移动端
+      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'
+  }
+}
+
+//鼠标松开
+const end = () => {
+  endMovetime.value = +new Date()
+  //判断是否重合
+  if (status.value && isEnd.value == false) {
+    var moveLeftDistance = parseInt((moveBlockLeft.value || '0').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
+            ${t('captcha.success')}`
+        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 = t('captcha.fail')
+        setTimeout(() => {
+          tipWords.value = ''
+        }, 1000)
+      }
+    })
+    status.value = false
+  }
+}
+
+const refresh = async () => {
+  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
+
+  await getPictrue()
+  setTimeout(() => {
+    transitionWidth.value = ''
+    transitionLeft.value = ''
+    text.value = explain.value
+  }, 300)
+}
+
+// 请求背景图片和验证图片
+const getPictrue = async () => {
+  let data = {
+    captchaType: captchaType.value
+  }
+  const res = await getCode(data)
+  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
+  }
+}
+</script>

Разница между файлами не показана из-за своего большого размера
+ 11 - 0
src/components/Verifition/Verify/getCodeTestData.js


+ 4 - 0
src/components/Verifition/Verify/index.js

@@ -0,0 +1,4 @@
+import VerifySlide from './VerifySlide.vue'
+import VerifyPoints from './VerifyPoints.vue'
+
+export { VerifySlide, VerifyPoints }

+ 32 - 0
src/components/Verifition/Verify/reqCheckTestData.js

@@ -0,0 +1,32 @@
+export const reqCheckFalseData = {
+  repCode: "6110", 
+  repMsg: "验证码已失效,请重新获取", 
+  repData: null, 
+  success: false
+}
+export const reqCheckTrueData = {
+  repCode: "0000", 
+  repMsg: null, 
+  repData: {
+    captchaId: null, 
+    projectCode: null, 
+    captchaType: "blockPuzzle", 
+    captchaOriginalPath: null, 
+    captchaFontType: null, 
+    captchaFontSize: null, 
+    secretKey: null, 
+    originalImageBase64: null, 
+    point: null, 
+    jigsawImageBase64: null, 
+    wordList: null, 
+    pointList: null, 
+    pointJson: "xPg4qeUPl+RAzeDfamQ+dghB/pRhfuoYYRRpUUvVU2U=", 
+    token: "4793854297d34362848f21013fb6e11f", 
+    result: true, 
+    captchaVerification: null, 
+    clientUid: null, 
+    ts: null, 
+    browserInfo: null
+  }, 
+  success: true
+}

Разница между файлами не показана из-за своего большого размера
+ 374 - 0
src/components/Verifition/index.vue


+ 14 - 0
src/components/Verifition/utils/ase.js

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

+ 97 - 0
src/components/Verifition/utils/util.js

@@ -0,0 +1,97 @@
+export function resetSize(vm) {
+  let img_width, img_height, bar_width, bar_height //图片的宽度、高度,移动条的宽度、高度
+  const EmployeeWindow = window
+  const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth
+  const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.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']

+ 7 - 0
src/locales/en.js

@@ -116,6 +116,13 @@ export default {
     loginAgain: 'please log in again',
     loginFailed: 'Login failed, And no enterprise was found under this user'
   },
+  captcha: {
+    verification: 'Please complete security verification',
+    slide: 'Swipe right to complete verification',
+    point: 'Please click',
+    success: 'Verification succeeded',
+    fail: 'verification failed'
+  },
   form: {},
   headhunting: {
     headhuntingName: 'Menduner Hunting Service',

+ 7 - 0
src/locales/zh-CN.js

@@ -116,6 +116,13 @@ export default {
     loginAgain: '请重新登录',
     loginFailed: '登录失败 未查询到该用户下存在企业'
   },
+  captcha: {
+    verification: '请完成安全验证',
+    slide: '向右滑动完成验证',
+    point: '请依次点击',
+    success: '验证成功',
+    fail: '验证失败'
+  },
   form: {},
   headhunting: {
     headhuntingName: '门墩儿猎寻服务',

+ 46 - 29
src/views/login/components/editPasswordEnt.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <v-form ref="passwordRef" style="width: 370px;">
+    <v-form ref="emailRef" style="width: 100%;">
       <v-text-field
         v-model="query.email"
         label="企业邮箱"
@@ -11,16 +11,20 @@
         prepend-inner-icon="mdi-email" 
         :rules="[v=> !!v || '请输入企业邮箱', v=> checkEmail(v)]"
       ></v-text-field>
-      <v-text-field
-        v-model="query.code"
-        label="邮箱验证码"
-        placeholder="请输入邮箱收到的验证码" 
-        variant="outlined" 
-        density="compact"
-        color="primary"
-        prepend-inner-icon="mdi-form-textbox-password"
-        :rules="[v=> !!v || '请输入邮箱收到的验证码']"
-      ></v-text-field>
+      <div class="d-flex">
+        <v-text-field
+          v-model="query.code"
+          label="验证码"
+          placeholder="请输入邮箱收到的验证码" 
+          variant="outlined" 
+          density="compact"
+          color="primary"
+          :rules="[v=> !!v || '请输入邮箱收到的验证码']"
+        ></v-text-field>
+        <v-btn color="primary" class="ml-2" style="margin-top: 2px;" @click="emit('getCode')">获取验证码</v-btn>
+      </div>
+    </v-form>
+    <v-form ref="passwordRef" style="width: 100%;">
       <v-text-field
         v-model="query.password"
         placeholder="请输入新密码" 
@@ -47,8 +51,8 @@
       ></v-text-field>
     </v-form>
     <div class="text-center mt-5">
-      <v-btn v-if="showCancelBtn" class="mr-5" color="primary" variant="outlined" @click="handleClose">取 消</v-btn>
-      <v-btn color="primary" :max-width="showCancelBtn ? 370 : 95" :min-width="showCancelBtn ? 95 : 370" @click="handleSubmit" :loading="loading">确认修改</v-btn>
+      <!-- <v-btn v-if="showCancelBtn" class="mr-5" color="primary" variant="outlined" @click="handleClose">取 消</v-btn> -->
+      <v-btn color="primary" style="width: 100%;" @click="handleSubmit" :loading="loading">确认修改</v-btn>
     </div>
   </div>
 </template>
@@ -60,12 +64,16 @@ import { updatePassword, resetPassword } from '@/api/common/index'
 import Snackbar from '@/plugins/snackbar'
 import { checkEmail } from '@/utils/validate'
 
-const emit = defineEmits(['cancel'])
+const emit = defineEmits(['cancel', 'getCode'])
 const props = defineProps({
   phone: {
     type: String,
     default: ''
   },
+  verifySuccess: {
+    type: Boolean,
+    default: false
+  },
   showCancelBtn: {
     type: Boolean,
     default: true
@@ -77,11 +85,12 @@ const props = defineProps({
 })
 
 let query = reactive({
-  email: '',
-  code: '',
-  password: '',
-  checkPassword: ''
+  email: null,
+  code: null,
+  password: null,
+  checkPassword: null,
 })
+const emailRef = ref(false)
 const passwordRef = ref(false)
 const show = ref(false)
 const loading = ref(false)
@@ -98,20 +107,27 @@ const confirmPassword = computed(() => {
 })
 
 
-const handleClose = () => {
-  query = {
-    password: '',
-    checkPassword: ''
-  }
-  passwordType.value = false
-  loading.value = false
-  emit('cancel')
-}
+// const handleClose = () => {
+//   query = {
+//     email: null,
+//     code: null,
+//     password: null,
+//     checkPassword: null,
+//   }
+//   passwordType.value = false
+//   loading.value = false
+//   emit('cancel')
+// }
 
 // 修改
 const handleSubmit = async () => {
+  const emailValid = await emailRef.value.validate()
   const passwordValid = await passwordRef.value.validate()
-  if (!passwordValid.valid) return
+  if (!emailValid.valid || !passwordValid.valid) return
+  if (!props.verifySuccess) {
+    emit('getCode')
+    return
+  }
   const data = {
     password: query.password,
   }
@@ -122,9 +138,10 @@ const handleSubmit = async () => {
     Snackbar.success('修改成功')
   } finally {
     loading.value = false
-    handleClose()
+    // handleClose()
   }
 }
+
 </script>
 
 <style scoped lang="scss">

+ 31 - 2
src/views/login/forgotPasswordEnt.vue

@@ -5,9 +5,16 @@
       <div class="resume-header">
         <div class="resume-title">企业修改密码</div>
       </div>
-      <editPasswordPage class="mt-5" :showCancelBtn="false" :isReset="true" @cancel="router.push('/login')"></editPasswordPage>
+      <editPasswordPage class="mt-5" :showCancelBtn="false" :isReset="true" :verifySuccess="verifySuccess" @getCode="getCode"  @cancel="router.push('/login')"></editPasswordPage>
     </div>
   </div>
+  <Verify
+    ref="verify"
+    :captchaType="captchaType"
+    :imgSize="{ width: '400px', height: '200px' }"
+    mode="pop"
+    @success="verifySuccess"
+  />
 </template>
 
 <script setup>
@@ -15,8 +22,30 @@ defineOptions({ name: 'forgotPasswordEnt'})
 import { useRouter } from 'vue-router'
 import navBar from '@/layout/personal/navBar.vue'
 import editPasswordPage from '@/views/login/components/editPasswordEnt.vue'
+import Verify from '@/components/Verifition'
+import { ref } from 'vue'
 
 const router = useRouter()
+
+
+// 验证码
+const useVerify = ref(true)
+const verify = ref()
+const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
+
+const verifySuccess = ref(false)
+// 获取验证码
+const getCode = async () => {
+  // 情况一,未开启:则直接登录
+  if (!useVerify.value) {
+    // await handleSubmit()
+    verifySuccess.value = true
+  } else {
+    // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
+    // 弹出验证码
+    verify.value.show()
+  }
+}
 </script>
 
 <style scoped lang="scss">
@@ -37,7 +66,7 @@ const router = useRouter()
   top: 50%;
   left: 50%;
   translate: -50% -50%;
-  width: 450px;
+  width: 500px;
   height: 450px;
   background-color: #fff;
   border-radius: 10px;

Некоторые файлы не были показаны из-за большого количества измененных файлов