This commit is contained in:
Mrx
2026-02-27 12:53:33 +08:00
parent 6fb4800283
commit ca32d17eb3
9 changed files with 311 additions and 102 deletions

View File

@@ -176,11 +176,19 @@
</head> </head>
<body> <body>
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
<script
type="text/javascript"
src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
></script>
<div id="app-loading"> <div id="app-loading">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<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

@@ -117,6 +117,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

@@ -122,6 +122,7 @@ const router = useRouter();
const show = defineModel("show"); const show = defineModel("show");
import { useCascaderAreaData } from "@vant/area-data"; import { useCascaderAreaData } from "@vant/area-data";
import { showToast } from "vant"; // 引入 showToast 方法 import { showToast } from "vant"; // 引入 showToast 方法
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(); // 确保 emit 可以正确使用 const emit = defineEmits(); // 确保 emit 可以正确使用
const props = defineProps({ const props = defineProps({
ancestor: { ancestor: {
@@ -150,6 +151,7 @@ const loadingSms = ref(false); // 控制验证码按钮的loading状态
const isCountingDown = ref(false); const isCountingDown = ref(false);
const isAgreed = ref(false); const isAgreed = ref(false);
const countdown = ref(60); const countdown = ref(60);
const { runWithCaptcha } = useAliyunCaptcha();
const onFinish = ({ selectedOptions }) => { const onFinish = ({ selectedOptions }) => {
showCascader.value = false; showCascader.value = false;
form.value.region = selectedOptions.map((option) => option.text).join("/"); form.value.region = selectedOptions.map((option) => option.text).join("/");
@@ -171,20 +173,25 @@ const getSmsCode = async () => {
loadingSms.value = true; loadingSms.value = true;
const { data, error } = await useApiFetch("auth/sendSms") await runWithCaptcha(
.post({ mobile: form.value.mobile, actionType: "agentApply" }) (captchaVerifyParam) =>
.json(); useApiFetch("auth/sendSms")
.post({
loadingSms.value = false; mobile: form.value.mobile,
actionType: "agentApply",
if (data.value && !error.value) { captchaVerifyParam,
if (data.value.code === 200) { })
showToast({ message: "获取成功" }); .json(),
startCountdown(); // 启动倒计时 (res) => {
} else { loadingSms.value = false;
showToast(data.value.msg); if (res.code === 200) {
} showToast({ message: "获取成功" });
} startCountdown();
} else {
showToast(res.msg || "获取失败");
}
},
);
}; };
let timer = null; let timer = null;

View File

@@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, computed, nextTick } from "vue"; import { ref, computed, nextTick } from "vue";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(['bind-success']) const emit = defineEmits(['bind-success'])
const router = useRouter(); const router = useRouter();
@@ -13,6 +14,7 @@ const isCountingDown = ref(false);
const countdown = ref(60); const countdown = ref(60);
const isAgreed = ref(false); const isAgreed = ref(false);
let timer = null; let timer = null;
const { runWithCaptcha } = useAliyunCaptcha();
// 聚焦状态变量 // 聚焦状态变量
const phoneFocused = ref(false); const phoneFocused = ref(false);
@@ -36,25 +38,30 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
} }
const { data, error } = await useApiFetch("auth/sendSms") await runWithCaptcha(
.post({ mobile: phoneNumber.value, actionType: "bindMobile" }) (captchaVerifyParam) =>
.json(); useApiFetch("auth/sendSms")
.post({
if (data.value && !error.value) { mobile: phoneNumber.value,
if (data.value.code === 200) { actionType: "bindMobile",
showToast({ message: "获取成功" }); captchaVerifyParam,
startCountdown(); })
// 聚焦到验证码输入框 .json(),
nextTick(() => { (res) => {
const verificationCodeInput = document.getElementById('verificationCode'); if (res.code === 200) {
if (verificationCodeInput) { showToast({ message: "获取成功" });
verificationCodeInput.focus(); startCountdown();
} nextTick(() => {
}); const verificationCodeInput = document.getElementById('verificationCode');
} else { if (verificationCodeInput) {
showToast(data.value.msg); verificationCodeInput.focus();
} }
} });
} else {
showToast(res.msg || "获取失败");
}
},
);
} }
function startCountdown() { function startCountdown() {

View File

@@ -283,6 +283,7 @@ 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 { showConfirmDialog } from "vant"; import { showConfirmDialog } from "vant";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
import Payment from "@/components/Payment.vue"; import Payment from "@/components/Payment.vue";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue"; import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
@@ -316,6 +317,7 @@ const { feature } = toRefs(props);
// Emits // Emits
const emit = defineEmits(['submit-success']); const emit = defineEmits(['submit-success']);
const { runWithCaptcha } = useAliyunCaptcha();
// 动态导入产品背景图片的函数 // 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => { const loadProductBackground = async (productType) => {
@@ -630,23 +632,30 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
} }
await runWithCaptcha(
const { data, error } = await useApiFetch("/auth/sendSms") (captchaVerifyParam) =>
.post({ mobile: formData.mobile, actionType: "query" }) useApiFetch("/auth/sendSms")
.json(); .post({
mobile: formData.mobile,
if (!error.value && data.value.code === 200) { actionType: "query",
showToast({ message: "验证码发送成功", type: "success" }); captchaVerifyParam,
startCountdown(); })
nextTick(() => { .json(),
const verificationCodeInput = document.getElementById('verificationCode'); (res) => {
if (verificationCodeInput) { if (res.code === 200) {
verificationCodeInput.focus(); 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; let timer = null;
@@ -776,4 +785,4 @@ button:active {
border-radius: 50%; border-radius: 50%;
margin-right: 8px; margin-right: 8px;
} }
</style> </style>

View File

@@ -4,6 +4,7 @@ import { showToast } from "vant";
import ClickCaptcha from "@/components/ClickCaptcha.vue"; import ClickCaptcha from "@/components/ClickCaptcha.vue";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(["login-success"]); const emit = defineEmits(["login-success"]);
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
@@ -17,6 +18,7 @@ const isAgreed = ref(false);
const isCountingDown = ref(false); const isCountingDown = ref(false);
const countdown = ref(60); const countdown = ref(60);
let timer = null; let timer = null;
const { runWithCaptcha } = useAliyunCaptcha();
// 验证组件状态 // 验证组件状态
const showCaptcha = ref(false); const showCaptcha = ref(false);
@@ -46,26 +48,31 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
} }
const { data, error } = await useApiFetch("auth/sendSms") await runWithCaptcha(
.post({ mobile: phoneNumber.value, actionType: "login" }) (captchaVerifyParam) =>
.json(); useApiFetch("auth/sendSms")
.post({
if (data.value && !error.value) { mobile: phoneNumber.value,
if (data.value.code === 200) { actionType: "login",
showToast({ message: "获取成功" }); captchaVerifyParam,
startCountdown(); })
// 聚焦到验证码输入框 .json(),
nextTick(() => { (res) => {
const verificationCodeInput = if (res.code === 200) {
document.getElementById("verificationCode"); showToast({ message: "获取成功" });
if (verificationCodeInput) { startCountdown();
verificationCodeInput.focus(); nextTick(() => {
} const verificationCodeInput =
}); document.getElementById("verificationCode");
} else { if (verificationCodeInput) {
showToast(data.value.msg); verificationCodeInput.focus();
} }
} });
} else {
showToast(res.msg || "获取失败");
}
},
);
} }
function startCountdown() { function startCountdown() {

View File

@@ -6,6 +6,7 @@ const dialogStore = useDialogStore();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
import { showToast } from "vant"; import { showToast } from "vant";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
// 表单数据 // 表单数据
const realName = ref(""); const realName = ref("");
const idCard = ref(""); const idCard = ref("");
@@ -17,6 +18,7 @@ const isAgreed = ref(false);
const isCountingDown = ref(false); const isCountingDown = ref(false);
const countdown = ref(60); const countdown = ref(60);
let timer = null; let timer = null;
const { runWithCaptcha } = useAliyunCaptcha();
// 聚焦状态变量 // 聚焦状态变量
const nameFocused = ref(false); const nameFocused = ref(false);
@@ -54,18 +56,24 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
} }
const { data, error } = await useApiFetch("auth/sendSms") await runWithCaptcha(
.post({ mobile: phoneNumber.value, actionType: "realName" }) (captchaVerifyParam) =>
.json(); useApiFetch("auth/sendSms")
.post({
if (data.value && !error.value) { mobile: phoneNumber.value,
if (data.value.code === 200) { actionType: "realName",
showToast({ message: "获取成功" }); captchaVerifyParam,
startCountdown(); })
} else { .json(),
showToast(data.value.msg); (res) => {
} if (res.code === 200) {
} showToast({ message: "获取成功" });
startCountdown();
} else {
showToast(res.msg || "获取失败");
}
},
);
} }
function startCountdown() { function startCountdown() {

View File

@@ -0,0 +1,155 @@
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;

View File

@@ -2,6 +2,7 @@
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 router = useRouter(); const router = useRouter();
const phoneNumber = ref(""); const phoneNumber = ref("");
@@ -12,6 +13,7 @@ const isAgreed = ref(false);
const isCountingDown = ref(false); const isCountingDown = ref(false);
const countdown = ref(60); const countdown = ref(60);
let timer = null; let timer = null;
const { runWithCaptcha } = useAliyunCaptcha();
// 验证组件状态 // 验证组件状态
const showCaptcha = ref(false); const showCaptcha = ref(false);
@@ -41,26 +43,31 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
} }
const { data, error } = await useApiFetch("auth/sendSms") await runWithCaptcha(
.post({ mobile: phoneNumber.value, actionType: "login" }) (captchaVerifyParam) =>
.json(); useApiFetch("auth/sendSms")
.post({
if (data.value && !error.value) { mobile: phoneNumber.value,
if (data.value.code === 200) { actionType: "login",
showToast({ message: "获取成功" }); captchaVerifyParam,
startCountdown(); })
// 聚焦到验证码输入框 .json(),
nextTick(() => { (res) => {
const verificationCodeInput = if (res.code === 200) {
document.getElementById("verificationCode"); showToast({ message: "获取成功" });
if (verificationCodeInput) { startCountdown();
verificationCodeInput.focus(); nextTick(() => {
} const verificationCodeInput =
}); document.getElementById("verificationCode");
} else { if (verificationCodeInput) {
showToast(data.value.msg); verificationCodeInput.focus();
} }
} });
} else {
showToast(res.msg || "获取失败");
}
},
);
} }
function startCountdown() { function startCountdown() {