Browse Source

feat: vue3 add aj-captcha

xingyu 2 years ago
parent
commit
414671e881

+ 1 - 0
yudao-ui-admin-vue3/package.json

@@ -38,6 +38,7 @@
     "element-plus": "2.2.12",
     "element-plus": "2.2.12",
     "intro.js": "^6.0.0",
     "intro.js": "^6.0.0",
     "jsencrypt": "^3.2.1",
     "jsencrypt": "^3.2.1",
+    "crypto-js": "^4.1.1",
     "lodash-es": "^4.17.21",
     "lodash-es": "^4.17.21",
     "mitt": "^3.0.0",
     "mitt": "^3.0.0",
     "nprogress": "^0.2.0",
     "nprogress": "^0.2.0",

+ 0 - 5
yudao-ui-admin-vue3/src/api/login/index.ts

@@ -18,11 +18,6 @@ export interface SmsLoginVO {
   code: string
   code: string
 }
 }
 
 
-// 获取验证码
-export const getCodeImgApi = () => {
-  return request.get({ url: '/system/captcha/get-image' })
-}
-
 // 登录
 // 登录
 export const loginApi = (data: UserLoginVO) => {
 export const loginApi = (data: UserLoginVO) => {
   return request.post({ url: '/system/auth/login', data })
   return request.post({ url: '/system/auth/login', data })

+ 0 - 2
yudao-ui-admin-vue3/src/api/login/types.ts

@@ -1,8 +1,6 @@
 export type UserLoginVO = {
 export type UserLoginVO = {
   username: string
   username: string
   password: string
   password: string
-  code: string
-  uuid: string
 }
 }
 
 
 export type TokenType = {
 export type TokenType = {

+ 3 - 0
yudao-ui-admin-vue3/src/components/Verifition/index.ts

@@ -0,0 +1,3 @@
+import Verify from './src/Verify.vue'
+
+export { Verify }

File diff suppressed because it is too large
+ 438 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/Verify.vue


+ 279 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/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: barAreaColor,
+        'border-color': barAreaBorderColor,
+        'line-height': barSize.height
+      }"
+    >
+      <span class="verify-msg">{{ text }}</span>
+    </div>
+  </div>
+</template>
+<script type="text/babel">
+/**
+ * VerifyPoints
+ * @description 点选
+ * */
+import { resetSize } from './../utils/util'
+import { aesEncrypt } from './../utils/ase'
+import { reqGet, reqCheck } from './../api/index'
+import { onMounted, reactive, ref, nextTick, toRefs, 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) {
+    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 = '验证成功'
+              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>

+ 422 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/Verify/VerifySlide.vue

@@ -0,0 +1,422 @@
+<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,
+  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) {
+    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(''), //移动结束的时间
+      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>

+ 4 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/Verify/index.ts

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

+ 24 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/api/index.ts

@@ -0,0 +1,24 @@
+/**
+ * 此处可直接引用自己项目封装好的 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
+  })
+}

+ 14 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/utils/ase.ts

@@ -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()
+}

+ 26 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/utils/axios.ts

@@ -0,0 +1,26 @@
+import axios from 'axios'
+
+axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
+
+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
+})
+export default service

+ 97 - 0
yudao-ui-admin-vue3/src/components/Verifition/src/utils/util.ts

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

+ 23 - 50
yudao-ui-admin-vue3/src/views/Login/components/LoginForm.vue

@@ -30,6 +30,7 @@ import { required } from '@/utils/formRules'
 import { Icon } from '@/components/Icon'
 import { Icon } from '@/components/Icon'
 import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
 import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
 import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
 import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
+import { Verify } from '@/components/Verifition'
 
 
 const { currentRoute, addRoute, push } = useRouter()
 const { currentRoute, addRoute, push } = useRouter()
 const permissionStore = usePermissionStore()
 const permissionStore = usePermissionStore()
@@ -46,12 +47,10 @@ const { t } = useI18n()
 const iconHouse = useIcon({ icon: 'ep:house' })
 const iconHouse = useIcon({ icon: 'ep:house' })
 const iconAvatar = useIcon({ icon: 'ep:avatar' })
 const iconAvatar = useIcon({ icon: 'ep:avatar' })
 const iconLock = useIcon({ icon: 'ep:lock' })
 const iconLock = useIcon({ icon: 'ep:lock' })
-const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
 const LoginCaptchaRules = {
 const LoginCaptchaRules = {
   tenantName: [required],
   tenantName: [required],
   username: [required],
   username: [required],
-  password: [required],
-  code: [required]
+  password: [required]
 }
 }
 const LoginRules = {
 const LoginRules = {
   tenantName: [required],
   tenantName: [required],
@@ -60,7 +59,6 @@ const LoginRules = {
 }
 }
 const loginLoading = ref(false)
 const loginLoading = ref(false)
 const loginData = reactive({
 const loginData = reactive({
-  codeImg: '',
   isShowPassword: false,
   isShowPassword: false,
   captchaEnable: true,
   captchaEnable: true,
   tenantEnable: true,
   tenantEnable: true,
@@ -72,20 +70,16 @@ const loginData = reactive({
     tenantName: '芋道源码',
     tenantName: '芋道源码',
     username: 'admin',
     username: 'admin',
     password: 'admin123',
     password: 'admin123',
-    rememberMe: false,
-    code: '',
-    uuid: ''
+    rememberMe: false
   }
   }
 })
 })
+// blockPuzzle 滑块 clickWord 点击文字
+const verify = ref()
+const captchaType = ref('blockPuzzle')
 
 
 // 获取验证码
 // 获取验证码
 const getCode = async () => {
 const getCode = async () => {
-  const res = await LoginApi.getCodeImgApi()
-  loginData.captchaEnable = res.enable
-  if (res.enable) {
-    loginData.codeImg = 'data:image/gif;base64,' + res.img
-    loginData.loginForm.uuid = res.uuid
-  }
+  verify.value.show()
 }
 }
 //获取租户ID
 //获取租户ID
 const getTenantId = async () => {
 const getTenantId = async () => {
@@ -112,19 +106,12 @@ const handleLogin = async () => {
   const data = await validForm()
   const data = await validForm()
   if (!data) return
   if (!data) return
   loginLoading.value = true
   loginLoading.value = true
-  await LoginApi.loginApi(loginData.loginForm)
-    .then(async (res) => {
-      setToken(res)
-      const userInfo = await LoginApi.getInfoApi()
-      await userStore.getUserInfoAction(userInfo)
-      await getRoutes()
-    })
-    .catch(() => {
-      getCode()
-    })
-    .finally(() => {
-      loginLoading.value = false
-    })
+  const res = await LoginApi.loginApi(loginData.loginForm)
+  setToken(res)
+  const userInfo = await LoginApi.getInfoApi()
+  await userStore.getUserInfoAction(userInfo)
+  await getRoutes()
+  loginLoading.value = false
 }
 }
 
 
 // 获取路由
 // 获取路由
@@ -159,8 +146,7 @@ watch(
     immediate: true
     immediate: true
   }
   }
 )
 )
-onMounted(async () => {
-  await getCode()
+onMounted(() => {
   getCookie()
   getCookie()
 })
 })
 </script>
 </script>
@@ -207,31 +193,11 @@ onMounted(async () => {
             type="password"
             type="password"
             :placeholder="t('login.passwordPlaceholder')"
             :placeholder="t('login.passwordPlaceholder')"
             show-password
             show-password
-            @keyup.enter="handleLogin"
+            @keyup.enter="getCode()"
             :prefix-icon="iconLock"
             :prefix-icon="iconLock"
           />
           />
         </el-form-item>
         </el-form-item>
       </el-col>
       </el-col>
-      <el-col :span="24" style="padding-left: 10px; padding-right: 10px">
-        <el-form-item prop="code" v-if="loginData.captchaEnable">
-          <el-row justify="space-between" style="width: 100%">
-            <el-col :span="14">
-              <el-input
-                v-model="loginData.loginForm.code"
-                :placeholder="t('login.codePlaceholder')"
-                @keyup.enter="handleLogin"
-                :prefix-icon="iconCircleCheck"
-                style="width: 90%"
-              />
-            </el-col>
-            <el-col :span="10">
-              <div class="login-code">
-                <img :src="loginData.codeImg" @click="getCode" class="login-code-img" alt="" />
-              </div>
-            </el-col>
-          </el-row>
-        </el-form-item>
-      </el-col>
       <el-col
       <el-col
         :span="24"
         :span="24"
         style="padding-left: 10px; padding-right: 10px; margin-top: -20px; margin-bottom: -20px"
         style="padding-left: 10px; padding-right: 10px; margin-top: -20px; margin-bottom: -20px"
@@ -251,11 +217,18 @@ onMounted(async () => {
       </el-col>
       </el-col>
       <el-col :span="24" style="padding-left: 10px; padding-right: 10px">
       <el-col :span="24" style="padding-left: 10px; padding-right: 10px">
         <el-form-item>
         <el-form-item>
-          <el-button :loading="loginLoading" type="primary" class="w-[100%]" @click="handleLogin">
+          <el-button :loading="loginLoading" type="primary" class="w-[100%]" @click="getCode()">
             {{ t('login.login') }}
             {{ t('login.login') }}
           </el-button>
           </el-button>
         </el-form-item>
         </el-form-item>
       </el-col>
       </el-col>
+      <Verify
+        ref="verify"
+        mode="pop"
+        :captchaType="captchaType"
+        :imgSize="{ width: '400px', height: '200px' }"
+        @success="handleLogin"
+      />
       <el-col :span="24" style="padding-left: 10px; padding-right: 10px">
       <el-col :span="24" style="padding-left: 10px; padding-right: 10px">
         <el-form-item>
         <el-form-item>
           <el-row justify="space-between" style="width: 100%" :gutter="5">
           <el-row justify="space-between" style="width: 100%" :gutter="5">