From 74b0c0e918b7a1deb3db31ad467cd3374f1bb4eb Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Thu, 26 Feb 2026 17:12:29 +0800 Subject: [PATCH] add --- index.html | 8 ++ src/auto-imports.d.ts | 1 + src/components/InquireForm.vue | 42 ++++--- src/composables/useAliyunCaptcha.js | 171 ++++++++++++++++++++++++++++ src/views/Login.vue | 46 +++++--- 5 files changed, 236 insertions(+), 32 deletions(-) create mode 100644 src/composables/useAliyunCaptcha.js diff --git a/index.html b/index.html index 72b9680..251bb17 100644 --- a/index.html +++ b/index.html @@ -82,6 +82,12 @@ window.jWeixin = window.wx; delete window.wx; + + + + @@ -181,6 +187,8 @@
加载中
+ +
diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 0c95b32..818e0f8 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -119,6 +119,7 @@ declare global { const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] const useAgent: typeof import('./composables/useAgent.js')['useAgent'] const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore'] + const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default'] const useAnimate: typeof import('@vueuse/core')['useAnimate'] const useApiFetch: typeof import('./composables/useApiFetch.js')['default'] const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] diff --git a/src/components/InquireForm.vue b/src/components/InquireForm.vue index 95c5232..6b66af9 100644 --- a/src/components/InquireForm.vue +++ b/src/components/InquireForm.vue @@ -282,6 +282,7 @@ import { useRoute, useRouter } from "vue-router"; import { useUserStore } from "@/stores/userStore"; import { useDialogStore } from "@/stores/dialogStore"; import { useEnv } from "@/composables/useEnv"; +import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; import { showConfirmDialog } from "vant"; import Payment from "@/components/Payment.vue"; @@ -289,6 +290,8 @@ import BindPhoneDialog from "@/components/BindPhoneDialog.vue"; import LoginDialog from "@/components/LoginDialog.vue"; import SectionTitle from "@/components/SectionTitle.vue"; +const { runWithCaptcha } = useAliyunCaptcha(); + // Props const props = defineProps({ // 查询类型:'normal' | 'promotion' @@ -631,22 +634,31 @@ async function sendVerificationCode() { return; } - const { data, error } = await useApiFetch("/auth/sendSms") - .post({ mobile: formData.mobile, actionType: "query" }) - .json(); - - if (!error.value && data.value.code === 200) { - showToast({ message: "验证码发送成功", type: "success" }); - startCountdown(); - nextTick(() => { - const verificationCodeInput = document.getElementById('verificationCode'); - if (verificationCodeInput) { - verificationCodeInput.focus(); + // 使用阿里云滑块验证码 + runWithCaptcha( + (captchaVerifyParam) => + useApiFetch("/auth/sendSms") + .post({ + mobile: formData.mobile, + actionType: "query", + captchaVerifyParam + }) + .json(), + (res) => { + if (res.code === 200) { + showToast({ message: "验证码发送成功", type: "success" }); + startCountdown(); + nextTick(() => { + const verificationCodeInput = document.getElementById('verificationCode'); + if (verificationCodeInput) { + verificationCodeInput.focus(); + } + }); + } else { + showToast({ message: res.msg || "验证码发送失败,请重试" }); } - }); - } else { - showToast({ message: "验证码发送失败,请重试" }); - } + } + ); } let timer = null; diff --git a/src/composables/useAliyunCaptcha.js b/src/composables/useAliyunCaptcha.js new file mode 100644 index 0000000..8ea0808 --- /dev/null +++ b/src/composables/useAliyunCaptcha.js @@ -0,0 +1,171 @@ +import { showToast, showLoadingToast, closeToast } from "vant"; +import useApiFetch from "@/composables/useApiFetch"; + +// 阿里云验证码场景 ID(请替换为您的实际场景ID) +const ALIYUN_CAPTCHA_SCENE_ID = "wynt39to"; +// 是否启用加密模式(通过环境变量控制,非加密模式时前端不调用后端获取 EncryptedSceneId) +const ENABLE_ENCRYPTED = false; + +let captchaInitialised = false; +/** 首次初始化后,SDK 会异步调用 getInstance,用此 Promise 在实例就绪后再 show */ +let captchaReadyPromise = null; +let captchaReadyResolve = null; + +async function ensureCaptchaInit() { + if (captchaInitialised || typeof window === "undefined") return; + if (typeof window.initAliyunCaptcha !== "function") return; + + captchaInitialised = true; + window.captcha = null; + window.__lastBizResponse = null; + window.__onCaptchaBizSuccess = null; + captchaReadyPromise = new Promise((resolve) => { + captchaReadyResolve = resolve; + }); + + // 非加密模式:仅传 SceneId,不调用后端接口 + if (!ENABLE_ENCRYPTED) { + window.initAliyunCaptcha({ + SceneId: ALIYUN_CAPTCHA_SCENE_ID, + mode: "popup", + element: "#captcha-element", + getInstance(instance) { + window.captcha = instance; + if (typeof captchaReadyResolve === "function") { + captchaReadyResolve(); + captchaReadyResolve = null; + } + }, + captchaVerifyCallback(param) { + return typeof window.__captchaVerifyCallback === "function" + ? window.__captchaVerifyCallback(param) + : Promise.resolve({ + captchaResult: false, + bizResult: false, + }); + }, + onBizResultCallback(bizResult) { + if (typeof window.__onBizResultCallback === "function") { + window.__onBizResultCallback(bizResult); + } + window.__lastBizResponse = null; + window.__onCaptchaBizSuccess = null; + }, + slideStyle: { width: 360, height: 40 }, + language: "cn", + }); + return; + } + + // 加密模式:先从后端获取 EncryptedSceneId,再初始化 + const { data, error } = await useApiFetch("/captcha/encryptedSceneId") + .post() + .json(); + const resp = data?.value; + const encryptedSceneId = resp?.data?.encryptedSceneId; + if (error?.value || !encryptedSceneId) { + showToast({ message: "获取验证码参数失败,请稍后重试" }); + captchaInitialised = false; + captchaReadyPromise = null; + captchaReadyResolve = null; + return; + } + window.initAliyunCaptcha({ + SceneId: ALIYUN_CAPTCHA_SCENE_ID, + EncryptedSceneId: encryptedSceneId, + mode: "popup", + element: "#captcha-element", + getInstance(instance) { + window.captcha = instance; + if (typeof captchaReadyResolve === "function") { + captchaReadyResolve(); + captchaReadyResolve = null; + } + }, + captchaVerifyCallback(param) { + return typeof window.__captchaVerifyCallback === "function" + ? window.__captchaVerifyCallback(param) + : Promise.resolve({ captchaResult: false, bizResult: false }); + }, + onBizResultCallback(bizResult) { + if (typeof window.__onBizResultCallback === "function") { + window.__onBizResultCallback(bizResult); + } + window.__lastBizResponse = null; + window.__onCaptchaBizSuccess = null; + }, + slideStyle: { width: 360, height: 40 }, + language: "cn", + }); +} + +/** + * 阿里云滑块验证码通用封装。 + * 依赖 index.html 中已加载的 AliyunCaptcha.js;初始化在首次调起时执行。 + * + * @param { (captchaVerifyParam: string) => Promise<{ data: Ref, error: Ref }> } bizVerify - 业务请求函数,接收滑块参数,返回 useApiFetch 的 { data, error } + * @param { (res: any) => void } [onSuccess] - 业务成功回调(code===200 时调用,传入接口返回的 data.value) + */ +export function useAliyunCaptcha() { + /** + * 先弹出滑块,通过后执行 bizVerify(captchaVerifyParam),再根据结果调用 onSuccess。 + */ + async function runWithCaptcha(bizVerify, onSuccess) { + if (typeof window === "undefined") { + showToast({ message: "验证码仅支持浏览器环境" }); + return; + } + + const loading = showLoadingToast({ + message: "安全验证加载中...", + forbidClick: true, + duration: 0, + loadingType: "spinner", + }); + + try { + window.__captchaVerifyCallback = async (captchaVerifyParam) => { + window.__lastBizResponse = null; + const { data, error } = await bizVerify(captchaVerifyParam); + const result = data?.value ?? data; + if (error?.value || !result) { + return { captchaResult: false, bizResult: false }; + } + window.__lastBizResponse = result; + const captchaOk = result.captchaVerifyResult !== false; + const bizOk = result.code === 200; + return { captchaResult: captchaOk, bizResult: bizOk }; + }; + + window.__onBizResultCallback = (bizResult) => { + if ( + bizResult === true && + window.__lastBizResponse && + typeof window.__onCaptchaBizSuccess === "function" + ) { + window.__onCaptchaBizSuccess(window.__lastBizResponse); + } + }; + + await ensureCaptchaInit(); + + // 首次初始化时 SDK 会异步调用 getInstance,需等待实例就绪后再 show + if (captchaReadyPromise) { + await captchaReadyPromise; + captchaReadyPromise = null; + } + if (!window.captcha) { + showToast({ message: "验证码未加载,请刷新页面重试" }); + return; + } + window.__onCaptchaBizSuccess = onSuccess; + window.captcha.show(); + } finally { + closeToast(); + } + } + + return { runWithCaptcha }; +} + +export default useAliyunCaptcha; diff --git a/src/views/Login.vue b/src/views/Login.vue index 4d1e9ab..53d56b7 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -2,6 +2,9 @@ import { ref, computed, onUnmounted, nextTick } from 'vue' import { showToast } from 'vant' import ClickCaptcha from '@/components/ClickCaptcha.vue' +import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha' + +const { runWithCaptcha } = useAliyunCaptcha() const router = useRouter() const phoneNumber = ref('') @@ -41,25 +44,34 @@ async function sendVerificationCode() { showToast({ message: "请输入有效的手机号" }); return } - const { data, error } = await useApiFetch('auth/sendSms') - .post({ mobile: phoneNumber.value, actionType: 'login' }) - .json() - if (data.value && !error.value) { - if (data.value.code === 200) { - showToast({ message: "获取成功" }); - startCountdown() - // 聚焦到验证码输入框 - nextTick(() => { - const verificationCodeInput = document.getElementById('verificationCode'); - if (verificationCodeInput) { - verificationCodeInput.focus(); - } - }); - } else { - showToast(data.value.msg) + + // 使用阿里云滑块验证码 + runWithCaptcha( + (captchaVerifyParam) => + useApiFetch('auth/sendSms') + .post({ + mobile: phoneNumber.value, + actionType: 'login', + captchaVerifyParam + }) + .json(), + (res) => { + if (res.code === 200) { + showToast({ message: "获取成功" }); + startCountdown(); + // 聚焦到验证码输入框 + nextTick(() => { + const verificationCodeInput = document.getElementById('verificationCode'); + if (verificationCodeInput) { + verificationCodeInput.focus(); + } + }); + } else { + showToast(res.msg || "获取失败"); + } } - } + ); } function startCountdown() {