Files
admin-web/src/modules/auth/pages/LoginView.vue
T
2026-08-14 18:50:11 +08:00

122 lines
5.7 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 { reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ArrowRight, LockKeyhole, ShieldCheck, UserRound } from '@lucide/vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
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'
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>()
const form = reactive({
account: '',
password: '',
})
const rules: FormRules<typeof form> = {
account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
}
async function submit() {
loginError.value = null
const isValid = await formRef.value?.validate().catch(() => false)
if (!isValid) return
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) {
loginError.value = getApiErrorDetails(
error,
'登录失败,请检查账号和密码后重试',
{ preserveAuthenticationMessage: true },
)
ElMessage.error(loginError.value.message)
} finally {
loading.value = false
}
}
</script>
<template>
<div class="login-page">
<section class="login-intro">
<div class="login-brand"><img class="login-brand-logo" :src="baiyueLogoWhite" alt="柏悦物流" /></div>
<div>
<h1>柏悦物流管理后台</h1>
<p class="intro-copy">平台内部运营与管理系统</p>
</div>
<div class="security-note">
<ShieldCheck :size="22" />
<span>平台内部系统,请使用授权账号登录</span>
</div>
</section>
<section class="login-panel">
<div class="login-card">
<div class="login-mobile-brand"><img :src="baiyueLogoWhite" alt="柏悦物流" /></div>
<div class="login-card-icon"><LockKeyhole :size="24" /></div>
<h2>登录管理后台</h2>
<p>使用平台内部员工账号登录</p>
<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>
<el-input v-model.trim="form.account" placeholder="请输入账号" :prefix-icon="UserRound" autocomplete="username" maxlength="100" autofocus />
</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" />
</el-form-item>
<el-button class="login-submit" native-type="submit" type="primary" :loading="loading">
登录 <ArrowRight :size="17" />
</el-button>
</el-form>
<div v-if="loginError" class="login-error" role="alert" aria-live="polite"><p>{{ loginError.message }}</p><ApiErrorMeta :details="loginError" /></div>
</div>
</section>
</div>
</template>
<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>