lianbingjie hai 5 meses
pai
achega
670f6f89fd

+ 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>

+ 6 - 2
plugins/login-auth/src/components/toast/Toast.vue

@@ -1,5 +1,9 @@
 <template>
-  <div v-if="showWrap" class="jy-toast" :class="showContent ? 'fadein' : 'fadeout'">
+  <div
+    v-if="showWrap"
+    class="jy-toast"
+    :class="showContent ? 'fadein' : 'fadeout'"
+  >
     {{ text }}
   </div>
 </template>
@@ -15,7 +19,7 @@
   transform: translate(-50%, -50%);
   color: #fff;
   font-size: 16px;
-  z-index: 9999;
+  z-index: 99999;
 }
 .fadein {
   animation: animate_in 0.25s;

+ 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
+  }
+}

+ 41 - 73
plugins/login-auth/src/views/form/login.vue

@@ -1,24 +1,23 @@
 <script setup lang="ts">
 import { some } from 'lodash'
-import { ajaxSetLogin } from '@/api'
 import { computed, ref } from 'vue'
-import { tabActive, doChangeTabActive } from '@/module-model/tab'
+import { ajaxSetLogin } from '@/api'
+import { doChangeTabActive, tabActive } from '@/module-model/tab'
 import { autoLoginState, doSelectAutoLogin } from '@/module-model/autoLogin'
 import { FormRules } from '@/data/constant'
 import { trackReport } from '@/utils/common'
 import { useToast } from '@/utils/use'
 import pluginLogin from '@/lib/pluginLogin'
+import { useSMSVerify } from '@/utils/useSmsVerify'
 
 // 埋点上报
 
 function track() {
   const href = window.location.href
   let sourceStr = ''
-  if (href.indexOf('/front/structed/pc_index.html?source=baidusem') > -1) {
+  if (href.includes('/front/structed/pc_index.html?source=baidusem')) {
     sourceStr = '结构化数据-pc-baidusem'
-  } else if (
-    href.indexOf('/bid/pc/page/bidfile_landpage?source=baidusem') > -1
-  ) {
+  } else if (href.includes('/bid/pc/page/bidfile_landpage?source=baidusem')) {
     sourceStr = '招标文件解读-pc-baidusem'
   }
 
@@ -41,11 +40,12 @@ const loginCodeFormState = ref({
     value: '',
     validate: false
   },
-  imgCaptcha: {
-    value: '',
-    validate: false
-  },
+  // imgCaptcha: {
+  //   value: '',
+  //   validate: true
+  // },
   smsCaptcha: {
+    captKey: '',
     value: '',
     validate: false
   }
@@ -76,6 +76,8 @@ const loginCodeForm = {
     const params = {
       reqType: 'identCodeLogin',
       identCode: loginCodeFormState.value.smsCaptcha.value,
+      phone: loginCodeFormState.value.phone.value,
+      captchaKey: loginCodeFormState.value.smsCaptcha.captKey,
       isAutoLogin: autoLoginState.value,
       source: urlParams.source
     }
@@ -126,19 +128,19 @@ const loginCodeFormSchema = {
         maxlength: 11
       }
     },
-    {
-      key: 'imgCaptcha',
-      type: 'imgCaptcha',
-      icon: 'icon-guard',
-      message: '图形验证码输入错误',
-      rule: FormRules.imgCode,
-      inputAttr: {
-        name: 'verify_code',
-        type: 'text',
-        placeholder: '输入图形验证码',
-        maxlength: 4
-      }
-    },
+    // {
+    //   key: 'imgCaptcha',
+    //   type: 'imgCaptcha',
+    //   icon: 'icon-guard',
+    //   message: '图形验证码输入错误',
+    //   rule: FormRules.imgCode,
+    //   inputAttr: {
+    //     name: 'verify_code',
+    //     type: 'text',
+    //     placeholder: '输入图形验证码',
+    //     maxlength: 4
+    //   }
+    // },
     {
       key: 'smsCaptcha',
       type: 'smsCaptcha',
@@ -153,58 +155,22 @@ const loginCodeFormSchema = {
       },
       expands: {
         preCheckState: computed(() => {
-          const result =
-            loginCodeFormState.value.phone.validate &&
-            loginCodeFormState.value.imgCaptcha.validate
+          const result = loginCodeFormState.value.phone.validate
           return result
         })
       },
       actions: {
-        preSubmit: async function () {
+        async preSubmit() {
           trackReport('huiju', 'c_register', {
             c_platform: 'pc',
             c_type: '注册行为-验证码登录/注册-获取验证码',
             date: new Date()
           })
 
-          const params = {
-            reqType: 'sendIdentCode',
-            phone: loginCodeFormState.value.phone.value,
-            code: loginCodeFormState.value.imgCaptcha.value
-          }
-          const result = await ajaxSetLogin(params)
-            .then(({ data }) => {
-              const type = data.status
-              if (type < 0) {
-                const errorMaps = {
-                  '-1': {
-                    key: 0,
-                    message: '手机号格式错误'
-                  },
-                  '-2': {
-                    key: 1,
-                    message: '图形验证码输入错误'
-                  },
-                  '-3': {
-                    key: 0,
-                    message: '手机号已被注册'
-                  }
-                }
-                loginCodeFormNode.value.showError(
-                  errorMaps[type].key,
-                  errorMaps[type].message
-                )
-                if (type === -2 || type === -3) {
-                  loginCodeFormNode.value.doRefreshCaptcha()
-                }
-                return false
-              }
-              return true
-            })
-            .catch(() => {
-              useToast('出现错误,请稍后重试')
-            })
-          return result
+          const phone = loginCodeFormState.value.phone.value
+          const { pass, captKey } = await useSMSVerify({ phone, mold: 1 })
+          loginCodeFormState.value.smsCaptcha.captKey = captKey
+          return pass
         }
       }
     }
@@ -308,6 +274,7 @@ const loginPassFormSchema = {
   ]
 }
 </script>
+
 <template>
   <!--  表单  -->
   <div class="login-auth--form">
@@ -335,15 +302,16 @@ const loginPassFormSchema = {
       </div>
       <!-- 验证码登录 -->
       <div
-        class="login-auth--switch-tab-content"
         v-if="tabActive === 'login-code'"
+        class="login-auth--switch-tab-content"
+        :class="{ mt40: tabActive === 'login-code' }"
       >
         <base-form
           ref="loginCodeFormNode"
-          @submit="loginCodeForm.doSubmit"
-          :schema="loginCodeFormSchema.fields"
           v-model="loginCodeFormState"
-        ></base-form>
+          :schema="loginCodeFormSchema.fields"
+          @submit="loginCodeForm.doSubmit"
+        />
         <button
           class="login-auth--submit-button"
           name="verify_submit"
@@ -356,16 +324,16 @@ const loginPassFormSchema = {
 
       <!-- 密码登录 -->
       <div
+        v-if="tabActive === 'login-pass'"
         class="login-auth--switch-tab-content"
         :class="{ mt40: tabActive === 'login-pass' }"
-        v-if="tabActive === 'login-pass'"
       >
         <base-form
           ref="loginPassFormNode"
-          @submit="loginPassForm.doSubmit"
-          :schema="loginPassFormSchema.fields"
           v-model="loginPassFormState"
-        ></base-form>
+          :schema="loginPassFormSchema.fields"
+          @submit="loginPassForm.doSubmit"
+        />
         <button
           class="login-auth--submit-button"
           name="verify_submit"

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 330 - 128
pnpm-lock.yaml


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio