Ver Fonte

feat: 添加图形验证

zhouyuhao há 1 ano atrás
pai
commit
724d102404

+ 1 - 0
package.json

@@ -37,6 +37,7 @@
     "vue": "^3.4.29",
     "vue-draggable-plus": "^0.5.3",
     "vue-router": "^4.3.3",
+    "vue3-puzzle-vcode": "^1.1.7",
     "vuedraggable": "^2.24.3",
     "vxe-pc-ui": "^4.1.7",
     "vxe-table": "^4.7.70",

+ 2 - 1
src/auto-imports.d.ts

@@ -3,6 +3,7 @@
 // @ts-nocheck
 // noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
+// biome-ignore lint: disable
 export {}
 declare global {
   const $api: typeof import('@/api/index')['default']
@@ -68,6 +69,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

+ 11 - 4
src/components/VBreadcrumb/src/VBreadcrumb.vue

@@ -6,12 +6,17 @@ const router = useRouter()
 const breadCrumb = useBreadCrumb()
 
 const handleGoBack = () => {
-  router.go(-1)
+  const routeData = breadCrumb.getUpperRoute()
+  router.push({
+    name: routeData.label,
+    query: routeData.query
+  })
 }
-const jumpLink = (label: string) => {
+const jumpLink = (label: string, query: any) => {
   label &&
     router.push({
-      name: label
+      name: label,
+      query: query
     })
 }
 </script>
@@ -21,7 +26,9 @@ const jumpLink = (label: string) => {
     <span class="font_family icon-icon_back_b" @click="handleGoBack"></span>
     <template v-for="(routeItem, index) in breadCrumb.routeList" :key="routeItem.label">
       <template v-if="index + 1 !== breadCrumb.routeList.length">
-        <span @click="jumpLink(routeItem.label)" class="previous-route">{{ routeItem.label }}</span>
+        <span @click="jumpLink(routeItem.label, routeItem.query)" class="previous-route">{{
+          routeItem.label
+        }}</span>
         <span class="interval">|</span>
       </template>
       <span v-else style="font-weight: 700">{{ routeItem.label }}</span>

+ 3 - 0
src/stores/modules/breadCrumb.ts

@@ -53,6 +53,9 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         ]
       }
       localStorage.setItem('routeList', JSON.stringify(this.routeList))
+    },
+    getUpperRoute() {
+      return this.routeList[this.routeList.length - 2]
     }
   }
 })

+ 193 - 0
src/views/Login/src/components/SliderVerification.vue

