Ver código fonte

修复BUG

main
leiyun 1 mês atrás
pai
commit
729fc71de3
2 arquivos alterados com 207 adições e 28 exclusões
  1. +189
    -27
      app/components/LoginModal.vue
  2. +18
    -1
      app/pages/index.vue

+ 189
- 27
app/components/LoginModal.vue Ver arquivo

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

+ 18
- 1
app/pages/index.vue Ver arquivo

@@ -195,7 +195,15 @@
<div class="mt-5 flex items-end gap-1">
<span class="text-4xl font-extrabold text-[var(--c-172033)]">¥{{ pkg.price_yuan }}</span>
</div>
<div v-if="pkg.old_price_yuan" class="mt-1 text-xs text-[var(--c-a0aabb)] line-through">¥{{ pkg.old_price_yuan }}</div>
<div v-if="pkg.old_price_yuan || pkgBonusPercent(pkg)" class="mt-1 flex min-h-5 items-center gap-2 text-xs">
<span v-if="pkg.old_price_yuan" class="text-[var(--c-a0aabb)] line-through">¥{{ pkg.old_price_yuan }}</span>
<span
v-if="pkgBonusPercent(pkg)"
class="rounded-full bg-[var(--c-edfdf4)] px-2 py-0.5 font-extrabold text-[var(--c-159a54)]"
>
多送{{ pkgBonusPercent(pkg) }}%
</span>
</div>

<ul class="mt-6 space-y-2.5">
<li v-for="f in pkgFeatures(pkg)" :key="f" class="flex items-start gap-2 text-sm text-[var(--c-4b5b70)]">
@@ -631,6 +639,15 @@ function pkgSubtitle(pkg) {
if (p <= 1500) return '日常创作,性价比最高'
return '高频创作,一次买够'
}
function pkgBonusPercent(pkg) {
const provided = Number(pkg.bonus_percent || 0)
if (Number.isFinite(provided) && provided > 0) return Math.round(provided)

const price = Number(pkg.price || 0) || Number(pkg.price_yuan || 0) * 100
const oldPrice = Number(pkg.old_price || 0) || Number(pkg.old_price_yuan || 0) * 100
if (!Number.isFinite(price) || !Number.isFinite(oldPrice) || price <= 0 || oldPrice <= price) return 0
return Math.max(0, Math.round((oldPrice / price - 1) * 100))
}
function pkgFeatures(pkg) {
return [
`获得 ${pkg.points} 点数`,


Carregando…
Cancelar
Salvar