feat: 增强情绪博物馆项目功能 - 新增用户评论和帖子功能,优化前端架构和WebSocket通信 - 更新文档和部署配置
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="login-form">
|
||||
<!-- 标题 -->
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">欢迎回来</h2>
|
||||
<p class="text-gray-600">登录您的账户继续使用</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<!-- 用户名 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名/邮箱/手机号"
|
||||
clearable
|
||||
:prefix-icon="User"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
clearable
|
||||
:prefix-icon="Lock"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<el-form-item v-if="showCaptcha" prop="captcha">
|
||||
<div class="flex space-x-2">
|
||||
<el-input
|
||||
v-model="loginForm.captcha"
|
||||
placeholder="请输入验证码"
|
||||
clearable
|
||||
class="flex-1"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<div
|
||||
class="w-24 h-10 bg-gray-100 rounded cursor-pointer flex items-center justify-center"
|
||||
@click="refreshCaptcha"
|
||||
>
|
||||
<img
|
||||
v-if="captchaImage"
|
||||
:src="captchaImage"
|
||||
alt="验证码"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
<span v-else class="text-xs text-gray-500">点击刷新</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 记住我和忘记密码 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<el-checkbox v-model="loginForm.rememberMe">
|
||||
记住我
|
||||
</el-checkbox>
|
||||
<el-link type="primary" @click="showForgotPassword">
|
||||
忘记密码?
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="w-full"
|
||||
:loading="isLoggingIn"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ isLoggingIn ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div class="mt-6">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">或使用以下方式登录</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-3 gap-3">
|
||||
<el-button
|
||||
v-for="provider in oauthProviders"
|
||||
:key="provider.name"
|
||||
class="oauth-button"
|
||||
@click="handleOAuthLogin(provider.name)"
|
||||
>
|
||||
<el-icon :size="20">
|
||||
<component :is="provider.icon" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div class="text-center mt-6">
|
||||
<span class="text-gray-600">还没有账户?</span>
|
||||
<router-link to="/auth/register" class="text-blue-600 hover:text-blue-500 ml-1">
|
||||
立即注册
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, ChatDotRound, Share, Link } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { authApi } from '@/api/auth'
|
||||
import type { LoginRequest } from '@/types/api'
|
||||
import { validateUsername, validatePassword } from '@/utils/validation'
|
||||
|
||||
// 状态管理
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
const isLoggingIn = computed(() => authStore.isLoggingIn)
|
||||
const showCaptcha = ref(false)
|
||||
const captchaImage = ref('')
|
||||
const captchaId = ref('')
|
||||
|
||||
// 登录表单
|
||||
const loginForm = reactive<LoginRequest>({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
captchaId: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
captcha: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: 'blur',
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (showCaptcha.value && !value) {
|
||||
callback(new Error('请输入验证码'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 第三方登录提供商
|
||||
const oauthProviders = [
|
||||
{ name: 'wechat', icon: ChatDotRound, title: '微信' },
|
||||
{ name: 'qq', icon: Share, title: 'QQ' },
|
||||
{ name: 'github', icon: Link, title: 'GitHub' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
await loginFormRef.value.validate()
|
||||
|
||||
// 设置验证码ID
|
||||
if (showCaptcha.value) {
|
||||
loginForm.captchaId = captchaId.value
|
||||
}
|
||||
|
||||
await authStore.login(loginForm)
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
|
||||
// 如果是验证码错误,刷新验证码
|
||||
if (error.message?.includes('验证码')) {
|
||||
await refreshCaptcha()
|
||||
}
|
||||
|
||||
// 连续登录失败后显示验证码
|
||||
if (!showCaptcha.value && error.code === 'LOGIN_FAILED_TOO_MANY') {
|
||||
showCaptcha.value = true
|
||||
await refreshCaptcha()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshCaptcha = async () => {
|
||||
try {
|
||||
const response = await authApi.getCaptcha()
|
||||
captchaImage.value = response.captchaImage
|
||||
captchaId.value = response.captchaId
|
||||
loginForm.captcha = ''
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error)
|
||||
ElMessage.error('获取验证码失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const handleOAuthLogin = async (provider: string) => {
|
||||
try {
|
||||
ElMessage.info(`${provider} 登录功能开发中...`)
|
||||
|
||||
// 这里实现第三方登录逻辑
|
||||
// 1. 跳转到第三方授权页面
|
||||
// 2. 获取授权码
|
||||
// 3. 调用后端接口完成登录
|
||||
|
||||
} catch (error) {
|
||||
console.error('第三方登录失败:', error)
|
||||
ElMessage.error('第三方登录失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const showForgotPassword = () => {
|
||||
ElMessage.info('忘记密码功能开发中...')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 如果已登录,跳转到首页
|
||||
if (authStore.isLoggedIn) {
|
||||
router.push('/home')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要显示验证码
|
||||
const loginAttempts = localStorage.getItem('login_attempts')
|
||||
if (loginAttempts && parseInt(loginAttempts) >= 3) {
|
||||
showCaptcha.value = true
|
||||
refreshCaptcha()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听登录失败次数
|
||||
let loginFailCount = 0
|
||||
watch(() => authStore.isLoggingIn, (isLogging) => {
|
||||
if (!isLogging && !authStore.isLoggedIn) {
|
||||
loginFailCount++
|
||||
localStorage.setItem('login_attempts', loginFailCount.toString())
|
||||
|
||||
// 失败3次后显示验证码
|
||||
if (loginFailCount >= 3 && !showCaptcha.value) {
|
||||
showCaptcha.value = true
|
||||
refreshCaptcha()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oauth-button {
|
||||
@apply w-full h-12 border border-gray-300 rounded-lg hover:border-gray-400 transition-colors;
|
||||
}
|
||||
|
||||
.oauth-button:hover {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
@apply h-12;
|
||||
}
|
||||
|
||||
:deep(.el-button--large) {
|
||||
@apply h-12;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user