feat(admin-frontend): 重做登录回跳与仪表盘样式

重构管理端登录、主布局和仪表盘,统一为 Apple 风格
并移除高成本装饰层以提升页面流畅度。

补充仪表盘统计、趋势、排行和系统状态接口封装,
同时完善受保护路由的 redirect 回跳逻辑。
This commit is contained in:
yinjianm
2026-04-21 04:23:23 +08:00
parent 4cfda0fbf1
commit f68ba190a8
27 changed files with 2886 additions and 148 deletions
File diff suppressed because it is too large Load Diff
+173 -42
View File
@@ -1,15 +1,21 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { computed, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
import { Lock, Message, Right } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useAppStore } from '@/stores/app'
import { DEFAULT_AFTER_LOGIN, normalizeRedirectTarget } from '@/utils/navigation'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const app = useAppStore()
const formRef = ref<FormInstance>()
const loading = ref(false)
const errorMessage = ref('')
const form = reactive({
email: '',
@@ -17,6 +23,13 @@ const form = reactive({
remember: false,
})
const redirectTarget = computed(() => normalizeRedirectTarget(route.query.redirect))
const redirectHint = computed(() => (
redirectTarget.value === DEFAULT_AFTER_LOGIN
? '登录后将进入仪表盘总览'
: `登录后将返回 ${redirectTarget.value}`
))
const rules: FormRules = {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
@@ -33,12 +46,14 @@ async function onSubmit() {
if (!valid) return
loading.value = true
errorMessage.value = ''
try {
await auth.login(form.email, form.password, form.remember)
ElMessage.success('登录成功')
router.push('/dashboard')
await router.replace(redirectTarget.value)
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '登录失败'
errorMessage.value = msg
ElMessage.error(msg)
} finally {
loading.value = false
@@ -47,12 +62,34 @@ async function onSubmit() {
</script>
<template>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1 class="login-title">Xboard Admin</h1>
<p class="login-subtitle">管理后台</p>
<div class="login-page">
<section class="login-hero">
<p class="login-eyebrow">XBOARD ADMIN</p>
<h1 class="login-headline">管理后台像产品页一样安静</h1>
<p class="login-description">
页面重新回到 Apple 式的信息编排方式减少装饰层和不必要的视觉负担让登录入口更直接
</p>
<div class="login-meta">
<span>secure_path /{{ app.securePath || 'admin' }}</span>
<span>{{ redirectHint }}</span>
</div>
</section>
<section class="login-panel">
<div class="login-header">
<p class="login-panel-kicker">Sign in</p>
<h2 class="login-title">管理员登录</h2>
<p class="login-subtitle">使用具备后台权限的账号进入 {{ app.title }}</p>
</div>
<ElAlert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
class="login-alert"
/>
<ElForm
ref="formRef"
@@ -66,7 +103,7 @@ async function onSubmit() {
<ElInput
v-model="form.email"
placeholder="admin@example.com"
prefix-icon="Message"
:prefix-icon="Message"
/>
</ElFormItem>
@@ -75,74 +112,168 @@ async function onSubmit() {
v-model="form.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
:prefix-icon="Lock"
show-password
/>
</ElFormItem>
<ElFormItem>
<div class="login-form-meta">
<ElCheckbox v-model="form.remember">记住登录</ElCheckbox>
</ElFormItem>
<span class="login-meta-text">{{ redirectHint }}</span>
</div>
<ElFormItem>
<ElButton
type="primary"
:loading="loading"
class="login-btn"
@click="onSubmit"
>
{{ loading ? '登录中...' : '登 录' }}
</ElButton>
</ElFormItem>
<ElButton
type="primary"
:loading="loading"
class="login-btn"
@click="onSubmit"
>
<span>{{ loading ? '登录中...' : '继续' }}</span>
<ElIcon><Right /></ElIcon>
</ElButton>
</ElForm>
</div>
</section>
</div>
</template>
<style scoped>
.login-container {
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
padding: 20px;
gap: 28px;
padding: 40px 32px;
}
.login-card {
width: 100%;
.login-hero,
.login-panel {
width: min(100%, 560px);
}
.login-hero {
padding: 48px;
border-radius: 28px;
background: #000000;
color: var(--xboard-text-on-dark);
display: grid;
gap: 18px;
}
.login-eyebrow,
.login-panel-kicker {
margin: 0;
font-size: 12px;
letter-spacing: 0.28em;
text-transform: uppercase;
}
.login-headline {
margin: 0;
font-size: clamp(40px, 5vw, 56px);
line-height: 1.07;
letter-spacing: -0.28px;
color: #ffffff;
}
.login-description {
margin: 0;
max-width: 420px;
padding: 40px;
background: var(--el-bg-color);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
line-height: 1.47;
color: var(--xboard-text-on-dark-muted);
}
.login-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.login-meta span {
border: 1px solid rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.72);
border-radius: 999px;
padding: 10px 16px;
font-size: 12px;
}
.login-panel {
border-radius: 28px;
background: #ffffff;
box-shadow: var(--xboard-shadow);
padding: 40px 36px;
}
.login-header {
text-align: center;
margin-bottom: 32px;
display: grid;
gap: 8px;
margin-bottom: 24px;
}
.login-title {
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
margin: 0 0 8px;
margin: 0;
font-size: 34px;
line-height: 1.12;
letter-spacing: -0.32px;
color: var(--xboard-text-strong);
}
.login-subtitle {
font-size: 14px;
color: var(--el-text-color-secondary);
margin: 0;
color: var(--xboard-text-secondary);
}
.login-alert {
margin-bottom: 16px;
}
.login-form-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.login-meta-text {
color: var(--xboard-text-muted);
font-size: 12px;
}
.login-btn {
width: 100%;
height: 44px;
border-radius: 999px;
justify-content: center;
gap: 8px;
}
@media (max-width: 480px) {
.login-card {
@media (max-width: 980px) {
.login-page {
flex-direction: column;
align-items: stretch;
gap: 20px;
}
.login-hero,
.login-panel {
width: 100%;
}
}
@media (max-width: 720px) {
.login-page {
padding: 20px;
}
.login-hero,
.login-panel {
padding: 24px;
}
.login-form-meta {
flex-direction: column;
align-items: flex-start;
}
}
</style>