diff --git a/index.html b/index.html index 5dc6ab3..71b7364 100644 --- a/index.html +++ b/index.html @@ -181,6 +181,14 @@
加载中
+
+ + diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 3a7330e..b965e9a 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -117,6 +117,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/AgentApplicationForm.vue b/src/components/AgentApplicationForm.vue index ded74b0..68563d6 100644 --- a/src/components/AgentApplicationForm.vue +++ b/src/components/AgentApplicationForm.vue @@ -122,6 +122,7 @@ const router = useRouter(); const show = defineModel("show"); import { useCascaderAreaData } from "@vant/area-data"; import { showToast } from "vant"; // 引入 showToast 方法 +import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; const emit = defineEmits(); // 确保 emit 可以正确使用 const props = defineProps({ ancestor: { @@ -158,6 +159,8 @@ const isPhoneNumberValid = computed(() => { return /^1[3-9]\d{9}$/.test(form.value.mobile); }); +const { runWithCaptcha } = useAliyunCaptcha(); + const getSmsCode = async () => { if (!form.value.mobile) { showToast({ message: "请输入手机号" }); @@ -170,21 +173,21 @@ const getSmsCode = async () => { } loadingSms.value = true; - - const { data, error } = await useApiFetch("auth/sendSms") - .post({ mobile: form.value.mobile, actionType: "agentApply" }) - .json(); - - loadingSms.value = false; - - if (data.value && !error.value) { - if (data.value.code === 200) { - showToast({ message: "获取成功" }); - startCountdown(); // 启动倒计时 - } else { - showToast(data.value.msg); + await runWithCaptcha( + (captchaVerifyParam) => + useApiFetch("auth/sendSms") + .post({ mobile: form.value.mobile, actionType: "agentApply", captchaVerifyParam }) + .json(), + (res) => { + loadingSms.value = false; + if (res.code === 200) { + showToast({ message: "获取成功" }); + startCountdown(); + } else { + showToast(res.msg); + } } - } + ); }; let timer = null; diff --git a/src/components/InquireForm.vue b/src/components/InquireForm.vue index 29c6f99..08aee3a 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"; @@ -351,6 +352,7 @@ const router = useRouter(); const dialogStore = useDialogStore(); const userStore = useUserStore(); const { isWeChat } = useEnv(); +const { runWithCaptcha } = useAliyunCaptcha(); // 响应式数据 const showPayment = ref(false); @@ -630,23 +632,26 @@ async function sendVerificationCode() { showToast({ message: "请输入有效的手机号" }); 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(); + await 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; @@ -776,4 +781,4 @@ button:active { border-radius: 50%; margin-right: 8px; } - \ No newline at end of file + diff --git a/src/components/LoginDialog.vue b/src/components/LoginDialog.vue index 171879f..2e2affb 100644 --- a/src/components/LoginDialog.vue +++ b/src/components/LoginDialog.vue @@ -5,6 +5,7 @@ import { showToast } from 'vant' import ClickCaptcha from '@/components/ClickCaptcha.vue' import { useDialogStore } from '@/stores/dialogStore' import { useUserStore } from '@/stores/userStore' +import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; const emit = defineEmits(['login-success']) const dialogStore = useDialogStore() @@ -19,6 +20,8 @@ const isCountingDown = ref(false) const countdown = ref(60) let timer = null +const { runWithCaptcha } = useAliyunCaptcha(); + // 验证组件状态 const showCaptcha = ref(false) const captchaVerified = ref(false) @@ -47,25 +50,26 @@ 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) + await 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({ message: res.msg || "发送失败" }); + } } - } + ); } function startCountdown() { diff --git a/src/composables/useAliyunCaptcha.js b/src/composables/useAliyunCaptcha.js new file mode 100644 index 0000000..c10554c --- /dev/null +++ b/src/composables/useAliyunCaptcha.js @@ -0,0 +1,153 @@ +import { showToast, showLoadingToast, closeToast } from "vant"; +import useApiFetch from "@/composables/useApiFetch"; + +const ALIYUN_CAPTCHA_SCENE_ID = "wynt39to"; +const ENABLE_ENCRYPTED = false; + +let captchaInitialised = false; +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; + }); + + 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; + } + + 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", + }); +} + +export function useAliyunCaptcha() { + 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(); + + 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 45654ed..66cf5a9 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -2,6 +2,7 @@ import { ref, computed, onUnmounted, nextTick } from 'vue' import { showToast } from 'vant' import ClickCaptcha from '@/components/ClickCaptcha.vue' +import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; const router = useRouter() const phoneNumber = ref('') @@ -13,6 +14,8 @@ const isCountingDown = ref(false) const countdown = ref(60) let timer = null +const { runWithCaptcha } = useAliyunCaptcha(); + // 验证组件状态 const showCaptcha = ref(false) const captchaVerified = ref(false) @@ -41,25 +44,26 @@ 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) + await 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({ message: res.msg || "发送失败" }); + } } - } + ); } function startCountdown() { diff --git a/vite.config.js b/vite.config.js index d0ebfb0..4cf83c2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -15,8 +15,8 @@ export default defineConfig({ strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口 proxy: { "/api/v1": { - // target: "http://127.0.0.1:8888", // 本地接口地址 - target: "https://www.tianyuandb.com", // 本地接口地址 + target: "http://127.0.0.1:8888", // 本地接口地址 + // target: "https://www.tianyuandb.com", // 本地接口地址 changeOrigin: true, }, },