@@ -0,0 +1,193 @@
+<script lang="ts" setup>
+//滑动图片验证码部分
+import Img01 from '../image/bg.png'
+import { ref } from 'vue'
+//引入'vue3-puzzle-vcode'插件
+import Vcode from 'vue3-puzzle-vcode'
+
+const openDialog = () => {
+  isShow.value = true
+  nextTick(() => {
+    addElement()
+    onSuccess()
+    updateSliderBackground('success')
+  })
+}
+// 添加自定义元素
+const addElement = () => {
+  addTipsNode()
+  addSliderBtnNode('start')
+}
+
+const addTipsNode = () => {
+  const childNode = document.createElement('div')
+  childNode.className = 'tips'
+  childNode.innerHTML = `
+    <p>Please drag the slider below to complete the</p>
+    <p>verification to ensure normal access</p>
+  `
+  const parentNode = document.querySelector('.vue-auth-box_')
+  if (parentNode.firstChild) {
+    parentNode.insertBefore(childNode, parentNode.firstChild)
+  } else {
+    parentNode.appendChild(childNode)
+  }
+}
+
+const styleMap = {
+  start: {
+    thumbColor: 'var(--color-neutral-1)',
+    thumbIcon: 'icon-icon_drag__line_b',
+    trackBackground: '#87909e'
+  },
+  dragging: {
+    thumbColor: 'var(--color-neutral-1)',
+    thumbIcon: 'icon-icon_drag__line_b',
+    trackBackground: 'var(--color-success)'
+  },
+  success: {
+    thumbColor: '#fff',
+    thumbIcon: 'icon-icon_confirm_b',
+    trackBackground: 'var(--color-success)'
+  },
+  error: {
+    thumbColor: '#fff',
+    thumbIcon: 'icon-icon_reject_b',
+    trackBackground: '#c7353f'
+  }
+}
+
+const addSliderBtnNode = (state: string) => {
+  const parentNode = document.querySelector('.range-btn')
+  if (state === 'start') {
+    parentNode.innerHTML = `
+      <span style="color: ${styleMap[state].thumbColor}" class="font_family ${styleMap[state].thumbIcon}"></span>
+  `
+  } else {
+    parentNode.innerHTML = `
+    <div class="icon-border" style="background: ${styleMap[state].trackBackground}">
+      <span style="color: ${styleMap[state].thumbColor}" class="font_family ${styleMap[state].thumbIcon}"></span>
+    </div>
+  `
+  }
+}
+
+const isShow = ref(false)
+
+// 根据不同的状态,设置已滑动区域的背景颜色
+const updateSliderBackground = (state: string) => {
+  const targetNode: any = document.querySelector('.range-slider')
+  console.log(targetNode, styleMap[state].trackBackground)
+  targetNode.style.background = styleMap[state].trackBackground
+}
+
+const emit = defineEmits<{
+  close: []
+}>()
+// 监听验证成功事件,因为这里库中的成功事件有0.8秒的延迟,所以这里手动监听验证成功事件
+const onSuccess = () => {
+  // 选择要监听的目标元素
+  const targetNode = document.querySelector('.info-box_') // 将此选择器替换为你的实际目标
+  if (targetNode) {
+    const observer = new MutationObserver((mutationsList) => {
+      mutationsList.forEach((mutation) => {
+        if (mutation.type === 'childList') {
+          if ((mutation.target as any).innerHTML === '验证通过!') {
+            updateSliderBackground('success')
+            addSliderBtnNode('success')
+            setTimeout(() => {
+              emit('close')
+              isShow.value = false
+            }, 500)
+          }
+        }
+      })
+    })
+
+    // 配置 MutationObserver,监听子节点的变化
+    const config = { childList: true, subtree: true }
+
+    // 启动监听
+    observer.observe(targetNode, config)
+  }
+}
+const close = () => {
+  isShow.value = true
+}
+const fail = () => {
+  updateSliderBackground('error')
+  addSliderBtnNode('error')
+}
+
+defineExpose({
+  openDialog
+})
+</script>
+<template>
+  <Vcode
+    :show="isShow"
+    @close="close"
+    @fail="fail"
+    :canvasWidth="320"
+    :canvasHeight="180"
+    sliderText="Swipe to verify"
+    :sliderSize="38"
+    :imgs="[Img01]"
+  ></Vcode>
+</template>
+<style lang="scss" scoped>
+.slider-verification {
+  width: 400px;
+  height: 373px;
+  padding: 40px;
+  .tips {
+    text-align: center;
+  }
+}
+</style>
+<style lang="scss">
+// 整体框架
+.vue-auth-box_ {
+  width: 400px;
+  height: 373px;
+  padding: 40px;
+  .tips {
+    margin-bottom: 16px;
+    text-align: center;
+  }
+  .icon-border {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 16px !important;
+    height: 16px !important;
+    border-radius: 50%;
+    border: none !important;
+    span {
+      font-size: 14px !important;
+    }
+  }
+}
+
+.vue-auth-box_ .auth-control_ div.range-box {
+  background-color: #87909e;
+  box-shadow: none;
+}
+.vue-auth-box_ div.auth-body_ {
+  border-radius: 6px;
+}
+// 已滑动区域的样式
+.vue-auth-box_ .auth-control_ .range-box div.range-slider {
+  background-color: var(--color-success);
+}
+
+.vue-auth-box_ .auth-body_ div.loading-box_.hide_ {
+  display: none;
+}
+
+// 隐藏成功的提示
+.vue-auth-box_ .auth-body_ div.info-box_ {
+  // display: none;
+  opacity: 0 !important;
+}
+</style>

+ 21 - 6
src/views/Login/src/loginView.vue

@@ -4,6 +4,7 @@ import ErrorTips from './components/ErrorTips.vue'
 import { useUserStore } from '@/stores/modules/user'
 import ScoringSystem from '@/views/Dashboard/src/components/ScoringSystem.vue'
 import CryptoJS from 'crypto-js'
+import SliderVerification from './components/SliderVerification.vue'
 
 const router = useRouter()
 const route = useRoute()
@@ -127,12 +128,26 @@ onMounted(() => {
 
 const userStore = useUserStore()
 
+const sliderVerificationRef = ref()
+const sliderVerificationFn = () => {
+  isShowSliderVerification.value = true
+  nextTick(() => {
+    sliderVerificationRef.value.openDialog()
+  })
+}
+const isShowSliderVerification = ref(false)
+
 // 点击登录按钮
 const handleLogin = () => {
   if (!isUserNameExit.value || !loginForm.value.username) {
     return
   }
+  sliderVerificationFn()
   // 这里是登录逻辑
+}
+// 验证结束后调用登录接口
+const handleLoginAfterVerify = () => {
+  isShowSliderVerification.value = false
   $api
     .login({
       uname: loginForm.value.username,
@@ -186,12 +201,8 @@ const handleLogin = () => {
             name: 'Reset Password'
           })
         }
-        getCode()
       }
     })
-    .catch(() => {
-      getCode()
-    })
 }
 
 // 从忘记密码返回登录
@@ -217,7 +228,7 @@ const handleForgot = () => {
   handleDeleteEmailTips()
 }
 const handleSendPassword = () => {
-  if (!isUserNameExit.value || !loginForm.value.username) {
+  if (!isUserNameExit.value || !loginForm.value.username || !loginForm.value.email) {
     return
   }
   // 这里是发送密码逻辑
@@ -432,7 +443,11 @@ const errorTipsRef = ref()
         </div>
       </template>
     </el-card>
-
+    <SliderVerification
+      v-if="isShowSliderVerification"
+      @close="handleLoginAfterVerify"
+      ref="sliderVerificationRef"
+    ></SliderVerification>
     <ErrorTips ref="errorTipsRef" @forget-password="status = 'reset'"></ErrorTips>
   </div>
 </template>