Files
hfb_sys/frontend/src/views/mobile/MobileLoginView.vue
T

408 lines
8.2 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, useRoute, useRouter } from "vue-router";
import { showToast, showDialog } from "vant";
import { sendSmsCode } from "@/api/auth";
import { useSessionStore } from "@/stores/session";
const router = useRouter();
const route = useRoute();
const session = useSessionStore();
const loading = ref(false);
const sending = ref(false);
const agreed = ref(false);
const form = reactive({
phone: "",
code: "",
});
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({ message: "请输入手机号", icon: "warning-o" });
return;
}
sending.value = true;
try {
await sendSmsCode(form.phone);
showToast({
message: "验证码已发送,开发环境请查看后端日志",
icon: "passed",
});
startCountDown();
} catch (error) {
showToast({
message: readError(error, "验证码发送失败,请稍后重试"),
icon: "cross",
});
} 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 handleLogin() {
if (!agreed.value) {
showDialog({
title: "提示",
message: "请先阅读并同意用户协议和隐私政策",
});
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-login-shell">
<header class="page-header">
<button class="back-btn" type="button" @click="router.back()">
<van-icon name="arrow-left" :size="20" />
</button>
<h1>登录</h1>
<span class="header-spacer"></span>
</header>
<section class="auth-body">
<div class="brand-row">
<div class="brand-logo">锤</div>
<div>
<strong>大锤商行</strong>
<p>平台担保 · 安全租号</p>
</div>
</div>
<div class="auth-switch" aria-label="登录注册切换">
<RouterLink to="/m/login" class="active">登录</RouterLink>
<RouterLink to="/m/register">注册</RouterLink>
</div>
<div class="form-section">
<div class="section-heading">
<h2>手机号登录</h2>
<p>使用短信验证码登录,开发环境验证码查看后端日志。</p>
</div>
<label class="auth-input-row">
<span class="input-label">手机号</span>
<span class="field-prefix">+86</span>
<input
v-model="form.phone"
type="tel"
maxlength="11"
placeholder="请输入手机号"
/>
</label>
<label class="auth-input-row code-row">
<span class="input-label">验证码</span>
<input
v-model="form.code"
type="text"
inputmode="numeric"
maxlength="6"
placeholder="6 位验证码"
/>
<span class="code-action">
<van-button
size="small"
type="primary"
plain
:disabled="sending || countDown > 0"
:loading="sending"
class="code-btn"
@click="handleSendCode"
>
{{
countDown > 0
? `${countDown}s`
: sending
? "发送中"
: "发送验证码"
}}
</van-button>
</span>
</label>
<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="handleLogin"
>
登录
</van-button>
</div>
<RouterLink to="/m/register" class="secondary-entry">
没有账号?立即注册
</RouterLink>
</section>
</main>
</template>
<style scoped>
.mobile-login-shell {
min-height: 100dvh;
background: #f4f6f8;
color: #151c28;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
background: #fff;
border-bottom: 1px solid #eceff3;
}
.page-header h1 {
flex: 1;
margin: 0;
font-size: 17px;
font-weight: 700;
text-align: center;
}
.back-btn {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: none;
background: none;
color: #25282d;
}
.header-spacer {
width: 36px;
}
.auth-body {
padding: 14px;
}
.brand-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 2px 14px;
}
.brand-logo {
display: grid;
width: 42px;
height: 42px;
place-items: center;
border-radius: 12px;
background: #1477ff;
color: #ffffff;
font-size: 18px;
font-weight: 900;
}
.brand-row strong {
display: block;
margin: 0 0 2px;
color: #171a1f;
font-size: 18px;
font-weight: 900;
}
.brand-row p {
margin: 0;
color: #6b7280;
font-size: 12px;
}
.auth-switch {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
margin-bottom: 12px;
padding: 4px;
border-radius: 8px;
background: #eaf0f8;
}
.auth-switch a {
padding: 9px 0;
border-radius: 7px;
color: #5f6875;
text-align: center;
font-size: 13px;
font-weight: 800;
text-decoration: none;
}
.auth-switch a.active {
background: #ffffff;
color: #1477ff;
}
.form-section {
padding: 14px;
background: #fff;
border: 1px solid #edf0f4;
border-radius: 8px;
}
.section-heading h2 {
margin: 0 0 4px;
color: #171a1f;
font-size: 18px;
font-weight: 800;
}
.section-heading p {
margin: 0 0 12px;
color: #858c96;
font-size: 12px;
line-height: 1.5;
}
.auth-input-row {
display: grid;
grid-template-columns: 58px 38px minmax(0, 1fr);
align-items: center;
min-height: 44px;
margin-bottom: 8px;
padding: 0 12px;
border-radius: 8px;
background: #f8fafc;
}
.auth-input-row.code-row {
grid-template-columns: 58px minmax(0, 1fr) 92px;
gap: 8px;
}
.input-label {
color: #30343a;
font-size: 13px;
font-weight: 700;
}
.auth-input-row input {
width: 100%;
min-width: 0;
border: none;
background: transparent;
color: #20242a;
font-size: 14px;
outline: none;
}
.auth-input-row input::placeholder {
color: #c0c4cc;
}
.field-prefix {
color: #1477ff;
font-weight: 800;
}
.code-action {
display: flex;
justify-content: flex-end;
}
.code-btn {
border-radius: 8px;
}
.agreement-row {
display: flex;
align-items: center;
margin: 10px 0 14px;
}
.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;
}
.primary-button {
height: 42px;
border: none !important;
border-radius: 8px !important;
background: #1477ff !important;
font-size: 15px;
font-weight: 700;
}
.secondary-entry {
display: block;
margin-top: 12px;
padding: 12px;
border: 1px solid #d7dce3;
border-radius: 8px;
background: #fff;
color: #1477ff;
font-size: 13px;
font-weight: 800;
text-align: center;
text-decoration: none;
}
</style>