Files
hfb_sys/frontend/src/views/mobile/MobileRegisterView.vue
T
2026-05-22 21:03:02 +08:00

442 lines
9.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.
<script setup lang="ts">
import { onUnmounted, reactive, ref } from "vue";
import { RouterLink, useRouter } from "vue-router";
import { showToast } from "vant";
import { sendSmsCode } from "@/api/auth";
import { useSessionStore } from "@/stores/session";
const router = useRouter();
const session = useSessionStore();
const loading = ref(false);
const sending = ref(false);
const agreed = ref(false);
const form = reactive({
phone: "",
code: "",
inviteCode: "",
});
/* --------- 验证码倒计时 --------- */
const countDown = ref(0);
let timer: ReturnType<typeof setInterval> | null = null;
function startCountDown() {
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer!);
timer = null;
}
}, 1000);
}
onUnmounted(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
/* --------- 业务方法 --------- */
async function handleSendCode() {
if (!form.phone.trim()) {
showToast("请输入手机号");
return;
}
sending.value = true;
try {
await sendSmsCode(form.phone);
showToast("验证码已发送,开发环境请查看后端日志");
startCountDown();
} catch (error) {
showToast(readError(error, "验证码发送失败,请稍后重试"));
} finally {
sending.value = false;
}
}
function readError(error: unknown, fallback: string) {
if (typeof error === "object" && error && "response" in error) {
const response = (error as { response?: { data?: { message?: string } } })
.response;
return response?.data?.message || fallback;
}
return fallback;
}
async function handleRegister() {
if (!agreed.value) {
showToast("请先阅读并同意用户协议和隐私政策");
return;
}
loading.value = true;
try {
await session.login(form.phone, form.code);
await router.push("/m/profile");
} catch {
showToast("注册失败,请检查手机号和验证码");
} finally {
loading.value = false;
}
}
</script>
<template>
<main class="mobile-register-shell">
<section class="register-card">
<!-- 品牌区域 -->
<div class="register-top">
<div class="monster-logo">锤</div>
<div>
<p>加入大锤商行</p>
<small>注册后即可租号、下单、发布账号</small>
</div>
</div>
<!-- 登录/注册 Tab 切换 -->
<div class="auth-switch" aria-label="登录注册切换">
<RouterLink to="/m/login">登录</RouterLink>
<RouterLink to="/m/register" class="active">注册</RouterLink>
</div>
<p class="register-eyebrow">CREATE ACCOUNT</p>
<h1>手机号注册</h1>
<p class="register-subtitle">
先用短信验证码创建账号,后续再补充实名和收款信息。
</p>
<!-- 手机号输入 -->
<van-field
v-model="form.phone"
type="tel"
maxlength="11"
inputmode="tel"
placeholder="请输入手机号"
clearable
class="register-field"
>
<template #left-icon>
<span class="field-prefix">+86</span>
</template>
</van-field>
<!-- 验证码输入 -->
<van-field
v-model="form.code"
type="digit"
maxlength="6"
placeholder="6 位验证码"
class="register-field"
:left-icon="'shield-o'"
>
<template #button>
<van-button
size="small"
type="primary"
plain
round
:disabled="sending || countDown > 0"
:loading="sending"
@click="handleSendCode"
>
{{
countDown > 0
? `${countDown}s`
: sending
? "发送中"
: "发送验证码"
}}
</van-button>
</template>
</van-field>
<!-- 邀请码输入 -->
<van-field
v-model="form.inviteCode"
maxlength="16"
placeholder="选填,有邀请码填入"
clearable
class="register-field"
:left-icon="'friends-o'"
/>
<!-- 用户协议 -->
<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
round
class="register-button"
:loading="loading"
loading-text="注册中..."
@click="handleRegister"
>
注册并登录
</van-button>
<!-- 底部登录入口 -->
<RouterLink
to="/m/login"
class="login-entry"
custom
v-slot="{ navigate }"
>
<van-button
plain
type="primary"
block
round
size="small"
class="login-link-button"
@click="navigate"
>
已有账号?去登录
</van-button>
</RouterLink>
</section>
</main>
</template>
<style scoped>
/* ========== Vant 主题变量覆盖 ========== */
:root {
--van-primary-color: #5b6ee1;
--van-button-primary-background: #5b6ee1;
--van-button-primary-border-color: transparent;
}
/* ========== 页面容器 ========== */
.mobile-register-shell {
min-height: 100dvh;
box-sizing: border-box;
overflow: hidden;
background: #f5f6f8;
color: #151c28;
padding: 40px 24px;
}
.register-card {
display: flex;
min-height: calc(100dvh - 80px);
flex-direction: column;
justify-content: center;
}
/* ========== 品牌区域 ========== */
.register-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.monster-logo {
display: grid;
width: 48px;
height: 48px;
flex: 0 0 auto;
place-items: center;
border-radius: 16px;
background: #5b6ee1;
color: #ffffff;
font-size: 21px;
font-weight: 900;
box-shadow: 0 4px 12px rgba(91, 110, 225, 0.18);
}
.register-top p {
margin: 0 0 2px;
font-size: 20px;
font-weight: 900;
}
.register-top small,
.register-subtitle {
color: #667386;
}
/* ========== Tab 切换 ========== */
.auth-switch {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 16px;
border-radius: 15px;
background: #eef4fb;
padding: 5px;
}
.auth-switch a {
border-radius: 11px;
color: #667386;
padding: 10px 0;
text-align: center;
font-size: 14px;
font-weight: 900;
text-decoration: none;
}
.auth-switch a.active {
background: #ffffff;
color: #111827;
box-shadow: 0 8px 18px rgba(23, 35, 61, 0.08);
}
/* ========== 标题区 ========== */
.register-eyebrow {
margin: 0 0 5px;
color: #02846e;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
}
.register-card h1 {
margin: 0;
font-size: 28px;
line-height: 1.15;
}
.register-subtitle {
margin: 7px 0 14px;
font-size: 13px;
line-height: 1.45;
}
/* ========== Vant Field 覆盖样式 ========== */
.register-field {
margin-top: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.92);
box-shadow: inset 0 1px 3px rgba(23, 35, 61, 0.04);
overflow: hidden;
}
.register-field :deep(.van-field__control) {
font-size: 15px;
color: #151c28;
}
.register-field :deep(.van-field__label) {
width: auto;
margin-right: 8px;
color: #333b48;
font-weight: 700;
font-size: 13px;
}
.register-field :deep(.van-field__left-icon) {
color: #1777ff;
margin-right: 0;
}
.register-field :deep(.van-field__right-icon) {
color: #999;
}
.field-prefix {
color: #1777ff;
font-weight: 800;
font-size: 14px;
margin-right: 4px;
}
/* ========== 协议行 ========== */
.agreement-row {
display: flex;
align-items: center;
margin-top: 16px;
margin-bottom: 16px;
}
.agreement-row :deep(.van-checkbox__label) {
margin-left: 6px;
color: #333b48;
font-size: 12px;
line-height: 1.4;
}
.agreement-row :deep(.van-checkbox__label b) {
color: #1777ff;
font-weight: 600;
}
/* ========== 注册按钮 ========== */
.register-button {
margin-top: 6px;
height: 46px;
font-size: 15px;
font-weight: 800;
background: #5b6ee1 !important;
border: none !important;
box-shadow: 0 4px 12px rgba(91, 110, 225, 0.18);
}
/* ========== 登录入口按钮 ========== */
.login-link-button {
margin-top: 12px;
font-size: 13px;
font-weight: 800;
}
.login-entry {
text-decoration: none;
}
/* ========== 响应式 ========== */
@media (max-height: 620px) {
.mobile-register-shell {
padding-top: 10px;
padding-bottom: 10px;
}
.register-card {
min-height: calc(100dvh - 20px);
}
.register-top {
margin-bottom: 10px;
}
.monster-logo {
width: 42px;
height: 42px;
border-radius: 14px;
}
.auth-switch {
margin-bottom: 11px;
}
.register-card h1 {
font-size: 24px;
}
.register-field {
margin-top: 8px;
}
.register-button {
height: 42px;
margin-top: 10px;
}
}
@media (min-width: 520px) {
.mobile-register-shell {
max-width: 430px;
margin: 0 auto;
box-shadow: 0 0 0 1px rgba(23, 35, 61, 0.08);
}
}
</style>