Browse Source

15. 接入验证码的功能

YunaiV 2 years ago
parent
commit
b4308aa178

+ 6 - 0
.env

@@ -7,3 +7,9 @@ VITE_DEV_PATH = 'http://127.0.0.1:48080'
 
 # production path
 VITE_PRO_PATH = 'https://demo.mtruning.club'
+
+# 租户开关
+VITE_APP_TENANT_ENABLE=true
+
+# 验证码的开关
+VITE_APP_CAPTCHA_ENABLE=true

+ 1 - 0
src/api/axios.ts

@@ -58,6 +58,7 @@ axiosInstance.interceptors.response.use(
     if (isPreview()) {
       return Promise.resolve(res.data)
     }
+    // 如果是验证码的返回,直接返回数据
     const { code } = res.data as { code: number }
 
     if (code === undefined || code === null) return Promise.resolve(res)

+ 20 - 0
src/api/path/system.api.ts

@@ -23,3 +23,23 @@ export const logoutApi = async () => {
     httpErrorHandle()
   }
 }
+
+// 获取验证图片  以及token
+export const getCodeApi = async (data: any) => {
+  try {
+    const res = await http(RequestHttpEnum.POST)(`${ModuleTypeEnum.SYSTEM}/captcha/get`, data)
+    return res.data
+  } catch (err) {
+    httpErrorHandle()
+  }
+}
+
+// 滑动或者点选验证
+export const reqCheckApi = async (data: any) => {
+  try {
+    const res = await http(RequestHttpEnum.POST)(`${ModuleTypeEnum.SYSTEM}/captcha/check`, data)
+    return res.data
+  } catch (err) {
+    httpErrorHandle()
+  }
+}

+ 3 - 0
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
+ 366 - 0
src/components/Verifition/src/Verify.vue


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

@@ -0,0 +1,251 @@
+<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" setup>
+/**
+ * VerifyPoints
+ * @description 点选
+ * */
+import { resetSize } from './../utils/util'
+import { aesEncrypt } from './../utils/ase'
+import { getCodeApi, reqCheckApi } from '@/api/path'
+import { onMounted, reactive, ref, nextTick, toRefs, getCurrentInstance } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+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
+      }
+      reqCheckApi(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 getCodeApi(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>

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

@@ -0,0 +1,378 @@
+<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" setup>
+/**
+ * VerifySlide
+ * @description 滑块
+ * */
+import { aesEncrypt } from './../utils/ase'
+import { resetSize } from './../utils/util'
+import { getCodeApi, reqCheckApi } from '@/api/path'
+import { useI18n } from 'vue-i18n'
+import { onMounted, reactive, ref, nextTick, toRefs, getCurrentInstance, computed, 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
+  if (!e.touches) {
+    //兼容PC端
+    var x = e.clientX
+  } else {
+    //兼容移动端
+    var 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) {
+    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'
+  }
+}
+
+//鼠标松开
+const 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
+    }
+    reqCheckApi(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 getCodeApi(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>

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

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

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

@@ -0,0 +1,14 @@
+import CryptoJS from 'crypto-js'
+/**
+ * @word 要加密的内容
+ * @keyWord String  服务器随机返回的关键字
+ *  */
+export function aesEncrypt(word : string, 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/src/utils/util.ts

@@ -0,0 +1,97 @@
+export function resetSize(vm: any) {
+  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']

+ 29 - 4
src/views/login/index.vue

@@ -94,13 +94,20 @@
                 <n-form-item>
                   <n-button
                     type="primary"
-                    @click="handleSubmit"
+                    @click="getCode"
                     size="large"
                     :loading="loading"
                     block
                     >{{ $t('login.form_button') }}</n-button
                   >
                 </n-form-item>
+                <Verify
+                    ref="verify"
+                    mode="pop"
+                    :captchaType="captchaType"
+                    :imgSize="{ width: '400px', height: '200px' }"
+                    @success="handleSubmit"
+                />
               </n-form>
             </n-card>
           </n-collapse-transition>
@@ -129,6 +136,7 @@ import { StorageEnum } from '@/enums/storageEnum'
 import { icon } from '@/plugins'
 import { routerTurnByName } from '@/utils'
 import { loginApi } from '@/api/path'
+import { Verify } from '@/components/Verifition'
 
 interface FormState {
   username: string
@@ -196,9 +204,25 @@ const shuffleHandle = () => {
   }, carouselInterval)
 }
 
+// 验证码
+const verify = ref()
+const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
+
+// 获取验证码
+const captchaEnable = import.meta.env.VITE_APP_CAPTCHA_ENABLE
+const getCode = async () => {
+  // 情况一,未开启:则直接登录
+  if (captchaEnable === 'false') {
+    await handleSubmit({})
+  } else {
+    // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
+    // 弹出验证码
+    verify.value.show()
+  }
+}
+
 // 登录
-const handleSubmit = async (e: Event) => {
-  e.preventDefault()
+const handleSubmit = async (params: any) => {
   formRef.value.validate(async (errors: any) => {
     if (!errors) {
       const { username, password } = formInline
@@ -206,7 +230,8 @@ const handleSubmit = async (e: Event) => {
       // 提交请求【登录】
       const loginRes = await loginApi({
         username,
-        password
+        password,
+        captchaVerification: params.captchaVerification
       })
       if(loginRes && loginRes.data) {
         // Token 信息

Some files were not shown because too many files changed in this diff