This commit is contained in:
Mrx
2026-02-26 17:12:29 +08:00
parent f882d04b5b
commit 74b0c0e918
5 changed files with 236 additions and 32 deletions

View File

@@ -83,6 +83,12 @@
delete window.wx; delete window.wx;
</script> </script>
<!-- 阿里云滑块验证码 -->
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
<script type="text/javascript" src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"></script>
<!-- 预加载关键资源 --> <!-- 预加载关键资源 -->
<link rel="preconnect" href="https://www.zhinengcha.cn"> <link rel="preconnect" href="https://www.zhinengcha.cn">
<link rel="preconnect" href="https://res.wx.qq.com"> <link rel="preconnect" href="https://res.wx.qq.com">
@@ -181,6 +187,8 @@
<div class="loading-text">加载中</div> <div class="loading-text">加载中</div>
</div> </div>
<div id="app"></div> <div id="app"></div>
<!-- 阿里云验证码挂载容器 -->
<div id="captcha-element"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

View File

@@ -119,6 +119,7 @@ declare global {
const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAgent: typeof import('./composables/useAgent.js')['useAgent'] const useAgent: typeof import('./composables/useAgent.js')['useAgent']
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore'] const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default']
const useAnimate: typeof import('@vueuse/core')['useAnimate'] const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default'] const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']

View File

@@ -282,6 +282,7 @@ import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv"; import { useEnv } from "@/composables/useEnv";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
import { showConfirmDialog } from "vant"; import { showConfirmDialog } from "vant";
import Payment from "@/components/Payment.vue"; import Payment from "@/components/Payment.vue";
@@ -289,6 +290,8 @@ import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import LoginDialog from "@/components/LoginDialog.vue"; import LoginDialog from "@/components/LoginDialog.vue";
import SectionTitle from "@/components/SectionTitle.vue"; import SectionTitle from "@/components/SectionTitle.vue";
const { runWithCaptcha } = useAliyunCaptcha();
// Props // Props
const props = defineProps({ const props = defineProps({
// 查询类型:'normal' | 'promotion' // 查询类型:'normal' | 'promotion'
@@ -631,11 +634,18 @@ async function sendVerificationCode() {
return; return;
} }
const { data, error } = await useApiFetch("/auth/sendSms") // 使用阿里云滑块验证码
.post({ mobile: formData.mobile, actionType: "query" }) runWithCaptcha(
.json(); (captchaVerifyParam) =>
useApiFetch("/auth/sendSms")
if (!error.value && data.value.code === 200) { .post({
mobile: formData.mobile,
actionType: "query",
captchaVerifyParam
})
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "验证码发送成功", type: "success" }); showToast({ message: "验证码发送成功", type: "success" });
startCountdown(); startCountdown();
nextTick(() => { nextTick(() => {
@@ -645,9 +655,11 @@ async function sendVerificationCode() {
} }
}); });
} else { } else {
showToast({ message: "验证码发送失败,请重试" }); showToast({ message: res.msg || "验证码发送失败,请重试" });
} }
} }
);
}
let timer = null; let timer = null;

View File

@@ -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;

View File

@@ -2,6 +2,9 @@
import { ref, computed, onUnmounted, nextTick } from 'vue' import { ref, computed, onUnmounted, nextTick } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import ClickCaptcha from '@/components/ClickCaptcha.vue' import ClickCaptcha from '@/components/ClickCaptcha.vue'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const { runWithCaptcha } = useAliyunCaptcha()
const router = useRouter() const router = useRouter()
const phoneNumber = ref('') const phoneNumber = ref('')
@@ -41,14 +44,22 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return 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) { // 使用阿里云滑块验证码
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch('auth/sendSms')
.post({
mobile: phoneNumber.value,
actionType: 'login',
captchaVerifyParam
})
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" }); showToast({ message: "获取成功" });
startCountdown() startCountdown();
// 聚焦到验证码输入框 // 聚焦到验证码输入框
nextTick(() => { nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode'); const verificationCodeInput = document.getElementById('verificationCode');
@@ -57,9 +68,10 @@ async function sendVerificationCode() {
} }
}); });
} else { } else {
showToast(data.value.msg) showToast(res.msg || "获取失败");
} }
} }
);
} }
function startCountdown() { function startCountdown() {