Files
admin-web/src/modules/auth/pages/LoginView.vue
T
2026-10-20 23:34:11 +08:00

128 lines
6.1 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'
import '@/styles/adminWorkbench.css'
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 admin-workbench">
<section class="login-intro">
<div class="login-brand workbench-brand-mark"><img class="login-brand-logo" :src="baiyueLogoWhite" alt="柏悦物流" /></div>
<div class="workbench-heading">
<h1 class="workbench-title">柏悦物流管理后台</h1>
<p class="login-intro-copy workbench-note">平台内部运营与管理系统</p>
</div>
<div class="login-security-note workbench-meta">
<ShieldCheck :size="22" aria-hidden="true" />
<span>平台内部系统,请使用授权账号登录</span>
</div>
</section>
<section class="login-panel">
<div class="login-card workbench-panel">
<div class="login-mobile-brand workbench-brand-mark"><img :src="baiyueLogoWhite" alt="柏悦物流" /></div>
<div class="login-card-icon workbench-icon"><LockKeyhole :size="24" aria-hidden="true" /></div>
<h2 class="workbench-title">登录管理后台</h2>
<p class="workbench-note">使用平台内部员工账号登录</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 workbench-notice workbench-tone" data-tone="rose" role="alert" aria-live="polite"><p>{{ loginError.message }}</p><ApiErrorMeta :details="loginError" /></div>
</div>
</section>
</div>
</template>
<style scoped>
.login-page { display: grid; min-width: 0; min-height: 100svh; grid-template-columns: minmax(420px, 1.05fr) minmax(420px, .95fr); background: var(--workbench-surface); color: var(--workbench-text); }
.login-intro { display: flex; min-width: 0; flex-direction: column; justify-content: space-between; padding: 46px clamp(42px, 7vw, 100px); }
.login-brand { display: flex; width: fit-content; min-height: 64px; align-items: center; padding: 8px 20px; }
.login-brand-logo { display: block; width: 205px; height: 64px; object-fit: contain; object-position: left center; }
.login-intro h1 { max-width: 560px; }
.login-intro-copy { max-width: 560px; }
.login-security-note > svg { flex: 0 0 auto; }
.login-panel { display: grid; min-width: 0; place-items: center; padding: 48px; background: var(--workbench-surface-tint); }
.login-card.workbench-panel { width: min(410px, 100%); min-width: 0; padding: 32px; }
.login-card h2 { margin-top: 22px; }
.login-card > p { margin: 8px 0 30px; }
.login-mobile-brand { display: none; }
.login-submit { width: 100%; min-height: 44px; }
.login-submit :deep(> span) { display: flex; align-items: center; gap: 8px; }
.login-error.workbench-notice { min-width: 0; margin-top: 14px; }
.login-error > p { margin: 0; }
.login-error :deep(*) { max-width: 100%; overflow-wrap: anywhere; }
@media (max-width: 960px) {
.login-page { grid-template-columns: 1fr; }
.login-intro { display: none; }
.login-panel { align-items: start; padding: 24px; }
.login-card.workbench-panel { width: min(440px, 100%); margin: auto; padding: 28px; }
.login-mobile-brand { display: flex; width: fit-content; min-height: 52px; align-items: center; margin-bottom: 28px; padding: 8px 16px; }
.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.workbench-panel { padding: 20px; }
.login-mobile-brand { margin-bottom: 24px; }
}
</style>