This commit is contained in:
2026-01-14 14:49:08 +08:00
parent 2ce76820da
commit 7065e5329a
23 changed files with 2236 additions and 413 deletions

View File

@@ -1,399 +1,147 @@
<script setup>
import { ref } from 'vue'
import { ref, reactive, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from '@/hooks/useI18n'
import { useUserStore } from '@/stores/modules/user'
import { ElMessage } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
import '@/assets/css/auth.css'
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const loginForm = ref({
username: '',
password: '',
})
const loginFormRef = ref(null)
const loading = ref(false)
const handleLogin = () => {
loading.value = true
const loginForm = reactive({
username: '',
password: '',
remember: false
})
// 模拟登录请求
setTimeout(() => {
console.log('登录信息:', loginForm.value)
loading.value = false
// 这里可以添加实际的登录逻辑
alert(t('login.loginSuccess') + '(模拟)')
}, 1000)
const loginRules = {
username: [
{ required: true, message: t('login.usernamePlaceholder'), trigger: 'blur' },
{ min: 3, max: 20, message: t('form.minLength', { min: 3, max: 20 }), trigger: 'blur' }
],
password: [
{ required: true, message: t('login.passwordPlaceholder'), trigger: 'blur' },
{ min: 6, max: 20, message: t('form.minLength', { min: 6 }), trigger: 'blur' }
]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
// 模拟登录请求
await new Promise((resolve) => setTimeout(resolve, 1000))
// 模拟登录成功
const mockUserInfo = {
id: 1,
username: loginForm.username,
nickname: '管理员',
email: 'admin@example.com',
role: ['admin'],
avatar: ''
}
const mockToken = 'mock-token-' + Date.now()
const mockRefreshToken = 'mock-refresh-token-' + Date.now()
userStore.setToken(mockToken)
userStore.setRefreshToken(mockRefreshToken)
userStore.setUserInfo(mockUserInfo)
ElMessage.success(t('login.loginSuccess'))
// 跳转到首页或重定向页面
const redirect = router.currentRoute.value.query.redirect || '/'
router.push(redirect)
} catch (error) {
ElMessage.error(error.message || t('login.loginFailed'))
} finally {
loading.value = false
}
}
})
}
const goToRegister = () => {
router.push('/register')
}
const goToResetPassword = () => {
router.push('/reset-password')
}
onBeforeUnmount(() => {
if (loginFormRef.value) {
loginFormRef.value.clearValidate()
}
})
</script>
<template>
<div class="login-container">
<div class="language-switcher-wrapper">
<div class="auth-container">
<div class="language-switcher">
<LanguageSwitcher />
</div>
<div class="login-card">
<div class="login-header">
<h2>{{ t('login.title') }}</h2>
<div class="auth-card">
<div class="auth-header">
<div class="logo">
<el-icon :size="48">
<User />
</el-icon>
</div>
<h1>{{ t('login.title') }}</h1>
<p>{{ t('login.subtitle') }}</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="form-group">
<label for="username">{{ t('common.username') }}</label>
<input id="username" v-model="loginForm.username" type="text"
:placeholder="t('login.usernamePlaceholder')" required />
</div>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="auth-form" size="large">
<el-form-item prop="username">
<el-input v-model="loginForm.username" :placeholder="t('login.usernamePlaceholder')"
:prefix-icon="User" />
</el-form-item>
<div class="form-group">
<label for="password">{{ t('common.password') }}</label>
<input id="password" v-model="loginForm.password" type="password"
:placeholder="t('login.passwordPlaceholder')" required />
</div>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" :placeholder="t('login.passwordPlaceholder')"
:prefix-icon="Lock" show-password @keyup.enter="handleLogin" />
</el-form-item>
<div class="form-options">
<label class="remember-me">
<input type="checkbox" />
<span>{{ t('common.rememberMe') }}</span>
</label>
<a href="#" class="forgot-password">{{ t('common.forgotPassword') }}</a>
</div>
<el-form-item>
<el-checkbox v-model="loginForm.remember">
{{ t('login.rememberMe') }}
</el-checkbox>
</el-form-item>
<button type="submit" class="login-btn" :disabled="loading">
<span v-if="!loading">{{ t('login.loginButton') }}</span>
<span v-else>{{ t('common.loading') }}</span>
</button>
</form>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleLogin">
{{ t('login.loginButton') }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<p>{{ t('login.noAccount') }} <a href="#">{{ t('login.registerNow') }}</a></p>
<div class="auth-actions">
<el-link type="primary" @click="goToResetPassword">
{{ t('login.forgotPassword') }}
</el-link>
</div>
<div class="auth-footer">
<span class="text">{{ t('login.noAccount') }}</span>
<router-link to="/register" class="link">
{{ t('login.registerNow') }}
</router-link>
</div>
</div>
</div>
</template>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0c1929 0%, #1a237e 50%, #0d47a1 100%);
padding: 20px;
position: relative;
overflow: hidden;
}
.language-switcher-wrapper {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
/* 网格背景 */
.login-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
animation: gridMove 20s linear infinite;
}
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
/* 科技光效 */
.login-container::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(0, 212, 255, 0.1) 0%, transparent 50%);
animation: rotate 30s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.login-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border-radius: 20px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.3),
0 0 40px rgba(0, 212, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
width: 100%;
max-width: 420px;
overflow: hidden;
position: relative;
z-index: 1;
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-header {
text-align: center;
padding: 40px 40px 20px;
position: relative;
}
.login-header::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 4px;
background: linear-gradient(90deg, transparent, #00d4ff, #7c4dff, transparent);
border-radius: 2px;
animation: lineGlow 2s ease-in-out infinite;
}
@keyframes lineGlow {
0%,
100% {
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(0, 212, 255, 0.8), 0 0 30px rgba(124, 77, 255, 0.5);
}
}
.login-header h2 {
margin: 0 0 10px;
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
letter-spacing: 1px;
}
.login-header p {
margin: 0;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
font-weight: 300;
}
.login-form {
padding: 20px 40px 40px;
}
.form-group {
margin-bottom: 20px;
position: relative;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
letter-spacing: 0.5px;
}
.form-group input {
width: 100%;
padding: 14px 20px;
border: 2px solid rgba(0, 212, 255, 0.2);
border-radius: 12px;
font-size: 14px;
transition: all 0.3s ease;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.05);
color: white;
backdrop-filter: blur(10px);
}
.form-group input:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3), inset 0 0 10px rgba(0, 212, 255, 0.1);
background: rgba(255, 255, 255, 0.1);
}
.form-group input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.form-group input:focus::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.remember-me {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.3s ease;
}
.remember-me:hover {
color: rgba(255, 255, 255, 0.9);
}
.remember-me input[type='checkbox'] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #00d4ff;
transition: all 0.3s ease;
}
.remember-me input[type='checkbox']:checked {
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.forgot-password {
font-size: 14px;
color: #00d4ff;
text-decoration: none;
transition: all 0.3s ease;
font-weight: 500;
}
.forgot-password:hover {
color: #7c4dff;
text-shadow: 0 0 10px rgba(124, 77, 255, 0.5);
}
.login-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
letter-spacing: 1px;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
.login-btn:hover:not(:disabled)::before {
left: 100%;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow:
0 10px 30px rgba(0, 212, 255, 0.4),
0 0 20px rgba(124, 77, 255, 0.3);
}
.login-btn:active:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 5px 15px rgba(0, 212, 255, 0.4);
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.login-footer {
text-align: center;
padding: 20px 40px;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.login-footer p {
margin: 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
}
.login-footer a {
color: #00d4ff;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.login-footer a:hover {
color: #7c4dff;
text-shadow: 0 0 10px rgba(124, 77, 255, 0.5);
}
@media (max-width: 480px) {
.login-card {
max-width: 100%;
}
.login-header,
.login-form,
.login-footer {
padding-left: 20px;
padding-right: 20px;
}
.login-header h2 {
font-size: 24px;
}
}
</style>

View File

@@ -0,0 +1,196 @@
<script setup>
import { ref, reactive, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from '@/hooks/useI18n'
import { ElMessage } from 'element-plus'
import { Lock, Message } from '@element-plus/icons-vue'
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
import '@/assets/css/auth.css'
const { t } = useI18n()
const router = useRouter()
const resetFormRef = ref(null)
const loading = ref(false)
const countdown = ref(0)
const timer = ref(null)
const resetForm = reactive({
email: '',
code: '',
newPassword: '',
confirmPassword: ''
})
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error(t('form.passwordMismatch')))
} else if (value !== resetForm.newPassword) {
callback(new Error(t('form.passwordMismatch')))
} else {
callback()
}
}
const resetRules = {
email: [
{ required: true, message: t('resetPassword.emailPlaceholder'), trigger: 'blur' },
{ type: 'email', message: t('resetPassword.emailRule'), trigger: 'blur' }
],
code: [
{ required: true, message: t('resetPassword.codePlaceholder'), trigger: 'blur' },
{ len: 6, message: t('resetPassword.codeRule'), trigger: 'blur' }
],
newPassword: [
{ required: true, message: t('resetPassword.newPasswordPlaceholder'), trigger: 'blur' },
{ min: 6, max: 20, message: t('resetPassword.passwordRule'), trigger: 'blur' }
],
confirmPassword: [
{ required: true, validator: validatePass2, trigger: 'blur' }
]
}
const sendCode = () => {
if (!resetForm.email) {
ElMessage.warning(t('resetPassword.sendCodeFirst'))
return
}
if (!/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(resetForm.email)) {
ElMessage.warning(t('resetPassword.emailRule'))
return
}
// 模拟发送验证码
countdown.value = 60
timer.value = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer.value)
timer.value = null
}
}, 1000)
ElMessage.success(t('resetPassword.codeSent'))
}
const handleReset = async () => {
if (!resetFormRef.value) return
await resetFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
// 模拟重置密码请求
await new Promise((resolve) => setTimeout(resolve, 1500))
ElMessage.success(t('resetPassword.resetSuccess'))
// 清除定时器
if (timer.value) {
clearInterval(timer.value)
timer.value = null
countdown.value = 0
}
// 跳转到登录页
router.push('/login')
} catch (error) {
ElMessage.error(error.message || t('resetPassword.resetFailed'))
} finally {
loading.value = false
}
}
})
}
const goToLogin = () => {
// 清除定时器
if (timer.value) {
clearInterval(timer.value)
timer.value = null
countdown.value = 0
}
router.push('/login')
}
onBeforeUnmount(() => {
if (resetFormRef.value) {
resetFormRef.value.clearValidate()
}
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
</script>
<template>
<div class="auth-container">
<div class="language-switcher">
<LanguageSwitcher />
</div>
<div class="auth-card">
<div class="auth-header">
<div class="logo">
<el-icon :size="48">
<Lock />
</el-icon>
</div>
<h1>{{ t('resetPassword.title') }}</h1>
<p>{{ t('resetPassword.subtitle') }}</p>
</div>
<el-form ref="resetFormRef" :model="resetForm" :rules="resetRules" class="auth-form" size="large">
<el-form-item prop="email">
<el-input v-model="resetForm.email" :placeholder="t('resetPassword.emailPlaceholder')"
:prefix-icon="Message" />
</el-form-item>
<el-form-item prop="code">
<el-row :gutter="10">
<el-col :span="14">
<el-input v-model="resetForm.code" :placeholder="t('resetPassword.codePlaceholder')"
:prefix-icon="Lock" maxlength="6" />
</el-col>
<el-col :span="10">
<el-button class="code-button" :disabled="countdown > 0" @click="sendCode">
{{
countdown > 0
? t('resetPassword.resendCode', { seconds: countdown })
: t('resetPassword.sendCode')
}}
</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="newPassword">
<el-input v-model="resetForm.newPassword" type="password"
:placeholder="t('resetPassword.newPasswordPlaceholder')" :prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="resetForm.confirmPassword" type="password"
:placeholder="t('resetPassword.confirmPasswordPlaceholder')" :prefix-icon="Lock" show-password
@keyup.enter="handleReset" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleReset">
{{ t('resetPassword.resetButton') }}
</el-button>
</el-form-item>
</el-form>
<div class="auth-footer">
<span class="text">{{ t('common.back') }}</span>
<el-link type="primary" @click="goToLogin">
{{ t('resetPassword.backToLogin') }}
</el-link>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,162 @@
<script setup>
import { ref, reactive, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from '@/hooks/useI18n'
import { useUserStore } from '@/stores/modules/user'
import { ElMessage } from 'element-plus'
import { User, Lock, Message } from '@element-plus/icons-vue'
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
import '@/assets/css/auth.css'
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const registerFormRef = ref(null)
const loading = ref(false)
const registerForm = reactive({
username: '',
email: '',
password: '',
confirmPassword: '',
agree: false
})
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error(t('form.passwordMismatch')))
} else if (value !== registerForm.password) {
callback(new Error(t('form.passwordMismatch')))
} else {
callback()
}
}
const registerRules = {
username: [
{ required: true, message: t('register.usernamePlaceholder'), trigger: 'blur' },
{ min: 3, max: 20, message: t('register.usernameRule'), trigger: 'blur' }
],
email: [
{ required: true, message: t('register.emailPlaceholder'), trigger: 'blur' },
{ type: 'email', message: t('register.emailRule'), trigger: 'blur' }
],
password: [
{ required: true, message: t('register.passwordPlaceholder'), trigger: 'blur' },
{ min: 6, max: 20, message: t('register.passwordRule'), trigger: 'blur' }
],
confirmPassword: [
{ required: true, validator: validatePass2, trigger: 'blur' }
],
agree: [
{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error(t('register.agreeRule')))
} else {
callback()
}
},
trigger: 'change'
}
]
}
const handleRegister = async () => {
if (!registerFormRef.value) return
await registerFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
// 模拟注册请求
await new Promise((resolve) => setTimeout(resolve, 1500))
ElMessage.success(t('register.registerSuccess'))
// 跳转到登录页
router.push('/login')
} catch (error) {
ElMessage.error(error.message || t('register.registerFailed'))
} finally {
loading.value = false
}
}
})
}
const goToLogin = () => {
router.push('/login')
}
onBeforeUnmount(() => {
if (registerFormRef.value) {
registerFormRef.value.clearValidate()
}
})
</script>
<template>
<div class="auth-container">
<div class="language-switcher">
<LanguageSwitcher />
</div>
<div class="auth-card">
<div class="auth-header">
<div class="logo">
<el-icon :size="48">
<User />
</el-icon>
</div>
<h1>{{ t('register.title') }}</h1>
<p>{{ t('register.subtitle') }}</p>
</div>
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" size="large">
<el-form-item prop="username">
<el-input v-model="registerForm.username" :placeholder="t('register.usernamePlaceholder')"
:prefix-icon="User" />
</el-form-item>
<el-form-item prop="email">
<el-input v-model="registerForm.email" :placeholder="t('register.emailPlaceholder')"
:prefix-icon="Message" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="registerForm.password" type="password"
:placeholder="t('register.passwordPlaceholder')" :prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password"
:placeholder="t('register.confirmPasswordPlaceholder')" :prefix-icon="Lock" show-password
@keyup.enter="handleRegister" />
</el-form-item>
<el-form-item prop="agree">
<el-checkbox v-model="registerForm.agree">
<span v-html="t('register.agreeTerms')"></span>
<el-link type="primary">{{ t('register.terms') }}</el-link>
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleRegister">
{{ t('register.registerButton') }}
</el-button>
</el-form-item>
</el-form>
<div class="auth-footer">
<span class="text">{{ t('register.hasAccount') }}</span>
<router-link to="/login" class="link">
{{ t('register.loginNow') }}
</router-link>
</div>
</div>
</div>
</template>