library-picturebook-activity/lesingle-creation-frontend/src/views/auth/Login.vue
En 98e9ad1d28 feat(前端): 测试环境登录框支持自动填充测试账号
通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用,
使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 17:03:22 +08:00

546 lines
12 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.

<template>
<div class="login-container">
<!-- Animated Background -->
<div class="bg-shapes">
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<div class="shape shape-3"></div>
<div class="shape shape-4"></div>
</div>
<div class="login-card">
<div class="login-header">
<img src="@/assets/images/logo-icon.png" alt="乐绘世界" class="login-logo" />
<h2>乐绘世界创想活动乐园</h2>
<p class="login-subtitle">{{ tenantName ? tenantName + ' — 管理端登录' : '管理端登录' }}</p>
</div>
<!-- 开发环境快捷切换 -->
<div v-if="isDev" class="tenant-tabs">
<div v-for="tab in tenantTabs" :key="tab.code" :class="['tenant-tab', { active: activeTab === tab.code }]"
@click="switchTab(tab.code)">
<component :is="tab.icon" />
<span>{{ tab.name }}</span>
</div>
</div>
<a-form :model="form" :rules="rules" @finish="handleSubmit" class="login-form" layout="vertical">
<a-form-item name="username" label="用户名">
<a-input v-model:value="form.username" size="large" placeholder="请输入用户名" class="custom-input">
<template #prefix>
<UserOutlined class="input-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item name="password" label="密码">
<a-input-password v-model:value="form.password" size="large" placeholder="请输入密码" class="custom-input">
<template #prefix>
<LockOutlined class="input-icon" />
</template>
</a-input-password>
</a-form-item>
<div class="form-options">
<a-checkbox v-model:checked="rememberMe">记住我</a-checkbox>
</div>
<a-form-item>
<a-button type="primary" html-type="submit" size="large" :loading="loading" block class="login-btn">
登录
</a-button>
</a-form-item>
</a-form>
<!-- 公众端入口 -->
<div class="public-entry">
<span>参与活动</span>
<a @click="$router.push('/p/login')">前往公众端</a>
</div>
<!-- 开发环境提示 -->
<div v-if="isDev" class="dev-hint">
<p>开发模式:点击上方标签快速切换管理端</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue"
import { useRouter, useRoute } from "vue-router"
import {
UserOutlined,
LockOutlined,
SafetyOutlined,
BankOutlined,
TrophyOutlined,
} from "@ant-design/icons-vue"
import { useAuthStore } from "@/stores/auth"
import { message } from "ant-design-vue"
import type { Rule } from "ant-design-vue/es/form"
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const loading = ref(false)
const rememberMe = ref(false)
const isDev = import.meta.env.DEV || import.meta.env.VITE_AUTO_FILL_TEST === 'true'
// 开发环境快捷切换 — 按新架构设计
const tenantTabs = [
{ code: "super", name: "平台超管", icon: SafetyOutlined, username: "admin", password: "admin123" },
{ code: "gdlib", name: "广东省图", icon: BankOutlined, username: "admin", password: "admin123" },
{ code: "judge", name: "评委端", icon: TrophyOutlined, username: "admin", password: "admin123" },
]
const activeTab = ref("")
// 租户名称映射
const tenantNameMap: Record<string, string> = {
super: "平台管理",
gdlib: "广东省立中山图书馆",
judge: "评委",
}
const form = ref({
username: "",
password: "",
tenantCode: "",
})
// 根据 tenantCode 显示机构名
const tenantName = computed(() => {
return tenantNameMap[form.value.tenantCode] || ""
})
// 切换标签
const switchTab = (code: string) => {
activeTab.value = code
const tab = tenantTabs.find(t => t.code === code)
if (tab) {
form.value.username = tab.username
form.value.password = tab.password
form.value.tenantCode = tab.code
}
}
onMounted(() => {
// 从 URL 中提取租户编码,如 /gdlib/login → tenantCode = gdlib
const pathMatch = route.path.match(/^\/([^/]+)\/login$/)
const tenantCodeFromPath = pathMatch ? pathMatch[1] : null
if (tenantCodeFromPath) {
form.value.tenantCode = tenantCodeFromPath
// 开发模式下自动匹配标签
const matchedTab = tenantTabs.find(t => t.code === tenantCodeFromPath)
if (matchedTab) {
activeTab.value = tenantCodeFromPath
if (isDev) {
form.value.username = matchedTab.username
form.value.password = matchedTab.password
}
}
} else if (isDev) {
// 开发模式默认选中超管
switchTab("super")
}
})
const rules: Record<string, Rule[]> = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
}
const handleSubmit = async () => {
loading.value = true
try {
const loginData: any = {
username: form.value.username,
password: form.value.password,
}
if (form.value.tenantCode) {
loginData.tenantCode = form.value.tenantCode
}
await authStore.login(loginData)
const userTenantCode = authStore.user?.tenantCode || form.value.tenantCode
if (!userTenantCode) {
message.error("登录失败:无法确定机构信息")
return
}
let redirectPath = route.query.redirect as string
if (!redirectPath) {
redirectPath = `/${userTenantCode}`
} else {
const redirectTenantCode = redirectPath.match(/^\/([^/]+)/)?.[1]
if (redirectTenantCode && redirectTenantCode !== userTenantCode) {
redirectPath = redirectPath.replace(
`/${redirectTenantCode}`,
`/${userTenantCode}`
)
} else if (!redirectTenantCode) {
const cleanPath = redirectPath.startsWith("/")
? redirectPath.slice(1)
: redirectPath
redirectPath = `/${userTenantCode}/${cleanPath}`
}
}
router.push(redirectPath)
} catch (error) {
console.error("Login failed:", error)
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
$primary: #6366f1;
$primary-light: #818cf8;
$coral: #f97066;
$rose: #ec4899;
$amber: #f59e0b;
$teal: #14b8a6;
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #eef2ff 0%, #fdf2f8 50%, #ecfdf5 100%);
position: relative;
overflow: hidden;
padding: 20px;
}
// 背景装饰 彩色柔和光斑
.bg-shapes {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
.shape {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.35;
animation: float 25s ease-in-out infinite;
}
.shape-1 {
width: 500px;
height: 500px;
background: linear-gradient(135deg, $primary, $primary-light);
top: -150px;
left: -100px;
}
.shape-2 {
width: 450px;
height: 450px;
background: linear-gradient(135deg, $coral, $rose);
bottom: -120px;
right: -100px;
animation-delay: -6s;
}
.shape-3 {
width: 350px;
height: 350px;
background: linear-gradient(135deg, $teal, #38bdf8);
top: 40%;
left: 55%;
animation-delay: -12s;
}
.shape-4 {
width: 280px;
height: 280px;
background: linear-gradient(135deg, $amber, $coral);
top: 15%;
right: 10%;
animation-delay: -18s;
}
}
@keyframes float {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(25px, -25px) scale(1.04);
}
50% {
transform: translate(-15px, 15px) scale(0.96);
}
75% {
transform: translate(-25px, -15px) scale(1.02);
}
}
// 登录卡片
.login-card {
width: 100%;
max-width: 440px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(24px);
border-radius: 24px;
padding: 44px 40px;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.08),
0 20px 48px rgba(99, 102, 241, 0.06);
border: 1px solid rgba(255, 255, 255, 0.6);
position: relative;
z-index: 1;
}
.login-header {
text-align: center;
margin-bottom: 32px;
.login-logo {
width: 72px;
height: 72px;
object-fit: contain;
margin: 0 auto 18px;
display: block;
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.15));
}
h2 {
font-size: 24px;
font-weight: 800;
background: linear-gradient(135deg, #1e1b4b 0%, $primary 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 8px 0;
letter-spacing: 1px;
}
p {
color: #9ca3af;
margin: 0;
font-size: 14px;
font-weight: 500;
}
}
// 端切换标签
.tenant-tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
flex-wrap: wrap;
justify-content: center;
}
.tenant-tab {
display: flex;
align-items: center;
gap: 5px;
padding: 7px 14px;
border-radius: 20px;
background: rgba(99, 102, 241, 0.06);
color: #6b7280;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.25s ease;
border: 1.5px solid transparent;
&:hover {
background: rgba(99, 102, 241, 0.10);
color: $primary;
}
&.active {
background: linear-gradient(135deg, $primary 0%, $rose 100%);
color: white;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3);
border-color: transparent;
}
.anticon {
font-size: 13px;
}
}
// 表单
.login-form {
.custom-input {
height: 46px;
border-radius: 12px;
border: 1.5px solid #e5e1f5;
transition: all 0.25s ease;
background: rgba(255, 255, 255, 0.6);
&:hover {
border-color: $primary-light;
}
&:focus-within {
border-color: $primary;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.08);
background: #fff;
}
:deep(.ant-input) {
font-size: 14px;
font-weight: 500;
}
:deep(.ant-input-password-icon) {
color: #9ca3af;
}
}
.input-icon {
color: #9ca3af;
font-size: 15px;
}
:deep(.ant-form-item-label > label) {
color: #374151;
font-weight: 600;
font-size: 13px;
}
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
:deep(.ant-checkbox-wrapper) {
color: #6b7280;
font-size: 13px;
}
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: $primary;
border-color: $primary;
border-radius: 4px;
}
.forgot-link {
color: $primary;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: color 0.25s ease;
&:hover {
color: $rose;
}
}
}
.login-btn {
height: 46px;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
background: linear-gradient(135deg, $primary 0%, $rose 100%) !important;
border: none !important;
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3);
transition: all 0.25s ease;
letter-spacing: 1px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(99, 102, 241, 0.4);
}
&:active {
transform: scale(0.98);
}
}
// 开发环境提示
.public-entry {
text-align: center;
margin-top: 20px;
font-size: 13px;
color: #9ca3af;
a {
color: $primary;
font-weight: 600;
cursor: pointer;
margin-left: 4px;
&:hover {
color: $rose;
}
}
}
.login-subtitle {
color: #9ca3af !important;
}
.dev-hint {
margin-top: 20px;
padding: 10px 14px;
background: rgba($amber, 0.08);
border: 1px solid rgba($amber, 0.2);
border-radius: 12px;
text-align: center;
p {
margin: 0;
color: #92400e;
font-size: 12px;
font-weight: 500;
}
}
@media (max-width: 480px) {
.login-container {
padding: 16px;
}
.login-card {
padding: 32px 24px;
border-radius: 20px;
}
.login-header {
.login-logo {
width: 56px;
height: 56px;
}
h2 {
font-size: 20px;
}
}
.tenant-tabs {
gap: 6px;
}
.tenant-tab {
padding: 6px 10px;
font-size: 11px;
}
}
</style>