Files
hfb_sys/frontend/src/features/admin/views/AdminLoginView.vue
T
yml f410be3797 代码格式化: prettier --write 统一代码风格
39 个文件按 .prettierrc.json (printWidth 100 / no semi / singleQuote) 统一格式, 纯空白与换行调整, 无逻辑变更。为接入代码门禁做准备, 让 npm run check (format:check + lint:strict + typecheck) 干净通过。
2026-06-14 16:24:01 +08:00

420 lines
9.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 { readError } from '@/shared/utils/error'
import { ElMessage } from 'element-plus'
import { Lock, User } from '@element-plus/icons-vue'
import { onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchAdminCaptcha, type AdminCaptcha } from '@/features/admin'
import { useAdminSessionStore } from '@/stores/adminSession'
import { ADMIN_DASHBOARD_PATH, isAdminPath } from '@/shared/utils/adminPath'
const router = useRouter()
const route = useRoute()
const adminSession = useAdminSessionStore()
const loading = ref(false)
const captchaLoading = ref(false)
const captcha = ref<AdminCaptcha | null>(null)
const form = reactive({
username: '',
password: '',
captchaCode: '',
})
onMounted(loadCaptcha)
async function loadCaptcha() {
captchaLoading.value = true
try {
captcha.value = await fetchAdminCaptcha()
form.captchaCode = ''
} catch (error) {
ElMessage.error(readError(error, '验证码加载失败'))
} finally {
captchaLoading.value = false
}
}
async function handleLogin() {
loading.value = true
try {
await adminSession.login(
form.username,
form.password,
captcha.value?.captcha_id || '',
form.captchaCode
)
ElMessage.success('后台登录成功')
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : ''
await router.push(isAdminPath(redirect) ? redirect : ADMIN_DASHBOARD_PATH)
} catch (error) {
ElMessage.error(readError(error, '后台登录失败'))
await loadCaptcha()
} finally {
loading.value = false
}
}
</script>
<template>
<section class="admin-login-shell">
<div class="admin-login-glow" aria-hidden="true" />
<div class="admin-login-glow secondary" aria-hidden="true" />
<div class="admin-login-card">
<div class="admin-login-left">
<div class="brand-lockup">
<div class="brand-logo">H</div>
<strong>大锤商行 - 后台管理</strong>
</div>
<div class="admin-login-hero">
<h2>高效管理<br />从容掌控</h2>
<p>统一的后台管理系统,助您轻松处理订单、用户与运营数据。</p>
</div>
<div class="admin-login-footer">
<span>© 哈夫币租号平台</span>
</div>
</div>
<div class="admin-login-right">
<div class="admin-login-header">
<p class="eyebrow">Admin Login</p>
<h1>管理员登录</h1>
</div>
<el-form class="admin-form" label-position="top" @submit.prevent>
<el-form-item label="用户名">
<el-input
v-model="form.username"
placeholder="请输入管理员用户名"
size="large"
:prefix-icon="User"
/>
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="form.password"
type="password"
show-password
placeholder="请输入管理员密码"
size="large"
:prefix-icon="Lock"
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item label="验证码">
<div class="admin-captcha-row">
<el-input
v-model="form.captchaCode"
maxlength="4"
placeholder="请输入验证码"
size="large"
@keyup.enter="handleLogin"
/>
<button
class="captcha-image-button"
type="button"
:disabled="captchaLoading"
@click="loadCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="验证码" />
<span v-else>刷新</span>
</button>
</div>
</el-form-item>
<el-button
class="admin-login-btn"
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
>
登录后台
</el-button>
</el-form>
<RouterLink class="back-home" to="/">← 返回首页</RouterLink>
</div>
</div>
</section>
</template>
<style scoped>
.admin-login-shell {
position: relative;
display: grid;
min-height: 100vh;
place-items: center;
overflow: hidden;
background:
radial-gradient(circle at 18% 20%, rgba(20, 119, 255, 0.14), transparent 42%),
radial-gradient(circle at 82% 80%, rgba(109, 40, 217, 0.12), transparent 42%),
linear-gradient(180deg, #0b1120 0%, #0f172a 60%, #111827 100%);
padding: 24px;
}
.admin-login-glow {
position: absolute;
top: -10%;
left: -10%;
width: 50vw;
height: 50vw;
border-radius: 50%;
background: radial-gradient(circle, rgba(20, 119, 255, 0.22), transparent 65%);
filter: blur(80px);
pointer-events: none;
}
.admin-login-glow.secondary {
top: auto;
left: auto;
right: -10%;
bottom: -10%;
background: radial-gradient(circle, rgba(109, 40, 217, 0.18), transparent 65%);
}
.admin-login-card {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr 420px;
width: min(960px, 100%);
min-height: 560px;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20px;
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(24px);
box-shadow:
0 24px 80px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
overflow: hidden;
}
/* ========== 左侧品牌区 ========== */
.admin-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.12), transparent 50%),
radial-gradient(circle at 80% 90%, rgba(109, 40, 217, 0.1), transparent 50%),
linear-gradient(160deg, rgba(20, 119, 255, 0.1), rgba(109, 40, 217, 0.06));
}
.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.25);
color: #fff;
font-size: 15px;
font-weight: 900;
}
.brand-lockup strong {
color: #f8fafc;
font-size: 15px;
font-weight: 800;
letter-spacing: 0.5px;
}
.admin-login-hero h2 {
margin: 0 0 14px;
color: #f1f5f9;
font-size: 28px;
font-weight: 800;
line-height: 1.25;
}
.admin-login-hero p {
margin: 0;
color: #94a3b8;
font-size: 14px;
line-height: 1.7;
max-width: 320px;
}
.admin-login-footer span {
color: #475569;
font-size: 12px;
}
/* ========== 右侧表单区 ========== */
.admin-login-right {
display: flex;
flex-direction: column;
justify-content: center;
padding: 36px 32px;
background: rgba(255, 255, 255, 0.03);
}
.admin-login-header {
margin-bottom: 24px;
}
.admin-login-header .eyebrow {
margin: 0 0 8px;
color: #1477ff;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
}
.admin-login-header h1 {
margin: 0;
color: #f8fafc;
font-size: 24px;
font-weight: 800;
line-height: 1.2;
}
.admin-form :deep(.el-form-item__label) {
color: #cbd5e1;
font-size: 13px;
font-weight: 600;
padding-bottom: 6px;
}
.admin-form :deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.04);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
border-radius: 10px;
padding: 0 12px;
transition:
box-shadow 0.2s,
background 0.2s;
}
.admin-form :deep(.el-input__wrapper:hover) {
background: rgba(255, 255, 255, 0.06);
}
.admin-form :deep(.el-input__wrapper.is-focus) {
background: rgba(255, 255, 255, 0.06);
box-shadow:
0 0 0 1px rgba(20, 119, 255, 0.45) inset,
0 0 0 3px rgba(20, 119, 255, 0.08);
}
.admin-form :deep(.el-input__inner) {
color: #f1f5f9;
font-size: 14px;
height: 44px;
}
.admin-form :deep(.el-input__inner::placeholder) {
color: #64748b;
}
.admin-form :deep(.el-input__prefix-inner) {
color: #64748b;
}
.admin-captcha-row {
display: grid;
grid-template-columns: 1fr 132px;
gap: 10px;
width: 100%;
}
.captcha-image-button {
display: flex;
align-items: center;
justify-content: center;
height: 44px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
background: rgba(255, 255, 255, 0.04);
padding: 0;
cursor: pointer;
overflow: hidden;
transition:
background 0.2s,
border-color 0.2s;
}
.captcha-image-button:hover {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.14);
}
.captcha-image-button:disabled {
cursor: wait;
opacity: 0.6;
}
.captcha-image-button img {
display: block;
width: 132px;
height: 44px;
}
.captcha-image-button span {
color: #94a3b8;
font-size: 13px;
}
.admin-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.25);
transition:
transform 0.15s,
box-shadow 0.2s;
}
.admin-login-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 36px rgba(20, 119, 255, 0.32);
}
.back-home {
display: block;
margin-top: 18px;
text-align: center;
color: #64748b;
font-size: 13px;
text-decoration: none;
transition: color 0.2s;
}
.back-home:hover {
color: #94a3b8;
}
/* ========== 响应式 ========== */
@media (max-width: 860px) {
.admin-login-card {
grid-template-columns: 1fr;
max-width: 420px;
}
.admin-login-left {
display: none;
}
.admin-login-right {
padding: 32px 28px;
}
}
@media (max-width: 480px) {
.admin-login-shell {
padding: 16px;
}
.admin-login-right {
padding: 24px 20px;
}
}
@media (prefers-reduced-motion: reduce) {
.admin-login-btn {
transition: none;
}
}
</style>