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

483 lines
10 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 { readError } from '@/shared/utils/error'
import { ElMessage } from 'element-plus'
import { ChatLineRound, Iphone, Key } from '@element-plus/icons-vue'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { fetchAuthCaptcha, sendSmsCode, type AuthCaptcha } from '@/features/auth/api/auth'
import { useSessionStore } from '@/stores/session'
const router = useRouter()
const session = useSessionStore()
const loading = ref(false)
const sending = ref(false)
const captchaLoading = ref(false)
const captcha = ref<AuthCaptcha | null>(null)
const countDown = ref(0)
const form = reactive({
phone: '',
captchaCode: '',
code: '',
})
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
}
})
onMounted(loadCaptcha)
async function loadCaptcha() {
captchaLoading.value = true
try {
captcha.value = await fetchAuthCaptcha()
form.captchaCode = ''
} catch (error) {
ElMessage.error(readError(error, '图形验证码加载失败'))
} finally {
captchaLoading.value = false
}
}
async function handleSendCode() {
if (!form.phone.trim()) {
ElMessage.warning('请输入手机号')
return
}
if (!captcha.value?.captcha_id || !form.captchaCode.trim()) {
ElMessage.warning('请输入图形验证码')
return
}
sending.value = true
try {
await sendSmsCode(form.phone, captcha.value.captcha_id, form.captchaCode)
ElMessage.success('验证码已发送,请注意查收')
startCountDown()
} catch (error) {
ElMessage.error(readError(error, '验证码发送失败'))
} finally {
await loadCaptcha()
sending.value = false
}
}
async function refreshCaptcha() {
if (captchaLoading.value) {
return
}
await loadCaptcha()
}
async function handleLogin() {
loading.value = true
try {
await session.login(form.phone, form.code)
ElMessage.success('登录成功')
await router.push('/')
} catch (error) {
ElMessage.error(readError(error, '登录失败'))
} finally {
loading.value = false
}
}
</script>
<template>
<section class="login-shell">
<div class="login-card">
<div class="login-left">
<div class="brand-lockup">
<div class="brand-logo"></div>
<strong>大锤商行</strong>
</div>
<div class="login-hero">
<h2>安全租号<br />畅享游戏</h2>
<p>
高效安全的哈夫币账号交易平台<br />
7×24 小时智能订单系统随时为您服务
</p>
</div>
<div class="login-footer">
<span>© 哈夫币租号平台</span>
</div>
</div>
<div class="login-right">
<div class="login-header">
<p class="eyebrow">SMS Login</p>
<h1>欢迎回来</h1>
<p class="subtitle">登录后即可发布或租赁账号</p>
</div>
<el-form class="user-form" label-position="top" @submit.prevent>
<el-form-item label="手机号">
<el-input
v-model="form.phone"
maxlength="11"
placeholder="请输入手机号"
size="large"
:prefix-icon="Iphone"
/>
</el-form-item>
<el-form-item label="图形验证码">
<div class="user-captcha-row">
<el-input
v-model="form.captchaCode"
maxlength="4"
placeholder="请输入图形验证码"
size="large"
:prefix-icon="Key"
@keyup.enter="handleSendCode"
/>
<button
class="user-captcha-image"
type="button"
:disabled="captchaLoading"
aria-label="刷新图形验证码"
@click="refreshCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
<span v-else>刷新</span>
</button>
</div>
</el-form-item>
<el-form-item label="验证码">
<div class="user-code-row">
<el-input
v-model="form.code"
maxlength="6"
placeholder="6 位验证码"
size="large"
:prefix-icon="ChatLineRound"
@keyup.enter="handleLogin"
/>
<el-button
size="large"
:disabled="countDown > 0 || sending"
:loading="sending"
@click="handleSendCode"
>
{{ countDown > 0 ? `${countDown}s` : sending ? '发送中' : '发送验证码' }}
</el-button>
</div>
</el-form-item>
<el-button
class="user-login-btn"
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
>
登录
</el-button>
<p class="login-notice">未收到验证码时请稍后重试或联系客服处理</p>
</el-form>
</div>
</div>
</section>
</template>
<style scoped>
.login-shell {
display: grid;
flex: 1;
place-items: center;
margin: -20px -24px;
padding: 32px 24px;
}
.login-card {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr 420px;
width: min(940px, 100%);
min-height: 520px;
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 20px;
background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(20px);
box-shadow:
0 24px 80px rgba(17, 24, 39, 0.08),
0 1px 0 rgba(255, 255, 255, 0.6) inset;
overflow: hidden;
}
/* ========== 左侧品牌区 ========== */
.login-left {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px 36px;
background:
radial-gradient(circle at 30% 20%, rgba(20, 119, 255, 0.08), transparent 50%),
radial-gradient(circle at 80% 90%, rgba(109, 40, 217, 0.06), transparent 50%),
linear-gradient(160deg, rgba(20, 119, 255, 0.06), rgba(109, 40, 217, 0.03));
}
.brand-lockup {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 10px;
background: linear-gradient(135deg, #1477ff, #2f8dff);
box-shadow: 0 8px 24px rgba(20, 119, 255, 0.22);
color: #fff;
font-size: 15px;
font-weight: 900;
}
.brand-lockup strong {
color: #0f172a;
font-size: 15px;
font-weight: 800;
letter-spacing: 0.3px;
}
.login-hero h2 {
margin: 0 0 14px;
color: #0f172a;
font-size: 28px;
font-weight: 800;
line-height: 1.25;
}
.login-hero p {
margin: 0;
color: #475569;
font-size: 14px;
line-height: 1.7;
max-width: 300px;
}
.login-footer span {
color: #94a3b8;
font-size: 12px;
}
/* ========== 右侧表单区 ========== */
.login-right {
display: flex;
flex-direction: column;
justify-content: center;
padding: 36px 32px;
background: rgba(255, 255, 255, 0.45);
}
.login-header {
margin-bottom: 24px;
}
.login-header .eyebrow {
margin: 0 0 8px;
color: #1477ff;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
}
.login-header h1 {
margin: 0;
color: #0f172a;
font-size: 24px;
font-weight: 800;
line-height: 1.2;
}
.login-header .subtitle {
margin: 8px 0 0;
color: #64748b;
font-size: 13px;
}
.user-form :deep(.el-form-item__label) {
color: #334155;
font-size: 13px;
font-weight: 600;
padding-bottom: 6px;
}
.user-form :deep(.el-input__wrapper) {
background: #ffffff;
box-shadow: 0 0 0 1px #e2e8f0 inset;
border-radius: 10px;
padding: 0 12px;
transition:
box-shadow 0.2s,
background 0.2s;
}
.user-form :deep(.el-input__wrapper:hover) {
background: #ffffff;
}
.user-form :deep(.el-input__wrapper.is-focus) {
background: #ffffff;
box-shadow:
0 0 0 1px rgba(20, 119, 255, 0.45) inset,
0 0 0 3px rgba(20, 119, 255, 0.08);
}
.user-form :deep(.el-input__inner) {
color: #0f172a;
font-size: 14px;
height: 44px;
}
.user-form :deep(.el-input__inner::placeholder) {
color: #94a3b8;
}
.user-form :deep(.el-input__prefix-inner) {
color: #94a3b8;
}
.user-code-row {
display: grid;
grid-template-columns: 1fr 120px;
gap: 10px;
width: 100%;
}
.user-captcha-row {
display: grid;
grid-template-columns: 1fr 132px;
gap: 10px;
width: 100%;
}
.user-captcha-image {
display: grid;
height: 46px;
place-items: center;
overflow: hidden;
border: 1px solid #dbe5f0;
border-radius: 10px;
background: #f8fafc;
color: #1477ff;
cursor: pointer;
font-size: 13px;
font-weight: 700;
padding: 0;
}
.user-captcha-image:disabled {
cursor: wait;
opacity: 0.7;
}
.user-captcha-image img {
display: block;
width: 132px;
height: 44px;
}
.user-code-row .el-button {
height: 46px;
border-radius: 10px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.3px;
background: #ffffff;
border-color: #1477ff;
color: #1477ff;
box-shadow: none;
transition:
background 0.2s,
color 0.2s;
}
.user-code-row .el-button:hover:not(:disabled) {
background: #1477ff;
border-color: #1477ff;
color: #ffffff;
}
.user-code-row .el-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.user-login-btn {
width: 100%;
height: 46px;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
margin-top: 8px;
letter-spacing: 0.5px;
background: linear-gradient(135deg, #1477ff, #2f8dff);
border: none;
box-shadow: 0 10px 28px rgba(20, 119, 255, 0.22);
transition:
transform 0.15s,
box-shadow 0.2s;
}
.user-login-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 36px rgba(20, 119, 255, 0.28);
}
.login-notice {
margin: 18px 0 0;
color: #94a3b8;
font-size: 12px;
text-align: center;
line-height: 1.5;
}
/* ========== 响应式 ========== */
@media (max-width: 860px) {
.login-card {
grid-template-columns: 1fr;
max-width: 420px;
}
.login-left {
display: none;
}
.login-right {
padding: 32px 28px;
}
.login-shell {
padding: 24px 16px;
}
}
@media (max-width: 480px) {
.login-shell {
padding: 16px 12px;
}
.login-right {
padding: 24px 20px;
}
}
@media (prefers-reduced-motion: reduce) {
.user-login-btn {
transition: none;
}
}
</style>