2026-07-30 17:59:43 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { reactive, ref } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-08-01 12:17:33 +08:00
|
|
|
|
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-10-20 23:34:11 +08:00
|
|
|
|
import '@/styles/adminWorkbench.css'
|
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)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
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
|
|
|
|
})
|
2026-08-01 12:17:33 +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
|
2026-08-01 12:17:33 +08:00
|
|
|
|
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>
|
2026-10-20 23:34:11 +08:00
|
|
|
|
<div class="login-page admin-workbench">
|
2026-07-30 17:59:43 +08:00
|
|
|
|
<section class="login-intro">
|
2026-10-20 23:34:11 +08:00
|
|
|
|
<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>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div>
|
2026-10-20 23:34:11 +08:00
|
|
|
|
<div class="login-security-note workbench-meta">
|
|
|
|
|
|
<ShieldCheck :size="22" aria-hidden="true" />
|
2026-07-30 17:59:43 +08:00
|
|
|
|
<span>平台内部系统,请使用授权账号登录</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="login-panel">
|
2026-10-20 23:34:11 +08:00
|
|
|
|
<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>
|
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">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<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>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<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>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<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-10-20 23:34:11 +08:00
|
|
|
|
<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>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-08-10 17:42:17 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-10-20 23:34:11 +08:00
|
|
|
|
.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; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
.login-mobile-brand { display: none; }
|
2026-10-20 23:34:11 +08:00
|
|
|
|
.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; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
.login-error :deep(*) { max-width: 100%; overflow-wrap: anywhere; }
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 960px) {
|
2026-10-20 23:34:11 +08:00
|
|
|
|
.login-page { grid-template-columns: 1fr; }
|
|
|
|
|
|
.login-intro { display: none; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
.login-panel { align-items: start; padding: 24px; }
|
2026-10-20 23:34:11 +08:00
|
|
|
|
.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; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
.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; }
|
2026-10-20 23:34:11 +08:00
|
|
|
|
.login-card.workbench-panel { padding: 20px; }
|
|
|
|
|
|
.login-mobile-brand { margin-bottom: 24px; }
|
2026-08-10 17:42:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|