Bläddra i källkod

feat: 新增滑块验证码组件相关调用

cuiyalong 5 månader sedan
förälder
incheckning
d292ec6bae

+ 10 - 9
plugins/login-auth/package.json

@@ -1,18 +1,18 @@
 {
   "name": "@jy/plugin-login-auth",
-  "version": "1.0.5",
-  "files": [
-    "dist"
-  ],
-  "main": "./dist/plugin-login-auth.umd.js",
-  "module": "./dist/plugin-login-auth.mjs",
+  "version": "1.0.6",
+  "private": true,
   "exports": {
     ".": {
       "import": "./dist/plugin-login-auth.mjs",
       "require": "./dist/plugin-login-auth.umd.js"
     }
   },
-  "private": true,
+  "main": "./dist/plugin-login-auth.umd.js",
+  "module": "./dist/plugin-login-auth.mjs",
+  "files": [
+    "dist"
+  ],
   "scripts": {
     "dev": "vite",
     "build": "npm run build:external & npm run build:internal",
@@ -24,16 +24,17 @@
     "format": "prettier --write --cache ."
   },
   "dependencies": {
+    "@jy/emiiter": "workspace:^",
     "@vueuse/core": "^9.13.0",
     "@vueuse/integrations": "^9.13.0",
     "axios": "^1.3.5",
     "core-js": "^3.29.1",
     "element-ui": "^2.15.23-rc",
+    "go-captcha-vue": "^1",
     "lodash": "^4.17.21",
     "postcss-prefix-selector": "^1.16.0",
     "qs": "^6.11.2",
-    "vue": "^2.7.14",
-    "@jy/emiiter": "workspace:^"
+    "vue": "^2.7.14"
   },
   "devDependencies": {
     "@antfu/eslint-config": "^0.38.2",

+ 17 - 2
plugins/login-auth/src/api/login.js

@@ -1,5 +1,5 @@
-import request from './service'
 import qs from 'qs'
+import request from './service'
 
 export function ajaxSetLogin(data) {
   return request({
@@ -42,7 +42,7 @@ export function ajaxGetLoginPolling(data) {
 
 export function ajaxGetLoginNum(id, data) {
   return request({
-    url: '/front/getLoginNum/' + id,
+    url: `/front/getLoginNum/${id}`,
     method: 'post',
     data: qs.stringify(data)
   })
@@ -61,3 +61,18 @@ export function ajaxSetSignOut() {
     method: 'post'
   })
 }
+
+export function ajaxGetSMSCode(data) {
+  return request({
+    url: '/publicapply/captcha/get',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
+export function ajaxCheckCaptchaCode(data) {
+  return request({
+    url: '/publicapply/captcha/check',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}

+ 2 - 0
plugins/login-auth/src/components.d.ts

@@ -10,11 +10,13 @@ declare module 'vue' {
     AutoCompleteInput: typeof import('./components/form/autoCompleteInput.vue')['default']
     BaseForm: typeof import('./components/form/baseForm.vue')['default']
     BaseInput: typeof import('./components/form/baseInput.vue')['default']
+    CaptchaDialog: typeof import('./components/dialog/captcha-dialog.vue')['default']
     ImgCaptchaInput: typeof import('./components/form/imgCaptchaInput.vue')['default']
     PassInput: typeof import('./components/form/passInput.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SmsCaptchaInput: typeof import('./components/form/smsCaptchaInput.vue')['default']
+    Teleport: typeof import('./components/teleport/index.vue')['default']
     Toast: typeof import('./components/toast/Toast.vue')['default']
   }
 }

+ 124 - 0
plugins/login-auth/src/components/dialog/async-captcha.js

@@ -0,0 +1,124 @@
+import Vue, { ref } from 'vue'
+import CaptchaDialog from './captcha-dialog.vue'
+
+const CaptchaDialogConstructor = Vue.extend(CaptchaDialog)
+
+export class Captcha {
+  vm = null // 保存实例
+  // 实例
+  get $vm() {
+    if (this.vm) {
+      return this.vm
+    } else {
+      return this.install()
+    }
+  }
+
+  // 显示隐藏的状态
+  get show() {
+    if (!this.vm) {
+      return false
+    } else {
+      return this.$vm?.value || false
+    }
+  }
+
+  defaultConf = {
+    noop: () => {},
+    getContainer: '.login-auth--form'
+  }
+
+  conf = {}
+
+  constructor(conf = {}) {
+    Object.assign(this.conf, this.defaultConf, conf)
+  }
+
+  install() {
+    if (this.vm) {
+      return this.vm
+    }
+    this.vm = new CaptchaDialogConstructor()
+    this.vm.$mount()
+    this.vm.$on('input', this.onInput.bind(this))
+    document.querySelector(this.conf.getContainer).appendChild(this.$vm.$el)
+    return this.vm
+  }
+
+  verify(conf = {}) {
+    if (!this.vm) {
+      this.install()
+    }
+
+    const vm = this.$vm
+    const { noop } = this.conf
+    const {
+      value = false,
+      captchaData = {},
+      refresh = noop,
+      confirm = noop
+    } = conf
+
+    this.refreshConfig(captchaData)
+
+    // 移除所有事件监听
+    vm.$off('refresh')
+    vm.$off('confirm')
+
+    if (value) {
+      vm.$on('refresh', refresh.bind(this))
+      vm.$on('confirm', confirm.bind(this))
+    }
+    // 显示或隐藏
+    vm.value = value
+  }
+
+  refreshConfig(captchaData) {
+    const vm = this.$vm
+    Object.assign(vm.slideData, captchaData)
+    if (vm.slideData && vm.slideData.captKey) {
+      vm.$forceUpdate()
+    }
+  }
+
+  onInput(value) {
+    this.verify({ value })
+  }
+
+  trigger(name) {
+    const vm = this.$vm
+    vm.$emit(name)
+  }
+}
+
+export default function showAsyncCaptchaDialog(options = {}) {
+  const { captchaData, refresh, confirm } = options
+
+  return new Promise((resolve) => {
+    const instance = new Captcha()
+    instance.verify({
+      value: true,
+      captchaData,
+      async refresh() {
+        if (refresh) {
+          const result = await refresh()
+          if (result) {
+            instance.refreshConfig(result)
+          }
+        }
+      },
+      async confirm({ point, reset }) {
+        if (confirm) {
+          const pass = await confirm({ point })
+          if (pass) {
+            instance.onInput(false)
+            resolve()
+          } else {
+            reset()
+            instance.trigger('refresh')
+          }
+        }
+      }
+    })
+  })
+}

+ 122 - 0
plugins/login-auth/src/components/dialog/captcha-dialog.vue

@@ -0,0 +1,122 @@
+<script setup lang="ts">
+// http://gocaptcha.wencodes.com/package/vue/
+import { ref } from 'vue'
+import { Slide } from 'go-captcha-vue'
+// import type { Config, ExportMethods, SlideData, SlideEvents, SlidePoint } from 'go-captcha-vue'
+import Teleport from '@/components/teleport/index.vue'
+import 'go-captcha-vue/dist/style.css'
+
+// // data = {}
+// interface SlideData {
+//   thumbX: number;
+//   thumbY: number;
+//   thumbWidth: number;
+//   thumbHeight: number;
+//   image: string;
+//   thumb: string;
+// }
+
+// // export component method
+// interface ExportMethods {
+//   reset: () => void,
+//   clear: () => void,
+//   refresh: () => void,
+//   close: () => void,
+// }
+
+const props = defineProps({
+  value: {
+    type: Boolean,
+    default: false
+  },
+  showMask: {
+    type: Boolean,
+    default: true
+  },
+  slideConfig: {
+    type: Object,
+    default: () => ({})
+  },
+  slideData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const emit = defineEmits(['input', 'refresh', 'confirm'])
+const captcha = ref(null)
+const captchaSlide = ref(null)
+
+const updateVisible = function (e: boolean) {
+  emit('input', e)
+}
+const captchaRefresh = function () {
+  emit('refresh')
+}
+
+const slideEvents = {
+  refresh() {
+    captchaRefresh()
+  },
+  close() {
+    updateVisible(false)
+  },
+  confirm(point: object, reset: () => void) {
+    emit('confirm', {
+      point,
+      reset
+    })
+  }
+}
+</script>
+
+<template>
+  <div ref="captcha" class="login-captcha-dialog-wrapper">
+    <Teleport v-if="props.value" to=".plugin-login-auth-container">
+      <div class="login-captcha-group">
+        <div v-if="props.showMask" class="login-captcha-mask" />
+        <div class="login-captcha-container">
+          <Slide
+            ref="captchaSlide"
+            :key="props.slideData.captKey"
+            :config="props.slideConfig"
+            :data="props.slideData"
+            :events="slideEvents"
+          />
+        </div>
+      </div>
+    </Teleport>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@keyframes fadeIn {
+  to {
+    /* 动画结束时,遮罩完全显示,透明度为 1 */
+    opacity: 1;
+    pointer-events: auto;
+  }
+}
+
+.login-captcha-mask {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  z-index: 99999;
+  opacity: 0;
+  /* 禁用指针事件,避免影响下层元素操作 */
+  pointer-events: none;
+  /* 定义动画,名称为 fadeIn,持续时间 0.5 秒,使用 ease 缓动函数 */
+  animation: fadeIn 0.5s ease forwards;
+}
+.login-captcha-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 999999;
+}
+</style>

+ 1 - 0
plugins/login-auth/src/components/form/smsCaptchaInput.vue

@@ -38,6 +38,7 @@ async function doGetSms() {
   }
 }
 </script>
+
 <template>
   <div class="login-auth--form-after" :class="{ disable: !canDoSetSms }">
     <span v-if="!countDown.isCounting.value" @click="doGetSms">获取验证码</span>

+ 84 - 0
plugins/login-auth/src/components/teleport/index.vue

@@ -0,0 +1,84 @@
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'Teleport',
+  abstract: true,
+  props: {
+    to: {
+      type: [String, HTMLElement],
+      required: true
+    },
+    disabled: Boolean
+  },
+  data() {
+    const nonReactive = {
+      teleported: false,
+      lastTo: '',
+      rootEl: undefined,
+      originalEl: undefined,
+      dumb: document.createComment(' teleport ')
+    } as {
+      teleported: boolean
+      lastTo: string | HTMLElement
+      rootEl?: Node
+      originalEl?: Node
+      dumb: Node
+    }
+    Object.preventExtensions(nonReactive)
+    return nonReactive
+  },
+  mounted() {
+    this.onEnter()
+  },
+  updated() {
+    this.onEnter() // 除了 to、disabled 外,rootEl 以及 originalEl 也可能发生变化
+  },
+  beforeDestroy() {
+    this.dumb.parentNode?.removeChild(this.dumb)
+    this.rootEl?.parentNode?.removeChild(this.rootEl)
+  },
+  methods: {
+    onEnter() {
+      // @ts-ignore
+      this.rootEl = this._vnode.elm
+      // @ts-ignore
+      this.originalEl = this.teleported
+        ? this.dumb?.parentNode
+        : this.rootEl?.parentNode
+
+      this.disabled ? this.restore() : this.telport()
+    },
+    restore() {
+      if (!this.teleported) return
+      this.$vnode.elm = this.rootEl
+      this.teleported = false
+      this.lastTo = ''
+
+      if (!this.originalEl) return
+      this.rootEl && this.originalEl.insertBefore(this.rootEl, this.dumb)
+      this.dumb.parentNode?.removeChild(this.dumb)
+    },
+    telport() {
+      if (!this.rootEl || !this.originalEl) return
+      if (!this.teleported) this.originalEl.insertBefore(this.dumb, this.rootEl)
+      this.$vnode.elm = this.dumb
+      this.teleported = true
+
+      let targetEl
+      if (this.to !== this.lastTo)
+        targetEl =
+          typeof this.to === 'string'
+            ? document.querySelector(this.to)
+            : this.to
+      if (!targetEl) return
+      targetEl.appendChild(this.rootEl)
+      this.lastTo = this.to
+    }
+  },
+  render(h) {
+    const first = this.$slots.default?.[0]
+    return first
+  }
+})
+</script>

+ 110 - 0
plugins/login-auth/src/utils/useSmsVerify.js

@@ -0,0 +1,110 @@
+import showAsyncCaptchaDialog from '../components/dialog/async-captcha'
+import { ajaxCheckCaptchaCode, ajaxGetSMSCode } from '@/api'
+import { useToast } from '@/utils/use'
+
+export async function getSMSVerifyCaptcha(phone, mold) {
+  const result = await ajaxGetSMSCode({ phone, mold })
+  // Object.assign(result.data.data, {
+  //   captcha_key: '2dbeb6c6db961938419408483bca30dc',
+  //   image_base64: 'data:image/jpeg;base64,/9j/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEI==',
+  //   tile_base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD8AAAA/CAYAAABXXxDfAAAWSElEQVR4nMx7W49kZ9Xe8x72qar6PNMznhnP2GPjzx4Ptj+fwP4iAsEJWMEBCyJAuUguo/wCbnKfm1zkKj8CRYqEiBAoyICxgwW+IDEk8Ql77PF4jl2HfXqPn9bau5pJggg==',
+  //   tile_width: 63,
+  //   tile_height: 63,
+  //   tile_x: 24,
+  //   tile_y: 24
+  // })
+  return result.data
+}
+
+function getCaptchaData(x = {}) {
+  return {
+    image: x.image_base64 || '',
+    thumb: x.tile_base64 || '',
+    captKey: x.captcha_key || '',
+    thumbX: x.tile_x || 0,
+    thumbY: x.tile_y || 0,
+    thumbWidth: x.tile_width || 0,
+    thumbHeight: x.tile_height || 0
+  }
+}
+
+async function refreshCaptcha(phone, mold) {
+  const { error_code: code, data } = await getSMSVerifyCaptcha(phone, mold)
+  if (code === 0 && data) {
+    if (data.code === 1) {
+      return getCaptchaData(data)
+    } else {
+      return false
+    }
+  } else {
+    return false
+  }
+}
+
+async function checkVerify(params = {}) {
+  const { data: axiosData } = await ajaxCheckCaptchaCode(params)
+  const { error_code: code, error_msg: msg, data } = axiosData
+  if (code === 0 && data && data.code === 0) {
+    return true
+  } else {
+    if (msg) {
+      useToast(msg)
+    }
+    return false
+  }
+}
+
+export async function useSMSVerify(options = {}) {
+  // /publicapply/captcha/get:mold:1:登录;2:注册;3:找回密码
+  const { phone, mold } = options
+  const captchaInfo = {}
+
+  const {
+    error_code: code,
+    error_msg: msg,
+    data
+  } = await getSMSVerifyCaptcha(phone, mold)
+  if (code === 0 && data) {
+    if (data.code === 0) {
+      return {
+        pass: true
+      }
+    } else if (data.code === 1) {
+      // 初始化值
+      const captchaData = getCaptchaData(data)
+      Object.assign(captchaInfo, captchaData)
+      await showAsyncCaptchaDialog({
+        captchaData,
+        async refresh() {
+          const refresh = await refreshCaptcha(phone, mold)
+          if (refresh) {
+            Object.assign(captchaInfo, refresh)
+          }
+          return refresh
+        },
+        async confirm({ point }) {
+          const pass = await checkVerify({
+            phone,
+            key: captchaInfo.captKey,
+            point: `${point.x},${point.y}`
+          })
+          return pass
+        }
+      })
+
+      return {
+        pass: true,
+        captKey: captchaInfo.captKey,
+        phone
+      }
+    }
+  } else {
+    if (msg) {
+      useToast(msg)
+    }
+  }
+
+  return {
+    pass: false
+  }
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 229 - 399
pnpm-lock.yaml


Vissa filer visades inte eftersom för många filer har ändrats