初始版本
This commit is contained in:
283
app/pages/bind-phone.vue
Normal file
283
app/pages/bind-phone.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<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
|
||||
type="warning"
|
||||
show-icon
|
||||
:message="pageMessage || '绑定成功后将自动完成当前扫码登录'"
|
||||
class="bind-alert"
|
||||
/>
|
||||
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="form.phone" size="large" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="短信验证码" name="smsCode">
|
||||
<div class="sms-row">
|
||||
<a-input v-model:value="form.smsCode" size="large" placeholder="请输入短信验证码" />
|
||||
<a-button :disabled="countdown > 0" :loading="sendingSms" size="large" @click="sendSmsCode">
|
||||
{{ countdown > 0 ? `${countdown}s 后重试` : '发送验证码' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-button type="primary" block size="large" :loading="submitting" @click="submit">
|
||||
绑定手机号并登录
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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>
|
||||
Reference in New Issue
Block a user