Browse Source

save

main
leiyun 1 month ago
parent
commit
94b8265a85
2 changed files with 170 additions and 38 deletions
  1. +145
    -38
      app/components/LoginModal.vue
  2. +25
    -0
      app/plugins/baidu-hm.client.js

+ 145
- 38
app/components/LoginModal.vue View File

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

+ 25
- 0
app/plugins/baidu-hm.client.js View File

@@ -0,0 +1,25 @@
const BAIDU_HM_ID = '1cafaa94863718681ae361aa0b209ce7'

export default defineNuxtPlugin(() => {
window._hmt = window._hmt || []

useHead({
script: [
{
key: 'baidu-hm',
src: `https://hm.baidu.com/hm.js?${BAIDU_HM_ID}`,
async: true,
},
],
})

const router = useRouter()
let lastFullPath = router.currentRoute.value.fullPath

router.afterEach((to, _from, failure) => {
if (failure || to.fullPath === lastFullPath) return

lastFullPath = to.fullPath
window._hmt.push(['_trackPageview', to.fullPath])
})
})

Loading…
Cancel
Save