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

444 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, showDialog } 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: "",
});
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 handleLogin() {
if (!agreed.value) {
showDialog({
title: "提示",
message: "请先阅读并同意用户协议和隐私政策",
});
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-login-shell">
<section class="login-card">
<!-- 品牌区域 -->
<div class="login-top">
<div class="monster-logo"></div>
<div>
<p>大锤商行</p>
<small>平台担保 · 安全租号</small>
</div>
</div>
<!-- 登录/注册 Tab 切换 -->
<div class="auth-switch" aria-label="登录注册切换">
<RouterLink to="/m/login" class="active">登录</RouterLink>
<RouterLink to="/m/register">注册</RouterLink>
</div>
<p class="login-eyebrow">SMS LOGIN</p>
<h1>手机号登录</h1>
<p class="login-subtitle">验证码登录开发环境验证码查看后端日志</p>
<!-- 手机号输入 -->
<van-field
v-model="form.phone"
type="tel"
maxlength="11"
placeholder="请输入手机号"
clearable
class="login-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="login-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>
<!-- 底部链接行 -->
<div class="login-links">
<van-button size="mini" plain type="primary">忘记密码</van-button>
</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
round
class="login-button"
:loading="loading"
loading-text="登录中..."
@click="handleLogin"
>
登录
</van-button>
<!-- 底部注册入口 -->
<RouterLink
to="/m/register"
class="register-entry"
custom
v-slot="{ navigate }"
>
<van-button
plain
type="primary"
block
round
size="small"
class="register-button"
@click="navigate"
>
没有账号立即注册
</van-button>
</RouterLink>
</section>
</main>
</template>
<style scoped>
/* ========== Vant 主题变量覆盖 ========== */
:root {
--van-primary-color: #1477ff;
--van-button-primary-background: #1477ff;
--van-button-primary-border-color: transparent;
}
/* ========== 页面容器 ========== */
.mobile-login-shell {
min-height: 100dvh;
box-sizing: border-box;
overflow: hidden;
background: #f5f7fa;
color: #151c28;
padding: 40px 24px;
}
.login-card {
display: flex;
min-height: calc(100dvh - 80px);
flex-direction: column;
justify-content: center;
}
/* ========== 品牌区域 ========== */
.login-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: #1477ff;
color: #ffffff;
font-size: 21px;
font-weight: 900;
box-shadow: 0 4px 12px rgba(20, 119, 255, 0.18);
}
.login-top p {
margin: 0 0 2px;
font-size: 20px;
font-weight: 900;
}
.login-top small,
.login-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);
}
/* ========== 标题区 ========== */
.login-eyebrow {
margin: 0 0 5px;
color: #02846e;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
}
.login-card h1 {
margin: 0;
font-size: 28px;
line-height: 1.15;
}
.login-subtitle {
margin: 7px 0 14px;
font-size: 13px;
line-height: 1.45;
}
/* ========== Vant Field 覆盖样式 ========== */
.login-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;
}
.login-field :deep(.van-field__control) {
font-size: 15px;
color: #151c28;
}
.login-field :deep(.van-field__label) {
width: auto;
margin-right: 8px;
color: #333b48;
font-weight: 700;
font-size: 13px;
}
.login-field :deep(.van-field__left-icon) {
color: #1777ff;
margin-right: 0;
}
.login-field :deep(.van-field__right-icon) {
color: #999;
}
.field-prefix {
color: #1777ff;
font-weight: 800;
font-size: 14px;
margin-right: 4px;
}
/* ========== 底部链接行 ========== */
.login-links {
display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 0 12px;
color: #566274;
font-size: 12px;
}
/* ========== 协议行 ========== */
.agreement-row {
display: flex;
align-items: center;
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;
}
/* ========== 登录按钮 ========== */
.login-button {
margin-top: 6px;
height: 46px;
font-size: 15px;
font-weight: 800;
background: #1477ff !important;
border: none !important;
box-shadow: 0 4px 12px rgba(20, 119, 255, 0.18);
}
/* ========== 注册入口按钮 ========== */
.register-button {
margin-top: 12px;
font-size: 13px;
font-weight: 800;
}
.register-entry {
text-decoration: none;
}
/* ========== 响应式 ========== */
@media (max-height: 620px) {
.mobile-login-shell {
padding-top: 10px;
padding-bottom: 10px;
}
.login-card {
min-height: calc(100dvh - 20px);
}
.login-top {
margin-bottom: 10px;
}
.monster-logo {
width: 42px;
height: 42px;
border-radius: 14px;
}
.auth-switch {
margin-bottom: 11px;
}
.login-card h1 {
font-size: 24px;
}
.login-field {
margin-top: 8px;
}
.login-button {
height: 42px;
margin-top: 10px;
}
}
@media (min-width: 520px) {
.mobile-login-shell {
max-width: 430px;
margin: 0 auto;
box-shadow: 0 0 0 1px rgba(23, 35, 61, 0.08);
}
}
</style>