Files
hfb_sys/frontend/src/features/auth/views/MobileRegisterView.vue
T

423 lines
8.8 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.
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant'
import { fetchAuthCaptcha, type AuthCaptcha } from '@/features/auth/api/auth'
import { useSessionStore } from '@/stores/session'
import { useSmsCountdown } from '@/shared/composables/useSmsCountdown'
import { readError } from '@/shared/utils/error'
const router = useRouter()
const route = useRoute()
const session = useSessionStore()
const loading = ref(false)
const agreed = ref(false)
const captchaLoading = ref(false)
const captcha = ref<AuthCaptcha | null>(null)
const form = reactive({
phone: '',
captchaCode: '',
code: '',
inviteCode: '',
})
const { countDown, sending, handleSendCode } = useSmsCountdown()
onMounted(loadCaptcha)
async function loadCaptcha() {
captchaLoading.value = true
try {
captcha.value = await fetchAuthCaptcha()
form.captchaCode = ''
} catch (error) {
showToast({ message: readError(error, '图形验证码加载失败'), icon: 'cross' })
} finally {
captchaLoading.value = false
}
}
async function sendCode() {
const shouldRefresh = Boolean(form.captchaCode.trim())
await handleSendCode(form.phone, {
captchaId: captcha.value?.captcha_id || '',
captchaCode: form.captchaCode,
})
if (shouldRefresh) {
await loadCaptcha()
}
}
async function handleRegister() {
if (!agreed.value) {
showToast({
message: '请先阅读并同意用户协议和隐私政策',
icon: 'warning-o',
})
return
}
loading.value = true
try {
await session.login(form.phone, form.code)
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/m/profile'
await router.replace(redirect)
} catch {
showToast({ message: '注册失败,请检查手机号和验证码', icon: 'cross' })
} finally {
loading.value = false
}
}
</script>
<template>
<main class="mobile-register-shell">
<header class="page-header">
<button class="back-btn" type="button" @click="router.back()">
<van-icon name="arrow-left" :size="20" />
</button>
</header>
<section class="auth-body">
<div class="brand-lockup">
<div class="brand-logo">锤</div>
<strong>大锤商行</strong>
</div>
<div class="form-section">
<h1>注册</h1>
<label class="auth-input-row">
<input
v-model="form.phone"
type="tel"
maxlength="11"
inputmode="tel"
placeholder="手机号"
/>
</label>
<label class="auth-input-row captcha-row">
<input
v-model="form.captchaCode"
type="text"
maxlength="4"
autocomplete="off"
placeholder="图形验证码"
/>
<button
class="captcha-image-button"
type="button"
:disabled="captchaLoading"
@click="loadCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
<span v-else>刷新</span>
</button>
</label>
<label class="auth-input-row code-row">
<input
v-model="form.code"
type="text"
inputmode="numeric"
maxlength="6"
placeholder="验证码"
/>
<span class="code-action">
<van-button
size="small"
plain
:disabled="sending || countDown > 0"
:loading="sending"
class="code-btn"
@click="sendCode"
>
{{ countDown > 0 ? `${countDown}s` : sending ? '发送中' : '发送验证码' }}
</van-button>
</span>
</label>
<label class="auth-input-row">
<input v-model="form.inviteCode" maxlength="16" placeholder="邀请码(选填)" />
</label>
<div class="assist-row">
<span>验证码注册</span>
<RouterLink to="/m/login">已有账号?<b>去登录</b></RouterLink>
</div>
<div class="agreement-row">
<van-checkbox v-model="agreed" shape="square" icon-size="16px">
我已阅读并同意《<b>用户协议</b>》和《<b>隐私政策</b>》
</van-checkbox>
</div>
<van-button
type="primary"
block
class="primary-button"
:loading="loading"
loading-text="注册中..."
@click="handleRegister"
>
注册并登录
</van-button>
<RouterLink to="/m/login" class="secondary-entry"> 返回登录 </RouterLink>
</div>
</section>
</main>
</template>
<style scoped>
.mobile-register-shell {
min-height: 100dvh;
background:
radial-gradient(circle at 50% 12%, rgba(20, 119, 255, 0.1), transparent 34%),
linear-gradient(180deg, #f8fbff 0%, #ffffff 70%, #f7fafc 100%);
color: #111827;
}
.page-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10;
display: flex;
align-items: center;
height: 48px;
margin: 0;
padding: 0 18px;
background: transparent;
}
.back-btn {
display: grid;
width: 34px;
height: 34px;
place-items: center;
border: none;
border-radius: 999px;
background: none;
color: #111827;
}
.auth-body {
box-sizing: border-box;
display: flex;
min-height: 100dvh;
flex-direction: column;
justify-content: center;
padding: 50px 34px 22px;
}
.brand-lockup {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin: 0 auto clamp(24px, 4vh, 38px);
}
.brand-logo {
display: grid;
width: 46px;
height: 46px;
place-items: center;
border-radius: 14px;
background: linear-gradient(135deg, #1477ff, #2f8dff);
box-shadow: 0 14px 32px rgba(20, 119, 255, 0.18);
color: #ffffff;
font-size: 19px;
font-weight: 900;
}
.brand-lockup strong {
display: block;
color: #111827;
font-size: 18px;
font-weight: 900;
letter-spacing: 0;
}
.form-section {
width: min(100%, 420px);
margin: 0 auto;
}
.form-section h1 {
margin: 0 0 20px;
color: #05070a;
font-size: 22px;
font-weight: 900;
line-height: 1.2;
}
.auth-input-row {
display: flex;
align-items: center;
min-height: 48px;
margin-bottom: 10px;
padding: 0 16px;
border: 1px solid #e1e5eb;
border-radius: 10px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.03);
}
.auth-input-row.code-row {
gap: 8px;
}
.auth-input-row.captcha-row {
gap: 10px;
padding-right: 4px;
}
.auth-input-row input {
width: 100%;
min-width: 0;
border: none;
background: transparent;
color: #0f172a;
font-size: 16px;
outline: none;
}
.auth-input-row input::placeholder {
color: #b6beca;
}
.code-action {
display: flex;
justify-content: flex-end;
}
.captcha-image-button {
display: grid;
flex: 0 0 116px;
width: 116px;
height: 40px;
place-items: center;
overflow: hidden;
border: 1px solid #dbe5f0;
border-radius: 8px;
background: #f8fafc;
color: #1477ff;
font-size: 12px;
font-weight: 800;
padding: 0;
}
.captcha-image-button:disabled {
opacity: 0.65;
}
.captcha-image-button img {
display: block;
width: 120px;
height: 40px;
object-fit: cover;
}
.code-btn {
min-width: 82px;
height: 32px;
border-color: #1477ff !important;
border-radius: 8px;
color: #1477ff !important;
font-weight: 700;
}
.assist-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin: 12px 0 18px;
color: #111827;
font-size: 13px;
}
.assist-row span {
min-width: 0;
color: #1477ff;
}
.assist-row a {
flex-shrink: 0;
color: #22252b;
text-decoration: none;
}
.assist-row b {
color: #05070a;
font-weight: 900;
}
.agreement-row {
display: flex;
align-items: center;
margin: 0 0 18px;
}
.agreement-row :deep(.van-checkbox__label) {
margin-left: 8px;
color: #252b36;
font-size: 12px;
line-height: 1.4;
}
.agreement-row :deep(.van-checkbox__label b) {
color: #1477ff;
font-weight: 700;
}
.primary-button {
height: 48px;
border: none !important;
border-radius: 10px !important;
background: #1477ff !important;
box-shadow: 0 12px 24px rgba(20, 119, 255, 0.18);
font-size: 17px;
font-weight: 900;
}
.secondary-entry {
display: grid;
min-height: 44px;
margin-top: 10px;
place-items: center;
border: 1px solid #e4e8ee;
border-radius: 10px;
background: rgba(255, 255, 255, 0.82);
color: #1477ff;
font-size: 15px;
font-weight: 900;
text-align: center;
text-decoration: none;
}
@media (max-height: 760px) {
.auth-body {
justify-content: flex-start;
padding-top: 48px;
}
.brand-lockup {
margin-bottom: 20px;
}
.form-section h1 {
margin-bottom: 16px;
}
.assist-row {
margin-bottom: 14px;
}
}
</style>