新版官网模板

This commit is contained in:
2026-04-29 01:33:33 +08:00
commit 0d82386f8f
341 changed files with 64526 additions and 0 deletions

283
app/pages/bind-phone.vue Normal file
View 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
: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>