|
|
|
@@ -153,17 +153,27 @@ |
|
|
|
:disabled="phoneForm.sendingCode || countdown > 0" |
|
|
|
:class="[ |
|
|
|
'shrink-0 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors cursor-pointer', |
|
|
|
countdown > 0 |
|
|
|
phoneForm.sendingCode || countdown > 0 |
|
|
|
? 'text-[var(--c-94a3b8)] bg-[var(--c-f1f5f9)] cursor-not-allowed' |
|
|
|
: 'text-[var(--c-0b8cff)] bg-[var(--c-eef7ff)] hover:bg-[var(--c-e0f0ff)]', |
|
|
|
]" |
|
|
|
@click="sendCode" |
|
|
|
> |
|
|
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }} |
|
|
|
{{ phoneForm.sendingCode ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码' }} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 阿里云验证码挂载点:生产环境发送登录短信前触发,开发环境仍走后端 mock --> |
|
|
|
<div id="captcha-element" class="absolute size-px overflow-hidden opacity-0" aria-hidden="true" /> |
|
|
|
<button |
|
|
|
id="captcha-button" |
|
|
|
type="button" |
|
|
|
class="absolute size-px overflow-hidden opacity-0" |
|
|
|
tabindex="-1" |
|
|
|
aria-hidden="true" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 开发环境提示(后端未发短信,直接返回验证码) --> |
|
|
|
<p v-if="phoneForm.devCode" class="text-xs text-[var(--c-94a3b8)]"> |
|
|
|
开发环境验证码:<span class="font-bold text-[var(--c-0b8cff)]">{{ phoneForm.devCode }}</span>(已自动填入) |
|
|
|
@@ -215,6 +225,12 @@ const { isMobileClient } = useClientDevice() |
|
|
|
const { reportCv } = usePvcv() |
|
|
|
const toast = useToast() |
|
|
|
|
|
|
|
const ALIYUN_CAPTCHA_SCRIPT = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js' |
|
|
|
const ALIYUN_CAPTCHA_SCENE_ID = 'fgtt5yvc' |
|
|
|
const ALIYUN_CAPTCHA_PREFIX = '1ggr84' |
|
|
|
let aliyunCaptchaScriptPromise = null |
|
|
|
let captchaInstance = null |
|
|
|
|
|
|
|
// Tab |
|
|
|
const activeTab = ref(isMobileClient.value ? 'phone' : 'qr') |
|
|
|
|
|
|
|
@@ -302,45 +318,188 @@ const phoneForm = reactive({ |
|
|
|
}) |
|
|
|
|
|
|
|
const countdown = ref(0) |
|
|
|
const captchaReady = ref(false) |
|
|
|
let countdownTimer = null |
|
|
|
|
|
|
|
// 发送验证码 |
|
|
|
async function sendCode() { |
|
|
|
if (!phoneForm.phone || phoneForm.phone.length !== 11) { |
|
|
|
phoneForm.error = '请输入正确的11位手机号' |
|
|
|
return |
|
|
|
function shouldUseSmsCaptcha() { |
|
|
|
return import.meta.client && !import.meta.dev |
|
|
|
} |
|
|
|
|
|
|
|
function cleanupAliyunCaptchaPopup() { |
|
|
|
if (!import.meta.client) return |
|
|
|
;['aliyunCaptcha-mask', 'aliyunCaptcha-window-popup'].forEach((id) => { |
|
|
|
document.getElementById(id)?.remove() |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function loadAliyunCaptchaScript() { |
|
|
|
if (!import.meta.client) return Promise.reject(new Error('验证码仅支持浏览器环境')) |
|
|
|
if (window.initAliyunCaptcha) return Promise.resolve() |
|
|
|
if (aliyunCaptchaScriptPromise) return aliyunCaptchaScriptPromise |
|
|
|
|
|
|
|
aliyunCaptchaScriptPromise = new Promise((resolve, reject) => { |
|
|
|
const existed = document.querySelector(`script[src="${ALIYUN_CAPTCHA_SCRIPT}"]`) |
|
|
|
const script = existed || document.createElement('script') |
|
|
|
const timeout = setTimeout(() => { |
|
|
|
aliyunCaptchaScriptPromise = null |
|
|
|
reject(new Error('验证码组件加载超时,请刷新后重试')) |
|
|
|
}, 15000) |
|
|
|
|
|
|
|
const done = () => { |
|
|
|
clearTimeout(timeout) |
|
|
|
if (window.initAliyunCaptcha) resolve() |
|
|
|
else { |
|
|
|
aliyunCaptchaScriptPromise = null |
|
|
|
reject(new Error('验证码组件加载失败')) |
|
|
|
} |
|
|
|
} |
|
|
|
const fail = () => { |
|
|
|
clearTimeout(timeout) |
|
|
|
aliyunCaptchaScriptPromise = null |
|
|
|
reject(new Error('验证码组件加载失败,请刷新后重试')) |
|
|
|
} |
|
|
|
|
|
|
|
script.addEventListener('load', done, { once: true }) |
|
|
|
script.addEventListener('error', fail, { once: true }) |
|
|
|
|
|
|
|
if (!existed) { |
|
|
|
script.src = ALIYUN_CAPTCHA_SCRIPT |
|
|
|
script.async = true |
|
|
|
document.head.appendChild(script) |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
return aliyunCaptchaScriptPromise |
|
|
|
} |
|
|
|
|
|
|
|
async function initSmsCaptcha() { |
|
|
|
if (!shouldUseSmsCaptcha()) return |
|
|
|
await loadAliyunCaptchaScript() |
|
|
|
await nextTick() |
|
|
|
|
|
|
|
const element = document.getElementById('captcha-element') |
|
|
|
const button = document.getElementById('captcha-button') |
|
|
|
if (!element || !button) { |
|
|
|
throw new Error('验证码初始化失败,请刷新后重试') |
|
|
|
} |
|
|
|
|
|
|
|
if (captchaReady.value && captchaInstance) return |
|
|
|
|
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
function getInstance(instance) { |
|
|
|
captchaInstance = instance |
|
|
|
} |
|
|
|
|
|
|
|
function onBizResultCallback() { |
|
|
|
// 阿里云验证码业务回调占位,短信发送结果已在 captchaVerifyCallback 中处理。 |
|
|
|
} |
|
|
|
|
|
|
|
function startSmsCountdown() { |
|
|
|
clearInterval(countdownTimer) |
|
|
|
countdown.value = 60 |
|
|
|
countdownTimer = setInterval(() => { |
|
|
|
countdown.value-- |
|
|
|
if (countdown.value <= 0) { |
|
|
|
clearInterval(countdownTimer) |
|
|
|
} |
|
|
|
}, 1000) |
|
|
|
} |
|
|
|
|
|
|
|
async function submitSmsCode(captchaVerifyParam = '') { |
|
|
|
const payload = { |
|
|
|
phone: phoneForm.phone, |
|
|
|
type: 3, |
|
|
|
} |
|
|
|
if (captchaVerifyParam) payload.captchaVerifyParam = captchaVerifyParam |
|
|
|
|
|
|
|
const res = await post('/common/sendcode', payload) |
|
|
|
if (res.code !== 0) { |
|
|
|
throw new Error(res.msg || '发送验证码失败') |
|
|
|
} |
|
|
|
|
|
|
|
// 开发/测试环境后端不发短信,直接把验证码返回,这里自动回填方便调试 |
|
|
|
if (res.data?.code) { |
|
|
|
phoneForm.code = String(res.data.code) |
|
|
|
phoneForm.devCode = String(res.data.code) |
|
|
|
} |
|
|
|
startSmsCountdown() |
|
|
|
} |
|
|
|
|
|
|
|
async function captchaVerifyCallback(captchaVerifyParam) { |
|
|
|
phoneForm.error = '' |
|
|
|
phoneForm.sendingCode = true |
|
|
|
|
|
|
|
try { |
|
|
|
const res = await post('/common/sendcode', { |
|
|
|
phone: phoneForm.phone, |
|
|
|
type: 3, |
|
|
|
}) |
|
|
|
if (res.code === 0) { |
|
|
|
// 开发/测试环境后端不发短信,直接把验证码返回,这里自动回填方便调试 |
|
|
|
if (res.data?.code) { |
|
|
|
phoneForm.code = String(res.data.code) |
|
|
|
phoneForm.devCode = String(res.data.code) |
|
|
|
} |
|
|
|
countdown.value = 60 |
|
|
|
countdownTimer = setInterval(() => { |
|
|
|
countdown.value-- |
|
|
|
if (countdown.value <= 0) { |
|
|
|
clearInterval(countdownTimer) |
|
|
|
} |
|
|
|
}, 1000) |
|
|
|
} else { |
|
|
|
phoneForm.error = res.msg || '发送验证码失败' |
|
|
|
await submitSmsCode(captchaVerifyParam) |
|
|
|
return { |
|
|
|
captchaResult: true, |
|
|
|
bizResult: true, |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
phoneForm.error = e.message || '发送验证码失败' |
|
|
|
return { |
|
|
|
captchaResult: false, |
|
|
|
bizResult: true, |
|
|
|
} |
|
|
|
} finally { |
|
|
|
phoneForm.sendingCode = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function validatePhone() { |
|
|
|
if (!phoneForm.phone || phoneForm.phone.length !== 11) { |
|
|
|
phoneForm.error = '请输入正确的11位手机号' |
|
|
|
return false |
|
|
|
} |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
// 发送验证码 |
|
|
|
async function sendCode() { |
|
|
|
if (!validatePhone()) return |
|
|
|
phoneForm.error = '' |
|
|
|
|
|
|
|
if (!shouldUseSmsCaptcha()) { |
|
|
|
phoneForm.sendingCode = true |
|
|
|
try { |
|
|
|
await submitSmsCode() |
|
|
|
} catch (e) { |
|
|
|
phoneForm.error = e.message || '发送验证码失败' |
|
|
|
} finally { |
|
|
|
phoneForm.sendingCode = false |
|
|
|
} |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
phoneForm.sendingCode = true |
|
|
|
await initSmsCaptcha() |
|
|
|
phoneForm.sendingCode = false |
|
|
|
await new Promise(resolve => setTimeout(resolve, 50)) |
|
|
|
document.getElementById('captcha-button')?.click() |
|
|
|
} catch (e) { |
|
|
|
phoneForm.sendingCode = false |
|
|
|
phoneForm.error = e.message || '验证码初始化失败,请刷新后重试' |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 手机号登录 |
|
|
|
async function loginByPhone() { |
|
|
|
if (!phoneForm.phone || !phoneForm.code) return |
|
|
|
@@ -396,5 +555,8 @@ watch(isMobileClient, (mobile) => { |
|
|
|
onUnmounted(() => { |
|
|
|
clearInterval(qrTimer) |
|
|
|
clearInterval(countdownTimer) |
|
|
|
captchaInstance = null |
|
|
|
captchaReady.value = false |
|
|
|
cleanupAliyunCaptchaPopup() |
|
|
|
}) |
|
|
|
</script> |