|
|
@@ -228,8 +228,15 @@ const toast = useToast() |
|
|
const ALIYUN_CAPTCHA_SCRIPT = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js' |
|
|
const ALIYUN_CAPTCHA_SCRIPT = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js' |
|
|
const ALIYUN_CAPTCHA_SCENE_ID = 'fgtt5yvc' |
|
|
const ALIYUN_CAPTCHA_SCENE_ID = 'fgtt5yvc' |
|
|
const ALIYUN_CAPTCHA_PREFIX = '1ggr84' |
|
|
const ALIYUN_CAPTCHA_PREFIX = '1ggr84' |
|
|
|
|
|
const ALIYUN_CAPTCHA_WIDTH = 360 |
|
|
|
|
|
const ALIYUN_CAPTCHA_WARMUP_MS = 2000 |
|
|
|
|
|
const SMS_CAPTCHA_ERROR_CODE = 1020 |
|
|
|
|
|
const SMS_COOLDOWN_SECONDS = 60 |
|
|
|
|
|
const SMS_LAST_SENT_AT_KEY = 'ai_last_send_time' |
|
|
let aliyunCaptchaScriptPromise = null |
|
|
let aliyunCaptchaScriptPromise = null |
|
|
let captchaInstance = null |
|
|
let captchaInstance = null |
|
|
|
|
|
let captchaInitPromise = null |
|
|
|
|
|
let captchaInitializedAt = 0 |
|
|
|
|
|
|
|
|
// Tab |
|
|
// Tab |
|
|
const activeTab = ref(isMobileClient.value ? 'phone' : 'qr') |
|
|
const activeTab = ref(isMobileClient.value ? 'phone' : 'qr') |
|
|
@@ -318,7 +325,6 @@ const phoneForm = reactive({ |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
const countdown = ref(0) |
|
|
const countdown = ref(0) |
|
|
const captchaReady = ref(false) |
|
|
|
|
|
let countdownTimer = null |
|
|
let countdownTimer = null |
|
|
|
|
|
|
|
|
function shouldUseSmsCaptcha() { |
|
|
function shouldUseSmsCaptcha() { |
|
|
@@ -334,6 +340,10 @@ function cleanupAliyunCaptchaPopup() { |
|
|
|
|
|
|
|
|
function loadAliyunCaptchaScript() { |
|
|
function loadAliyunCaptchaScript() { |
|
|
if (!import.meta.client) return Promise.reject(new Error('验证码仅支持浏览器环境')) |
|
|
if (!import.meta.client) return Promise.reject(new Error('验证码仅支持浏览器环境')) |
|
|
|
|
|
window.AliyunCaptchaConfig = { |
|
|
|
|
|
region: 'cn', |
|
|
|
|
|
prefix: ALIYUN_CAPTCHA_PREFIX, |
|
|
|
|
|
} |
|
|
if (window.initAliyunCaptcha) return Promise.resolve() |
|
|
if (window.initAliyunCaptcha) return Promise.resolve() |
|
|
if (aliyunCaptchaScriptPromise) return aliyunCaptchaScriptPromise |
|
|
if (aliyunCaptchaScriptPromise) return aliyunCaptchaScriptPromise |
|
|
|
|
|
|
|
|
@@ -374,55 +384,128 @@ function loadAliyunCaptchaScript() { |
|
|
|
|
|
|
|
|
async function initSmsCaptcha() { |
|
|
async function initSmsCaptcha() { |
|
|
if (!shouldUseSmsCaptcha()) return |
|
|
if (!shouldUseSmsCaptcha()) return |
|
|
await loadAliyunCaptchaScript() |
|
|
|
|
|
await nextTick() |
|
|
|
|
|
|
|
|
if (captchaInstance) return captchaInstance |
|
|
|
|
|
if (captchaInitPromise) return captchaInitPromise |
|
|
|
|
|
|
|
|
const element = document.getElementById('captcha-element') |
|
|
|
|
|
const button = document.getElementById('captcha-button') |
|
|
|
|
|
if (!element || !button) { |
|
|
|
|
|
throw new Error('验证码初始化失败,请刷新后重试') |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
captchaInitPromise = (async () => { |
|
|
|
|
|
await loadAliyunCaptchaScript() |
|
|
|
|
|
await nextTick() |
|
|
|
|
|
|
|
|
if (captchaReady.value && captchaInstance) return |
|
|
|
|
|
|
|
|
const element = document.getElementById('captcha-element') |
|
|
|
|
|
const button = document.getElementById('captcha-button') |
|
|
|
|
|
if (!element || !button) { |
|
|
|
|
|
throw new Error('验证码初始化失败,请刷新后重试') |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
cleanupAliyunCaptchaPopup() |
|
|
|
|
|
window.initAliyunCaptcha({ |
|
|
|
|
|
SceneId: ALIYUN_CAPTCHA_SCENE_ID, |
|
|
|
|
|
prefix: ALIYUN_CAPTCHA_PREFIX, |
|
|
|
|
|
mode: 'popup', |
|
|
|
|
|
element: '#captcha-element', |
|
|
|
|
|
button: '#captcha-button', |
|
|
|
|
|
captchaVerifyCallback, |
|
|
|
|
|
onBizResultCallback, |
|
|
|
|
|
getInstance, |
|
|
|
|
|
slideStyle: { |
|
|
|
|
|
width: 360, |
|
|
|
|
|
height: 40, |
|
|
|
|
|
}, |
|
|
|
|
|
language: 'cn', |
|
|
|
|
|
}) |
|
|
|
|
|
captchaReady.value = true |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
cleanupAliyunCaptchaPopup() |
|
|
|
|
|
|
|
|
|
|
|
const pageWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) |
|
|
|
|
|
const rem = pageWidth > 0 && pageWidth <= ALIYUN_CAPTCHA_WIDTH |
|
|
|
|
|
? Math.floor(pageWidth / ALIYUN_CAPTCHA_WIDTH * 100) / 100 |
|
|
|
|
|
: 1 |
|
|
|
|
|
|
|
|
|
|
|
return await new Promise((resolve, reject) => { |
|
|
|
|
|
let settled = false |
|
|
|
|
|
const timeout = window.setTimeout(() => { |
|
|
|
|
|
if (settled) return |
|
|
|
|
|
settled = true |
|
|
|
|
|
reject(new Error('验证码初始化超时,请刷新后重试')) |
|
|
|
|
|
}, 15000) |
|
|
|
|
|
|
|
|
|
|
|
const resolveInstance = (instance) => { |
|
|
|
|
|
if (settled) return |
|
|
|
|
|
settled = true |
|
|
|
|
|
window.clearTimeout(timeout) |
|
|
|
|
|
captchaInstance = instance |
|
|
|
|
|
resolve(instance) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function getInstance(instance) { |
|
|
|
|
|
captchaInstance = instance |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
captchaInitializedAt = Date.now() |
|
|
|
|
|
window.initAliyunCaptcha({ |
|
|
|
|
|
SceneId: ALIYUN_CAPTCHA_SCENE_ID, |
|
|
|
|
|
prefix: ALIYUN_CAPTCHA_PREFIX, |
|
|
|
|
|
mode: 'popup', |
|
|
|
|
|
element: '#captcha-element', |
|
|
|
|
|
button: '#captcha-button', |
|
|
|
|
|
captchaVerifyCallback, |
|
|
|
|
|
onBizResultCallback, |
|
|
|
|
|
getInstance: resolveInstance, |
|
|
|
|
|
onError: () => { |
|
|
|
|
|
if (settled) return |
|
|
|
|
|
settled = true |
|
|
|
|
|
window.clearTimeout(timeout) |
|
|
|
|
|
reject(new Error('验证码组件加载失败,请稍后重试')) |
|
|
|
|
|
}, |
|
|
|
|
|
autoRefresh: true, |
|
|
|
|
|
slideStyle: { |
|
|
|
|
|
width: ALIYUN_CAPTCHA_WIDTH, |
|
|
|
|
|
height: 40, |
|
|
|
|
|
}, |
|
|
|
|
|
language: 'cn', |
|
|
|
|
|
rem, |
|
|
|
|
|
}) |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
if (settled) return |
|
|
|
|
|
settled = true |
|
|
|
|
|
window.clearTimeout(timeout) |
|
|
|
|
|
reject(error) |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
})() |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
return await captchaInitPromise |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
captchaInitPromise = null |
|
|
|
|
|
captchaInstance = null |
|
|
|
|
|
captchaInitializedAt = 0 |
|
|
|
|
|
throw error |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function onBizResultCallback() { |
|
|
function onBizResultCallback() { |
|
|
// 阿里云验证码业务回调占位,短信发送结果已在 captchaVerifyCallback 中处理。 |
|
|
// 阿里云验证码业务回调占位,短信发送结果已在 captchaVerifyCallback 中处理。 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function startSmsCountdown() { |
|
|
|
|
|
|
|
|
function getSmsCooldownSeconds() { |
|
|
|
|
|
if (!import.meta.client) return 0 |
|
|
|
|
|
const lastSentAt = Number(window.localStorage.getItem(SMS_LAST_SENT_AT_KEY)) |
|
|
|
|
|
if (!Number.isFinite(lastSentAt) || lastSentAt <= 0) { |
|
|
|
|
|
window.localStorage.removeItem(SMS_LAST_SENT_AT_KEY) |
|
|
|
|
|
return 0 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const remaining = lastSentAt + SMS_COOLDOWN_SECONDS - Math.floor(Date.now() / 1000) |
|
|
|
|
|
return Math.min(SMS_COOLDOWN_SECONDS, Math.max(0, remaining)) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function syncSmsCountdown() { |
|
|
clearInterval(countdownTimer) |
|
|
clearInterval(countdownTimer) |
|
|
countdown.value = 60 |
|
|
|
|
|
|
|
|
countdown.value = getSmsCooldownSeconds() |
|
|
|
|
|
if (countdown.value <= 0) { |
|
|
|
|
|
window.localStorage.removeItem(SMS_LAST_SENT_AT_KEY) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
countdownTimer = setInterval(() => { |
|
|
countdownTimer = setInterval(() => { |
|
|
countdown.value-- |
|
|
|
|
|
|
|
|
countdown.value = getSmsCooldownSeconds() |
|
|
if (countdown.value <= 0) { |
|
|
if (countdown.value <= 0) { |
|
|
clearInterval(countdownTimer) |
|
|
clearInterval(countdownTimer) |
|
|
|
|
|
window.localStorage.removeItem(SMS_LAST_SENT_AT_KEY) |
|
|
} |
|
|
} |
|
|
}, 1000) |
|
|
}, 1000) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function startSmsCountdown() { |
|
|
|
|
|
window.localStorage.setItem(SMS_LAST_SENT_AT_KEY, String(Math.floor(Date.now() / 1000))) |
|
|
|
|
|
syncSmsCountdown() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleSmsCooldownStorage(event) { |
|
|
|
|
|
if (event.key === SMS_LAST_SENT_AT_KEY) syncSmsCountdown() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
async function submitSmsCode(captchaVerifyParam = '') { |
|
|
async function submitSmsCode(captchaVerifyParam = '') { |
|
|
const payload = { |
|
|
const payload = { |
|
|
phone: phoneForm.phone, |
|
|
phone: phoneForm.phone, |
|
|
@@ -439,6 +522,9 @@ async function submitSmsCode(captchaVerifyParam = '') { |
|
|
if (res.data?.code) { |
|
|
if (res.data?.code) { |
|
|
phoneForm.code = String(res.data.code) |
|
|
phoneForm.code = String(res.data.code) |
|
|
phoneForm.devCode = String(res.data.code) |
|
|
phoneForm.devCode = String(res.data.code) |
|
|
|
|
|
} else { |
|
|
|
|
|
phoneForm.code = '' |
|
|
|
|
|
phoneForm.devCode = '' |
|
|
} |
|
|
} |
|
|
startSmsCountdown() |
|
|
startSmsCountdown() |
|
|
} |
|
|
} |
|
|
@@ -454,9 +540,10 @@ async function captchaVerifyCallback(captchaVerifyParam) { |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
phoneForm.error = e.message || '发送验证码失败' |
|
|
phoneForm.error = e.message || '发送验证码失败' |
|
|
|
|
|
const captchaFailed = Number(e?.code) === SMS_CAPTCHA_ERROR_CODE |
|
|
return { |
|
|
return { |
|
|
captchaResult: false, |
|
|
|
|
|
bizResult: true, |
|
|
|
|
|
|
|
|
captchaResult: !captchaFailed, |
|
|
|
|
|
bizResult: false, |
|
|
} |
|
|
} |
|
|
} finally { |
|
|
} finally { |
|
|
phoneForm.sendingCode = false |
|
|
phoneForm.sendingCode = false |
|
|
@@ -490,10 +577,22 @@ async function sendCode() { |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
phoneForm.sendingCode = true |
|
|
phoneForm.sendingCode = true |
|
|
await initSmsCaptcha() |
|
|
|
|
|
|
|
|
const instance = await initSmsCaptcha() |
|
|
|
|
|
const warmupRemaining = ALIYUN_CAPTCHA_WARMUP_MS - (Date.now() - captchaInitializedAt) |
|
|
|
|
|
if (warmupRemaining > 0) { |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, warmupRemaining)) |
|
|
|
|
|
} |
|
|
phoneForm.sendingCode = false |
|
|
phoneForm.sendingCode = false |
|
|
await new Promise(resolve => setTimeout(resolve, 50)) |
|
|
|
|
|
document.getElementById('captcha-button')?.click() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof instance?.startTracelessVerification === 'function') { |
|
|
|
|
|
instance.show?.() |
|
|
|
|
|
instance.startTracelessVerification() |
|
|
|
|
|
} else { |
|
|
|
|
|
const captchaButton = document.getElementById('captcha-button') |
|
|
|
|
|
if (captchaButton) captchaButton.click() |
|
|
|
|
|
else if (typeof instance?.show === 'function') instance.show() |
|
|
|
|
|
else throw new Error('验证码触发失败,请刷新后重试') |
|
|
|
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
phoneForm.sendingCode = false |
|
|
phoneForm.sendingCode = false |
|
|
phoneForm.error = e.message || '验证码初始化失败,请刷新后重试' |
|
|
phoneForm.error = e.message || '验证码初始化失败,请刷新后重试' |
|
|
@@ -540,6 +639,9 @@ function handleLoginSuccess(data) { |
|
|
// 初始化 |
|
|
// 初始化 |
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
reportCv('login_dialog_show', { page_key: 'login_dialog' }) |
|
|
reportCv('login_dialog_show', { page_key: 'login_dialog' }) |
|
|
|
|
|
syncSmsCountdown() |
|
|
|
|
|
window.addEventListener('storage', handleSmsCooldownStorage) |
|
|
|
|
|
if (shouldUseSmsCaptcha()) initSmsCaptcha().catch(() => {}) |
|
|
if (!isMobileClient.value) refreshQR() |
|
|
if (!isMobileClient.value) refreshQR() |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
@@ -555,8 +657,13 @@ watch(isMobileClient, (mobile) => { |
|
|
onUnmounted(() => { |
|
|
onUnmounted(() => { |
|
|
clearInterval(qrTimer) |
|
|
clearInterval(qrTimer) |
|
|
clearInterval(countdownTimer) |
|
|
clearInterval(countdownTimer) |
|
|
|
|
|
window.removeEventListener('storage', handleSmsCooldownStorage) |
|
|
|
|
|
try { |
|
|
|
|
|
captchaInstance?.destroyCaptcha?.() |
|
|
|
|
|
} catch {} |
|
|
captchaInstance = null |
|
|
captchaInstance = null |
|
|
captchaReady.value = false |
|
|
|
|
|
|
|
|
captchaInitPromise = null |
|
|
|
|
|
captchaInitializedAt = 0 |
|
|
cleanupAliyunCaptchaPopup() |
|
|
cleanupAliyunCaptchaPopup() |
|
|
}) |
|
|
}) |
|
|
</script> |
|
|
</script> |