Files
admin-web/src/modules/auth/pages/LoginView.vue
T

122 lines
5.7 KiB
Vue
Raw Normal View History

2026-07-30 17:59:43 +08:00
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
2026-07-30 17:59:43 +08:00
import { ArrowRight, LockKeyhole, ShieldCheck, UserRound } from '@lucide/vue'
2026-08-03 18:49:18 +08:00
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
2026-07-31 15:34:35 +08:00
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
2026-08-03 18:49:18 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
2026-07-30 17:59:43 +08:00
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const access = useAccessPreviewStore()
const loading = ref(false)
const loginError = ref<ApiErrorDetails | null>(null)
const formRef = ref<FormInstance>()
2026-07-30 17:59:43 +08:00
const form = reactive({
2026-07-31 15:34:35 +08:00
account: '',
password: '',
2026-07-30 17:59:43 +08:00
})
const rules: FormRules<typeof form> = {
account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
}
2026-07-30 17:59:43 +08:00
async function submit() {
loginError.value = null
const isValid = await formRef.value?.validate().catch(() => false)
if (!isValid) return
2026-07-30 17:59:43 +08:00
loading.value = true
try {
await auth.login(form.account, form.password)
access.initializeAfterLogin()
ElMessage.success('登录成功')
const requestedPath = sanitizeInternalPath(route.query.redirect)
const targetPath = requestedPath === '/'
? access.firstAccessiblePath ?? '/403'
: requestedPath
await router.replace(targetPath)
} catch (error) {
2026-08-14 18:50:11 +08:00
loginError.value = getApiErrorDetails(
error,
'登录失败,请检查账号和密码后重试',
{ preserveAuthenticationMessage: true },
)
2026-07-30 17:59:43 +08:00
ElMessage.error(loginError.value.message)
} finally {
loading.value = false
}
}
</script>
<template>
<div class="login-page">
<section class="login-intro">
2026-07-31 15:34:35 +08:00
<div class="login-brand"><img class="login-brand-logo" :src="baiyueLogoWhite" alt="柏悦物流" /></div>
2026-07-30 17:59:43 +08:00
<div>
2026-08-10 17:42:17 +08:00
<h1>柏悦物流管理后台</h1>
<p class="intro-copy">平台内部运营与管理系统</p>
2026-07-30 17:59:43 +08:00
</div>
<div class="security-note">
<ShieldCheck :size="22" />
<span>平台内部系统,请使用授权账号登录</span>
</div>
</section>
<section class="login-panel">
<div class="login-card">
2026-08-10 17:42:17 +08:00
<div class="login-mobile-brand"><img :src="baiyueLogoWhite" alt="柏悦物流" /></div>
2026-07-30 17:59:43 +08:00
<div class="login-card-icon"><LockKeyhole :size="24" /></div>
<h2>登录管理后台</h2>
<p>使用平台内部员工账号登录</p>
2026-08-10 17:42:17 +08:00
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" size="large" aria-label="平台员工登录" @submit.prevent="submit">
<el-form-item label="账号" prop="account" required>
2026-08-10 17:42:17 +08:00
<el-input v-model.trim="form.account" placeholder="请输入账号" :prefix-icon="UserRound" autocomplete="username" maxlength="100" autofocus />
2026-07-30 17:59:43 +08:00
</el-form-item>
<el-form-item label="密码" prop="password" required>
<el-input v-model="form.password" type="password" show-password placeholder="请输入密码" :prefix-icon="LockKeyhole" autocomplete="current-password" maxlength="128" />
2026-07-30 17:59:43 +08:00
</el-form-item>
<el-button class="login-submit" native-type="submit" type="primary" :loading="loading">
2026-07-30 17:59:43 +08:00
登录 <ArrowRight :size="17" />
</el-button>
</el-form>
2026-08-10 17:42:17 +08:00
<div v-if="loginError" class="login-error" role="alert" aria-live="polite"><p>{{ loginError.message }}</p><ApiErrorMeta :details="loginError" /></div>
2026-07-30 17:59:43 +08:00
</div>
</section>
</div>
</template>
2026-08-10 17:42:17 +08:00
<style scoped>
.login-page { min-width: 0; min-height: 100svh; background: var(--cc-surface); color: var(--cc-text); }
.login-intro { min-width: 0; background: var(--cc-navy); }
.login-intro h1 { max-width: 560px; margin: 16px 0 14px; font-size: 40px; line-height: 1.2; overflow-wrap: anywhere; }
.intro-copy { margin: 0; color: #cbd5e1; font-size: 15px; line-height: 1.7; overflow-wrap: anywhere; }
.login-panel { min-width: 0; background: var(--cc-surface-muted); }
.login-card { min-width: 0; padding: 32px; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-lg); background: var(--cc-surface); box-shadow: var(--cc-shadow-md); }
.login-card-icon { border-radius: var(--cc-radius-lg); background: var(--cc-primary-soft); }
.login-card h2, .login-card > p { overflow-wrap: anywhere; }
.login-mobile-brand { display: none; }
.login-submit { min-height: 44px; }
.login-error { min-width: 0; padding: 12px 14px; border: 1px solid #fecdca; border-radius: var(--cc-radius-md); background: var(--cc-danger-soft); color: var(--cc-danger); overflow-wrap: anywhere; }
.login-error :deep(*) { max-width: 100%; overflow-wrap: anywhere; }
.login-card :deep(.el-input__wrapper:focus-within) { box-shadow: 0 0 0 1px var(--cc-primary) inset, 0 0 0 3px var(--cc-primary-soft); }
@media (max-width: 960px) {
.login-panel { align-items: start; padding: 24px; }
.login-card { width: min(440px, 100%); margin: auto; padding: 28px; }
.login-mobile-brand { display: flex; min-height: 52px; align-items: center; margin: -28px -28px 28px; padding: 12px 20px; border-radius: var(--cc-radius-lg) var(--cc-radius-lg) 0 0; background: var(--cc-navy); }
.login-mobile-brand img { display: block; width: 154px; height: 48px; object-fit: contain; object-position: left center; }
}
@media (max-width: 360px) {
.login-panel { padding: 12px; }
.login-card { padding: 20px; }
.login-mobile-brand { margin: -20px -20px 24px; padding-inline: 16px; }
.login-card h2 { font-size: 24px; }
}
</style>