Files
template-nuxt4/app/pages/bind-phone.vue
2026-04-29 01:33:33 +08:00

284 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="bind-phone-page">
<div class="bind-card">
<div class="bind-header">
<h1>绑定手机号</h1>
<p>首次通过公众号登录请先完成手机号绑定</p>
</div>
<div v-if="pageState === 'loading'" class="bind-state">
<a-spin size="large" />
<span>正在校验登录状态...</span>
</div>
<div v-else-if="pageState === 'error'" class="bind-state error">
<CloseCircleOutlined class="state-icon" />
<p>{{ pageMessage }}</p>
<a-button type="primary" @click="goToLogin">返回登录</a-button>
</div>
<div v-else class="bind-form-wrap">
<a-alert
:message="pageMessage || '绑定成功后将自动完成当前扫码登录'"
class="bind-alert"
show-icon
type="warning"
/>
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="手机号" name="phone">
<a-input v-model:value="form.phone" placeholder="请输入手机号" size="large" />
</a-form-item>
<a-form-item label="短信验证码" name="smsCode">
<div class="sms-row">
<a-input v-model:value="form.smsCode" placeholder="请输入短信验证码" size="large" />
<a-button :disabled="countdown > 0" :loading="sendingSms" size="large" @click="sendSmsCode">
{{ countdown > 0 ? `${countdown}s 后重试` : '发送验证码' }}
</a-button>
</div>
</a-form-item>
<a-button :loading="submitting" block size="large" type="primary" @click="submit">
绑定手机号并登录
</a-button>
</a-form>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { message, type FormInstance } from 'ant-design-vue'
import { CloseCircleOutlined } from '@ant-design/icons-vue'
import { checkQrCodeStatus, bindQrLoginPhone, type QrCodeStatusResponse } from '@/api/passport/qrLogin'
import { sendSmsCaptcha } from '@/api/passport/login'
import { setToken } from '@/utils/token-util'
definePageMeta({ layout: 'blank' })
const route = useRoute()
const router = useRouter()
const token = computed(() => String(route.query.token || ''))
const formRef = ref<FormInstance>()
const submitting = ref(false)
const sendingSms = ref(false)
const countdown = ref(0)
const pageState = ref<'loading' | 'ready' | 'error'>('loading')
const pageMessage = ref('')
let countdownTimer: ReturnType<typeof setInterval> | null = null
const form = reactive({
phone: '',
smsCode: ''
})
const phoneReg = /^1[3-9]\d{9}$/
const rules = reactive({
phone: [
{ required: true, message: '请输入手机号', type: 'string' },
{ pattern: phoneReg, message: '手机号格式不正确', trigger: 'blur' }
],
smsCode: [{ required: true, message: '请输入短信验证码', type: 'string' }]
})
function stopCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
countdown.value = 0
}
function persistUserInfo(result: QrCodeStatusResponse) {
const accessToken = result.accessToken || result.access_token
if (accessToken) {
setToken(String(accessToken), true)
}
if (import.meta.client) {
if (result.tenantId) {
localStorage.setItem('TenantId', String(result.tenantId))
}
const userId = result.userInfo?.userId
if (userId) {
localStorage.setItem('UserId', String(userId))
}
}
}
async function applyLoginResult(result: QrCodeStatusResponse, successText = '登录成功') {
persistUserInfo(result)
message.success(successText)
await router.replace('/')
}
async function loadStatus() {
if (!token.value) {
pageState.value = 'error'
pageMessage.value = '缺少二维码参数,请重新扫码'
return
}
try {
const result = await checkQrCodeStatus(token.value)
if (result.status === 'confirmed') {
await applyLoginResult(result)
return
}
if (result.status === 'bind_phone') {
pageState.value = 'ready'
pageMessage.value = result.message || '请输入手机号和短信验证码,完成首次登录'
return
}
if (result.status === 'expired') {
pageState.value = 'error'
pageMessage.value = '二维码已过期,请返回登录页重新扫码'
return
}
pageState.value = 'error'
pageMessage.value = '当前二维码尚未进入绑定流程,请先完成扫码关注'
} catch (error: unknown) {
pageState.value = 'error'
pageMessage.value = error instanceof Error ? error.message : '校验扫码状态失败'
}
}
async function sendSmsCode() {
if (!phoneReg.test(form.phone)) {
return message.warning('请先输入正确的手机号')
}
sendingSms.value = true
try {
await sendSmsCaptcha({ phone: form.phone })
message.success('验证码已发送')
stopCountdown()
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
stopCountdown()
}
}, 1000)
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '发送验证码失败')
} finally {
sendingSms.value = false
}
}
async function submit() {
if (!formRef.value || !token.value) return
submitting.value = true
try {
await formRef.value.validate()
const result = await bindQrLoginPhone({
token: token.value,
phone: form.phone,
code: form.smsCode
})
await applyLoginResult(result, '手机号绑定成功,已完成登录')
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '绑定手机号失败')
} finally {
submitting.value = false
}
}
function goToLogin() {
router.replace('/login')
}
onMounted(async () => {
await loadStatus()
})
onUnmounted(() => {
stopCountdown()
})
</script>
<style scoped>
.bind-phone-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: linear-gradient(135deg, #eff6ff 0%, #f5f3ff 100%);
}
.bind-card {
width: 460px;
max-width: 100%;
padding: 32px;
border-radius: 20px;
background: #fff;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
}
.bind-header {
text-align: center;
margin-bottom: 24px;
}
.bind-header h1 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: #111827;
}
.bind-header p {
margin: 0;
color: #6b7280;
font-size: 14px;
}
.bind-state {
min-height: 240px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
text-align: center;
color: #6b7280;
}
.bind-state.error {
color: #ef4444;
}
.state-icon {
font-size: 52px;
}
.bind-form-wrap {
display: flex;
flex-direction: column;
gap: 20px;
}
.bind-alert {
margin-bottom: 4px;
}
.sms-row {
display: grid;
grid-template-columns: 1fr 132px;
gap: 12px;
}
@media (max-width: 640px) {
.bind-card {
padding: 24px 20px;
}
.sms-row {
grid-template-columns: 1fr;
}
}
</style>