Files
ggl/src/pages/admin/Login.vue
2025-09-23 07:35:11 +00:00

929 lines
20 KiB
Vue
Executable File
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="admin-login-page">
<!-- 背景装饰 -->
<div class="bg-decoration">
<div class="decoration-pattern"></div>
<div class="floating-elements">
<div class="element" v-for="i in 6" :key="i"></div>
</div>
</div>
<!-- 登录容器 -->
<div class="login-container">
<!-- 品牌区域 -->
<div class="brand-section">
<div class="brand-logo">
<img src="/logo.svg" alt="梦回高句丽" class="logo-img" />
</div>
<h1 class="brand-title">梦回高句丽</h1>
<p class="brand-subtitle">管理后台</p>
<div class="brand-desc">
<p>探索古代文明的数字化管理平台</p>
<p>传承历史文化连接过去与未来</p>
</div>
</div>
<!-- 登录表单 -->
<div class="login-form-section">
<div class="form-header">
<h2 class="form-title">管理员登录</h2>
<p class="form-subtitle">请使用管理员账号登录系统</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<!-- 用户名输入 -->
<div class="form-group">
<label class="form-label">
<i class="icon icon-user"></i>
用户名
</label>
<div class="input-wrapper">
<input
v-model="loginForm.username"
type="text"
class="form-input"
placeholder="请输入管理员用户名"
:class="{ error: errors.username }"
@blur="validateUsername"
@input="clearError('username')"
required
/>
<div class="input-icon">
<i class="icon icon-user"></i>
</div>
</div>
<div class="error-message" v-if="errors.username">
{{ errors.username }}
</div>
</div>
<!-- 密码输入 -->
<div class="form-group">
<label class="form-label">
<i class="icon icon-lock"></i>
密码
</label>
<div class="input-wrapper">
<input
v-model="loginForm.password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
placeholder="请输入密码"
:class="{ error: errors.password }"
@blur="validatePassword"
@input="clearError('password')"
required
/>
<div class="input-icon">
<i class="icon icon-lock"></i>
</div>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
>
<i class="icon" :class="showPassword ? 'icon-eye-off' : 'icon-eye'"></i>
</button>
</div>
<div class="error-message" v-if="errors.password">
{{ errors.password }}
</div>
</div>
<!-- 验证码输入 -->
<div class="form-group">
<label class="form-label">
<i class="icon icon-shield"></i>
验证码
</label>
<div class="captcha-wrapper">
<div class="input-wrapper captcha-input">
<input
v-model="loginForm.captcha"
type="text"
class="form-input"
placeholder="请输入验证码"
:class="{ error: errors.captcha }"
@blur="validateCaptcha"
@input="clearError('captcha')"
maxlength="4"
required
/>
<div class="input-icon">
<i class="icon icon-shield"></i>
</div>
</div>
<div class="captcha-image" @click="refreshCaptcha">
<canvas ref="captchaCanvas" width="120" height="40"></canvas>
<div class="captcha-refresh">
<i class="icon icon-refresh-cw"></i>
</div>
</div>
</div>
<div class="error-message" v-if="errors.captcha">
{{ errors.captcha }}
</div>
</div>
<!-- 记住登录 -->
<div class="form-options">
<label class="checkbox-wrapper">
<input
v-model="loginForm.remember"
type="checkbox"
class="checkbox-input"
/>
<span class="checkbox-custom"></span>
<span class="checkbox-label">记住登录状态</span>
</label>
<a href="#" class="forgot-link" @click.prevent="handleForgotPassword">
忘记密码
</a>
</div>
<!-- 登录按钮 -->
<button
type="submit"
class="login-btn"
:class="{ loading: isLoading }"
:disabled="isLoading || !isFormValid"
>
<div class="btn-content">
<i class="icon icon-loader" v-if="isLoading"></i>
<i class="icon icon-log-in" v-else></i>
<span>{{ isLoading ? '登录中...' : '立即登录' }}</span>
</div>
</button>
<!-- 安全提示 -->
<div class="security-notice">
<i class="icon icon-info"></i>
<div class="notice-content">
<p>为了您的账户安全请妥善保管登录凭证</p>
<p>如发现异常登录行为请及时联系系统管理员</p>
</div>
</div>
</form>
</div>
</div>
<!-- 页脚信息 -->
<div class="login-footer">
<div class="footer-links">
<a href="#">使用条款</a>
<a href="#">隐私政策</a>
<a href="#">技术支持</a>
</div>
<div class="footer-copyright">
<p>&copy; 2024 梦回高句丽. 保留所有权利.</p>
<p>Powered by 高句丽文化数字化平台</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { authApi } from '@/api'
import { ElMessage } from 'element-plus'
interface LoginForm {
username: string
password: string
captcha: string
remember: boolean
}
interface FormErrors {
username?: string
password?: string
captcha?: string
}
const router = useRouter()
const userStore = useUserStore()
// 表单数据
const loginForm = ref<LoginForm>({
username: '',
password: '',
captcha: '',
remember: false
})
// 表单状态
const isLoading = ref(false)
const showPassword = ref(false)
const errors = ref<FormErrors>({})
const captchaText = ref('')
const captchaCanvas = ref<HTMLCanvasElement>()
// 表单验证
const isFormValid = computed(() => {
return loginForm.value.username.length >= 3 &&
loginForm.value.password.length >= 6 &&
loginForm.value.captcha.length === 4
})
// 验证用户名
const validateUsername = () => {
if (!loginForm.value.username) {
errors.value.username = '请输入用户名'
} else if (loginForm.value.username.length < 3) {
errors.value.username = '用户名至少3个字符'
} else if (!/^[a-zA-Z0-9_]+$/.test(loginForm.value.username)) {
errors.value.username = '用户名只能包含字母、数字和下划线'
} else {
delete errors.value.username
}
}
// 验证密码
const validatePassword = () => {
if (!loginForm.value.password) {
errors.value.password = '请输入密码'
} else if (loginForm.value.password.length < 6) {
errors.value.password = '密码至少6个字符'
} else {
delete errors.value.password
}
}
// 验证验证码
const validateCaptcha = () => {
if (!loginForm.value.captcha) {
errors.value.captcha = '请输入验证码'
} else if (loginForm.value.captcha.length !== 4) {
errors.value.captcha = '验证码为4位字符'
} else {
delete errors.value.captcha
}
}
// 清除错误
const clearError = (field: keyof FormErrors) => {
delete errors.value[field]
}
// 生成验证码
const generateCaptcha = () => {
const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
let result = ''
for (let i = 0; i < 4; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
captchaText.value = result
drawCaptcha()
}
// 绘制验证码
const drawCaptcha = () => {
if (!captchaCanvas.value) return
const canvas = captchaCanvas.value
const ctx = canvas.getContext('2d')!
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 设置背景
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
gradient.addColorStop(0, 'rgba(212, 175, 55, 0.1)')
gradient.addColorStop(1, 'rgba(212, 175, 55, 0.2)')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制干扰线
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = `rgba(212, 175, 55, ${Math.random() * 0.3 + 0.1})`
ctx.lineWidth = Math.random() * 2 + 1
ctx.beginPath()
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height)
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height)
ctx.stroke()
}
// 绘制验证码文字
const text = captchaText.value
for (let i = 0; i < text.length; i++) {
ctx.font = `${Math.random() * 8 + 20}px Arial`
ctx.fillStyle = `hsl(${Math.random() * 60 + 30}, 70%, ${Math.random() * 30 + 50}%)`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const x = (canvas.width / text.length) * (i + 0.5)
const y = canvas.height / 2 + (Math.random() - 0.5) * 8
ctx.save()
ctx.translate(x, y)
ctx.rotate((Math.random() - 0.5) * 0.5)
ctx.fillText(text[i], 0, 0)
ctx.restore()
}
// 绘制干扰点
for (let i = 0; i < 30; i++) {
ctx.fillStyle = `rgba(212, 175, 55, ${Math.random() * 0.5})`
ctx.beginPath()
ctx.arc(
Math.random() * canvas.width,
Math.random() * canvas.height,
Math.random() * 2 + 1,
0,
2 * Math.PI
)
ctx.fill()
}
}
// 刷新验证码
const refreshCaptcha = () => {
generateCaptcha()
loginForm.value.captcha = ''
clearError('captcha')
}
// 处理登录
const handleLogin = async () => {
// 验证表单
validateUsername()
validatePassword()
validateCaptcha()
// 验证验证码是否正确
if (loginForm.value.captcha.toLowerCase() !== captchaText.value.toLowerCase()) {
ElMessage.error('验证码错误')
refreshCaptcha()
return
}
if (!isFormValid.value) {
ElMessage.error('请检查输入信息')
return
}
try {
isLoading.value = true
const response = await authApi.login(
loginForm.value.username,
loginForm.value.password
)
// 保存用户信息
userStore.login(response.data.user, response.data.token)
ElMessage.success('登录成功')
// 跳转到管理后台首页
router.push('/admin')
} catch (error: any) {
console.error('登录失败:', error)
if (error.response?.status === 401) {
ElMessage.error('用户名或密码错误')
} else if (error.response?.status === 400) {
ElMessage.error('验证码错误')
refreshCaptcha()
} else {
ElMessage.error('登录失败,请重试')
}
} finally {
isLoading.value = false
}
}
// 忘记密码
const handleForgotPassword = () => {
ElMessage.info('请联系系统管理员重置密码')
}
onMounted(async () => {
// 如果已经登录,直接跳转
if (userStore.isLoggedIn) {
router.push('/admin')
return
}
await nextTick()
generateCaptcha()
})
</script>
<style scoped>
.admin-login-page {
min-height: 100vh;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 25%, #334155 50%, #475569 75%, #64748b 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
position: relative;
overflow: hidden;
}
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
}
.decoration-pattern {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 25% 25%, rgba(212, 175, 55, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(212, 175, 55, 0.05) 0%, transparent 50%);
background-size: 400px 400px;
animation: patternMove 20s linear infinite;
}
.floating-elements {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.floating-elements .element {
position: absolute;
width: 4px;
height: 4px;
background: rgba(212, 175, 55, 0.3);
border-radius: 50%;
animation: float 15s linear infinite;
}
.floating-elements .element:nth-child(1) { left: 10%; animation-delay: 0s; }
.floating-elements .element:nth-child(2) { left: 20%; animation-delay: -2s; }
.floating-elements .element:nth-child(3) { left: 40%; animation-delay: -4s; }
.floating-elements .element:nth-child(4) { left: 60%; animation-delay: -6s; }
.floating-elements .element:nth-child(5) { left: 80%; animation-delay: -8s; }
.floating-elements .element:nth-child(6) { left: 90%; animation-delay: -10s; }
@keyframes patternMove {
0% { transform: translate(0, 0); }
100% { transform: translate(-400px, -400px); }
}
@keyframes float {
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
}
.login-container {
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 1000px;
width: 100%;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2;
}
.brand-section {
padding: 3rem;
background: linear-gradient(135deg, rgba(212, 175, 55, 0.1), rgba(212, 175, 55, 0.05));
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
border-right: 1px solid rgba(212, 175, 55, 0.2);
}
.brand-logo {
margin-bottom: 2rem;
}
.logo-img {
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid #d4af37;
box-shadow: 0 8px 25px rgba(212, 175, 55, 0.3);
}
.brand-title {
font-size: 2.5rem;
font-weight: bold;
color: #d4af37;
margin: 0 0 0.5rem 0;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.brand-subtitle {
font-size: 1.2rem;
color: #e2e8f0;
margin: 0 0 2rem 0;
opacity: 0.9;
}
.brand-desc {
color: #94a3b8;
line-height: 1.6;
}
.brand-desc p {
margin: 0 0 0.5rem 0;
}
.login-form-section {
padding: 3rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.form-header {
margin-bottom: 2rem;
text-align: center;
}
.form-title {
font-size: 2rem;
font-weight: bold;
color: #e2e8f0;
margin: 0 0 0.5rem 0;
}
.form-subtitle {
color: #94a3b8;
margin: 0;
}
.login-form {
width: 100%;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: flex;
align-items: center;
gap: 0.5rem;
color: #e2e8f0;
font-weight: 500;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.input-wrapper {
position: relative;
}
.form-input {
width: 100%;
padding: 1rem 1rem 1rem 3rem;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(212, 175, 55, 0.2);
border-radius: 10px;
color: #e2e8f0;
font-size: 1rem;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: #d4af37;
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
}
.form-input.error {
border-color: #ef4444;
}
.form-input::placeholder {
color: #64748b;
}
.input-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
pointer-events: none;
}
.password-toggle {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.3s ease;
}
.password-toggle:hover {
color: #d4af37;
background: rgba(212, 175, 55, 0.1);
}
.captcha-wrapper {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.captcha-input {
flex: 1;
}
.captcha-image {
position: relative;
cursor: pointer;
border: 2px solid rgba(212, 175, 55, 0.2);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.captcha-image:hover {
border-color: #d4af37;
}
.captcha-image canvas {
display: block;
}
.captcha-refresh {
position: absolute;
top: 2px;
right: 2px;
background: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 4px;
padding: 2px;
font-size: 0.7rem;
opacity: 0;
transition: opacity 0.3s ease;
}
.captcha-image:hover .captcha-refresh {
opacity: 1;
}
.error-message {
color: #ef4444;
font-size: 0.8rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-input {
display: none;
}
.checkbox-custom {
width: 18px;
height: 18px;
border: 2px solid rgba(212, 175, 55, 0.3);
border-radius: 4px;
position: relative;
transition: all 0.3s ease;
}
.checkbox-input:checked + .checkbox-custom {
background: #d4af37;
border-color: #d4af37;
}
.checkbox-input:checked + .checkbox-custom::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #1a1a2e;
font-size: 0.8rem;
font-weight: bold;
}
.checkbox-label {
color: #e2e8f0;
font-size: 0.9rem;
}
.forgot-link {
color: #d4af37;
text-decoration: none;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.forgot-link:hover {
color: #f1c40f;
text-decoration: underline;
}
.login-btn {
width: 100%;
padding: 1rem;
background: linear-gradient(135deg, #d4af37, #f1c40f);
color: #1a1a2e;
border: none;
border-radius: 10px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 2rem;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(212, 175, 55, 0.4);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.login-btn.loading {
pointer-events: none;
}
.btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-content .icon-loader {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.security-notice {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
color: #93c5fd;
font-size: 0.8rem;
line-height: 1.4;
}
.notice-content p {
margin: 0 0 0.25rem 0;
}
.notice-content p:last-child {
margin-bottom: 0;
}
.login-footer {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
text-align: center;
color: #64748b;
font-size: 0.8rem;
z-index: 2;
}
.footer-links {
display: flex;
gap: 2rem;
justify-content: center;
margin-bottom: 1rem;
}
.footer-links a {
color: #94a3b8;
text-decoration: none;
transition: color 0.3s ease;
}
.footer-links a:hover {
color: #d4af37;
}
.footer-copyright p {
margin: 0 0 0.25rem 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.admin-login-page {
padding: 1rem;
}
.login-container {
grid-template-columns: 1fr;
max-width: 400px;
}
.brand-section {
padding: 2rem;
border-right: none;
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
}
.brand-title {
font-size: 2rem;
}
.login-form-section {
padding: 2rem;
}
.form-title {
font-size: 1.5rem;
}
.captcha-wrapper {
flex-direction: column;
}
.captcha-image {
align-self: center;
}
.form-options {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.login-footer {
position: static;
transform: none;
margin-top: 2rem;
}
.footer-links {
flex-direction: column;
gap: 1rem;
}
}
@media (max-width: 480px) {
.brand-section,
.login-form-section {
padding: 1.5rem;
}
.brand-title {
font-size: 1.8rem;
}
.form-title {
font-size: 1.3rem;
}
}
</style>