15 changed files with 2998 additions and 207 deletions
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
# GoFaster 登录功能说明 |
||||
|
||||
## 功能概述 |
||||
|
||||
GoFaster 前端应用已成功集成了完整的用户登录系统,包括: |
||||
|
||||
- 🔐 登录弹窗组件 |
||||
- 📱 响应式设计 |
||||
- 🔒 验证码支持 |
||||
- 💾 本地状态管理 |
||||
- 🎨 主题适配 |
||||
|
||||
## 使用方法 |
||||
|
||||
### 1. 显示登录弹窗 |
||||
|
||||
登录弹窗可以通过以下两种方式触发: |
||||
|
||||
#### 方式一:点击顶部导航栏的"登录"按钮 |
||||
- 在未登录状态下,顶部导航栏会显示"🔐 登录"按钮 |
||||
- 点击即可打开登录弹窗 |
||||
|
||||
#### 方式二:点击首页的登录按钮 |
||||
- 在首页的欢迎区域,未登录状态下会显示登录提示 |
||||
- 点击"立即登录"按钮即可打开登录弹窗 |
||||
|
||||
### 2. 登录流程 |
||||
|
||||
1. **获取验证码**:弹窗打开时自动获取验证码图片 |
||||
2. **填写信息**: |
||||
- 用户名:输入您的用户名 |
||||
- 密码:输入您的密码 |
||||
- 验证码:输入图片中显示的4位验证码 |
||||
3. **提交登录**:点击"登录"按钮提交 |
||||
4. **验证码刷新**:点击验证码图片可刷新获取新的验证码 |
||||
|
||||
### 3. 默认账号 |
||||
|
||||
系统提供了默认的管理员账号用于测试: |
||||
|
||||
- **用户名**:`sysadmin` |
||||
- **密码**:`sysadmin@123` |
||||
|
||||
## 技术实现 |
||||
|
||||
### 组件结构 |
||||
|
||||
``` |
||||
MainLayout.vue (主布局) |
||||
├── LoginModal.vue (登录弹窗) |
||||
├── Toast.vue (消息提示) |
||||
└── 其他组件... |
||||
``` |
||||
|
||||
### 状态管理 |
||||
|
||||
- 使用 Vue 3 Composition API |
||||
- 通过 `provide/inject` 在组件间共享登录状态 |
||||
- 本地存储用户信息和登录状态 |
||||
|
||||
### 后端接口 |
||||
|
||||
登录功能需要后端提供以下接口: |
||||
|
||||
1. **获取验证码**:`GET /api/auth/captcha` |
||||
2. **用户登录**:`POST /api/auth/login` |
||||
|
||||
### 配置说明 |
||||
|
||||
前端配置位于 `src/config/app.config.js`,默认连接 `http://localhost:8080/api` |
||||
|
||||
## 样式特性 |
||||
|
||||
### 主题适配 |
||||
- 支持深色/浅色主题切换 |
||||
- 使用 CSS 变量实现主题一致性 |
||||
- 响应式设计,支持移动端 |
||||
|
||||
### 动画效果 |
||||
- 弹窗滑入动画 |
||||
- 加载状态指示器 |
||||
- 平滑的过渡效果 |
||||
|
||||
## 错误处理 |
||||
|
||||
- 网络连接失败提示 |
||||
- 验证码错误处理 |
||||
- 登录失败重试机制 |
||||
- 表单验证提示 |
||||
|
||||
## 安全特性 |
||||
|
||||
- 密码输入框隐藏内容 |
||||
- 验证码防机器人 |
||||
- Token 认证机制 |
||||
- 自动登出处理 |
||||
|
||||
## 开发说明 |
||||
|
||||
### 添加新的登录相关功能 |
||||
|
||||
1. 在 `LoginModal.vue` 中添加新的表单字段 |
||||
2. 在 `userService.js` 中添加对应的 API 调用 |
||||
3. 在 `MainLayout.vue` 中处理新的状态变化 |
||||
|
||||
### 自定义样式 |
||||
|
||||
登录弹窗的样式可以通过修改 `LoginModal.vue` 中的 CSS 变量来调整: |
||||
|
||||
```css |
||||
:root { |
||||
--primary-color: #1976d2; |
||||
--primary-hover: #1565c0; |
||||
--bg-primary: #ffffff; |
||||
--text-primary: #2c3e50; |
||||
} |
||||
``` |
||||
|
||||
## 故障排除 |
||||
|
||||
### 常见问题 |
||||
|
||||
1. **登录弹窗不显示** |
||||
- 检查浏览器控制台是否有错误 |
||||
- 确认组件是否正确导入 |
||||
|
||||
2. **验证码获取失败** |
||||
- 检查后端服务是否正常运行 |
||||
- 确认网络连接正常 |
||||
|
||||
3. **登录失败** |
||||
- 检查用户名和密码是否正确 |
||||
- 确认验证码输入正确 |
||||
- 查看后端日志获取详细错误信息 |
||||
|
||||
### 调试模式 |
||||
|
||||
在开发环境中,可以打开浏览器开发者工具查看: |
||||
- 网络请求状态 |
||||
- 控制台日志 |
||||
- Vue 组件状态 |
||||
|
||||
## 更新日志 |
||||
|
||||
- **v1.0.0**:初始版本,包含基本的登录功能 |
||||
- 支持用户名/密码登录 |
||||
- 集成验证码系统 |
||||
- 响应式设计 |
||||
- 主题适配 |
@ -0,0 +1,222 @@
@@ -0,0 +1,222 @@
|
||||
# GoFaster 登录功能改进说明 |
||||
|
||||
## 🎯 问题修复 |
||||
|
||||
### 1. 验证码获取失败问题 ✅ |
||||
|
||||
**问题描述**: |
||||
- 验证码接口调用失败时,用户没有清晰的反馈 |
||||
- 错误处理不够完善 |
||||
|
||||
**解决方案**: |
||||
- 添加了 `captchaLoading` 状态管理 |
||||
- 改进了错误处理和用户提示 |
||||
- 添加了验证码加载动画 |
||||
- 在控制台输出详细的调试信息 |
||||
|
||||
**具体改进**: |
||||
```javascript |
||||
// 添加验证码加载状态 |
||||
captchaLoading: false |
||||
|
||||
// 改进错误处理 |
||||
async refreshCaptcha() { |
||||
try { |
||||
this.captchaLoading = true |
||||
this.errorMessage = '' |
||||
const response = await userService.getCaptcha() |
||||
// ... 处理成功响应 |
||||
} catch (error) { |
||||
// ... 处理错误 |
||||
} finally { |
||||
this.captchaLoading = false |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 2. 登录按钮显示逻辑问题 ✅ |
||||
|
||||
**问题描述**: |
||||
- 用户不清楚为什么登录按钮被禁用 |
||||
- 缺少表单验证的实时反馈 |
||||
|
||||
**解决方案**: |
||||
- 修改了 `isFormValid` 计算属性,确保验证码图片加载后才启用按钮 |
||||
- 添加了实时表单验证提示 |
||||
- 提供了清晰的用户指导 |
||||
|
||||
**具体改进**: |
||||
```javascript |
||||
// 改进的表单验证逻辑 |
||||
isFormValid() { |
||||
return this.loginForm.username.trim() && |
||||
this.loginForm.password.trim() && |
||||
this.loginForm.captcha.trim() && |
||||
this.captchaImage // 确保验证码图片已加载 |
||||
} |
||||
|
||||
// 添加表单验证提示 |
||||
<div v-if="!isFormValid && (loginForm.username || loginForm.password || loginForm.captcha)" class="form-validation-hint"> |
||||
<span v-if="!loginForm.username.trim()">请填写用户名</span> |
||||
<span v-else-if="!loginForm.password.trim()">请填写密码</span> |
||||
<span v-else-if="!loginForm.captcha.trim()">请填写验证码</span> |
||||
<span v-else-if="!captchaImage">请先获取验证码</span> |
||||
</div> |
||||
``` |
||||
|
||||
## 🎨 用户体验改进 |
||||
|
||||
### 1. 验证码状态指示 |
||||
- **加载中**:显示"验证码加载中..."和旋转动画 |
||||
- **获取失败**:显示"点击获取验证码"和错误提示 |
||||
- **获取成功**:显示验证码图片 |
||||
|
||||
### 2. 表单验证反馈 |
||||
- 实时显示缺少的字段信息 |
||||
- 清晰的错误提示信息 |
||||
- 智能的表单状态管理 |
||||
|
||||
### 3. 交互优化 |
||||
- 验证码占位符支持悬停效果 |
||||
- 加载状态动画 |
||||
- 更好的视觉反馈 |
||||
|
||||
## 🔧 技术实现细节 |
||||
|
||||
### 状态管理 |
||||
```javascript |
||||
data() { |
||||
return { |
||||
loading: false, // 登录加载状态 |
||||
captchaLoading: false, // 验证码加载状态 |
||||
errorMessage: '', // 错误消息 |
||||
captchaImage: '', // 验证码图片 |
||||
captchaId: '', // 验证码ID |
||||
loginForm: { // 登录表单数据 |
||||
username: '', |
||||
password: '', |
||||
captcha: '' |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 计算属性 |
||||
```javascript |
||||
computed: { |
||||
isFormValid() { |
||||
// 所有字段都必须填写,且验证码图片必须已加载 |
||||
return this.loginForm.username.trim() && |
||||
this.loginForm.password.trim() && |
||||
this.loginForm.captcha.trim() && |
||||
this.captchaImage |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 样式改进 |
||||
```css |
||||
/* 表单验证提示样式 */ |
||||
.form-validation-hint { |
||||
margin-top: 8px; |
||||
padding: 8px 12px; |
||||
background: var(--bg-secondary); |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 6px; |
||||
font-size: 12px; |
||||
color: var(--text-secondary); |
||||
text-align: center; |
||||
} |
||||
|
||||
/* 验证码加载动画 */ |
||||
.captcha-loading-spinner { |
||||
width: 12px; |
||||
height: 12px; |
||||
border: 2px solid transparent; |
||||
border-top: 2px solid currentColor; |
||||
border-radius: 50%; |
||||
animation: spin 1s linear infinite; |
||||
margin-top: 4px; |
||||
} |
||||
``` |
||||
|
||||
## 📱 响应式设计 |
||||
|
||||
### 移动端适配 |
||||
- 验证码容器在小屏幕下垂直排列 |
||||
- 表单提示信息自适应屏幕宽度 |
||||
- 触摸友好的交互设计 |
||||
|
||||
### 主题适配 |
||||
- 支持深色/浅色主题切换 |
||||
- 使用CSS变量确保一致性 |
||||
- 悬停效果和过渡动画 |
||||
|
||||
## 🧪 测试建议 |
||||
|
||||
### 功能测试 |
||||
1. **验证码获取**: |
||||
- 点击验证码区域 |
||||
- 观察加载状态 |
||||
- 检查错误处理 |
||||
|
||||
2. **表单验证**: |
||||
- 逐个填写字段 |
||||
- 观察按钮状态变化 |
||||
- 验证提示信息 |
||||
|
||||
3. **登录流程**: |
||||
- 完整填写表单 |
||||
- 测试登录按钮启用 |
||||
- 验证成功/失败处理 |
||||
|
||||
### 边界情况测试 |
||||
- 网络连接失败 |
||||
- 验证码接口错误 |
||||
- 表单数据不完整 |
||||
- 快速点击操作 |
||||
|
||||
## 🚀 后续优化建议 |
||||
|
||||
### 1. 性能优化 |
||||
- 验证码图片缓存 |
||||
- 防抖处理 |
||||
- 懒加载优化 |
||||
|
||||
### 2. 功能扩展 |
||||
- 记住用户名 |
||||
- 自动登录 |
||||
- 多因素认证 |
||||
|
||||
### 3. 用户体验 |
||||
- 键盘快捷键支持 |
||||
- 语音输入 |
||||
- 无障碍访问 |
||||
|
||||
## 📝 更新日志 |
||||
|
||||
### v1.1.0 (2025-08-23) |
||||
- ✅ 修复验证码获取失败问题 |
||||
- ✅ 改进登录按钮显示逻辑 |
||||
- ✅ 添加表单验证实时反馈 |
||||
- ✅ 优化验证码状态指示 |
||||
- ✅ 改进错误处理和用户提示 |
||||
- ✅ 添加加载状态动画 |
||||
- ✅ 优化响应式设计 |
||||
|
||||
### v1.0.0 (2025-08-23) |
||||
- 🎉 初始版本发布 |
||||
- 🔐 基础登录功能 |
||||
- 🎨 美观的UI设计 |
||||
- 📱 响应式布局 |
||||
|
||||
## 🎊 总结 |
||||
|
||||
通过这次改进,GoFaster 的登录功能现在具有: |
||||
|
||||
1. **更好的用户体验**:清晰的验证码状态指示和表单验证反馈 |
||||
2. **更稳定的功能**:改进的错误处理和状态管理 |
||||
3. **更美观的界面**:加载动画和悬停效果 |
||||
4. **更智能的逻辑**:验证码加载完成后才启用登录按钮 |
||||
|
||||
这些改进让用户能够更清楚地了解登录流程的每个步骤,提高了整体的可用性和满意度!🎉 |
@ -0,0 +1,214 @@
@@ -0,0 +1,214 @@
|
||||
# GoFaster 登录功能测试指南 |
||||
|
||||
## 🚀 快速开始 |
||||
|
||||
### 1. 启动应用 |
||||
```bash |
||||
# 在 app 目录下运行 |
||||
npm run dev |
||||
``` |
||||
|
||||
### 2. 测试登录弹窗显示 |
||||
|
||||
#### 方式一:顶部导航栏登录按钮 |
||||
- 应用启动后,顶部导航栏右侧会显示"🔐 登录"按钮 |
||||
- 点击按钮,登录弹窗应该优雅地滑入显示 |
||||
|
||||
#### 方式二:首页登录按钮 |
||||
- 在首页欢迎区域,未登录状态下会显示"立即登录"按钮 |
||||
- 点击按钮,同样会显示登录弹窗 |
||||
|
||||
### 3. 测试登录弹窗功能 |
||||
|
||||
#### 验证码获取 |
||||
- 弹窗打开时应该自动获取验证码图片 |
||||
- 如果验证码图片显示失败,会显示"点击获取验证码"的占位符 |
||||
- 点击验证码区域可以刷新获取新的验证码 |
||||
|
||||
#### 表单验证 |
||||
- 尝试不填写任何信息直接点击登录按钮 |
||||
- 应该显示"请填写完整的登录信息"错误提示 |
||||
- 填写部分信息后,登录按钮应该保持禁用状态 |
||||
|
||||
#### 登录流程测试 |
||||
1. **输入测试账号**: |
||||
- 用户名:`sysadmin` |
||||
- 密码:`sysadmin@123` |
||||
- 验证码:输入图片中显示的4位数字 |
||||
|
||||
2. **点击登录按钮**: |
||||
- 按钮应该显示"登录中..."状态 |
||||
- 显示加载动画(旋转的圆圈) |
||||
|
||||
3. **登录结果**: |
||||
- 如果后端服务正常运行,应该显示登录成功提示 |
||||
- 如果后端服务未运行,会显示网络连接失败错误 |
||||
|
||||
## 🔧 后端服务要求 |
||||
|
||||
### 必需的后端接口 |
||||
|
||||
1. **验证码接口** |
||||
``` |
||||
GET /api/auth/captcha |
||||
响应格式: |
||||
{ |
||||
"captchaImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", |
||||
"captchaID": "abc123" |
||||
} |
||||
``` |
||||
|
||||
2. **登录接口** |
||||
``` |
||||
POST /api/auth/login |
||||
请求格式: |
||||
{ |
||||
"username": "sysadmin", |
||||
"password": "sysadmin@123", |
||||
"captcha": "1234", |
||||
"captchaId": "abc123" |
||||
} |
||||
|
||||
响应格式: |
||||
{ |
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
||||
"user": { |
||||
"username": "sysadmin", |
||||
"email": "admin@gofaster.com", |
||||
"avatar": null |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 启动后端服务 |
||||
```bash |
||||
# 在 backend 目录下运行 |
||||
.\gofaster.exe |
||||
``` |
||||
|
||||
## 🎯 测试检查点 |
||||
|
||||
### ✅ 功能检查 |
||||
- [ ] 登录弹窗能正常显示 |
||||
- [ ] 验证码能正常获取和显示 |
||||
- [ ] 表单验证正常工作 |
||||
- [ ] 登录按钮状态正确 |
||||
- [ ] 错误提示信息正确显示 |
||||
- [ ] 登录成功后弹窗关闭 |
||||
- [ ] 用户信息正确更新 |
||||
- [ ] Toast 消息提示正常显示 |
||||
|
||||
### ✅ 样式检查 |
||||
- [ ] 弹窗动画流畅 |
||||
- [ ] 响应式设计正常 |
||||
- [ ] 主题适配正确 |
||||
- [ ] 加载状态指示器正常 |
||||
- [ ] 错误提示样式正确 |
||||
|
||||
### ✅ 交互检查 |
||||
- [ ] 点击遮罩层可以关闭弹窗 |
||||
- [ ] 点击关闭按钮可以关闭弹窗 |
||||
- [ ] 验证码点击可以刷新 |
||||
- [ ] 表单输入响应正常 |
||||
- [ ] 键盘操作支持(Enter 提交) |
||||
|
||||
## 🐛 常见问题排查 |
||||
|
||||
### 1. 登录弹窗不显示 |
||||
**可能原因**: |
||||
- 组件导入错误 |
||||
- 方法名冲突 |
||||
- 事件绑定失败 |
||||
|
||||
**排查步骤**: |
||||
- 检查浏览器控制台是否有错误 |
||||
- 确认 `showLoginModal` 方法是否正确注入 |
||||
- 验证点击事件是否正确绑定 |
||||
|
||||
### 2. 验证码获取失败 |
||||
**可能原因**: |
||||
- 后端服务未启动 |
||||
- 网络连接问题 |
||||
- API 接口路径错误 |
||||
|
||||
**排查步骤**: |
||||
- 确认后端服务是否正常运行 |
||||
- 检查网络请求是否成功 |
||||
- 验证 API 基础 URL 配置 |
||||
|
||||
### 3. 登录失败 |
||||
**可能原因**: |
||||
- 用户名或密码错误 |
||||
- 验证码输入错误 |
||||
- 后端认证逻辑问题 |
||||
|
||||
**排查步骤**: |
||||
- 确认测试账号信息正确 |
||||
- 检查验证码是否输入正确 |
||||
- 查看后端日志获取详细错误信息 |
||||
|
||||
## 📱 响应式测试 |
||||
|
||||
### 桌面端测试 |
||||
- 窗口大小调整时弹窗布局正常 |
||||
- 不同分辨率下显示效果一致 |
||||
|
||||
### 移动端测试 |
||||
- 在小屏幕设备上弹窗适配正常 |
||||
- 触摸操作响应正常 |
||||
- 虚拟键盘弹出时布局正常 |
||||
|
||||
## 🎨 主题测试 |
||||
|
||||
### 深色主题 |
||||
- 弹窗在深色主题下显示正常 |
||||
- 颜色对比度合适 |
||||
|
||||
### 浅色主题 |
||||
- 弹窗在浅色主题下显示正常 |
||||
- 文字清晰可读 |
||||
|
||||
## 📝 测试报告模板 |
||||
|
||||
``` |
||||
测试日期:_________ |
||||
测试环境:_________ |
||||
测试人员:_________ |
||||
|
||||
## 功能测试结果 |
||||
- [ ] 登录弹窗显示:□通过 □失败 |
||||
- [ ] 验证码获取:□通过 □失败 |
||||
- [ ] 表单验证:□通过 □失败 |
||||
- [ ] 登录流程:□通过 □失败 |
||||
- [ ] 错误处理:□通过 □失败 |
||||
|
||||
## 样式测试结果 |
||||
- [ ] 动画效果:□通过 □失败 |
||||
- [ ] 响应式设计:□通过 □失败 |
||||
- [ ] 主题适配:□通过 □失败 |
||||
|
||||
## 发现的问题 |
||||
1. ________________ |
||||
2. ________________ |
||||
|
||||
## 建议改进 |
||||
1. ________________ |
||||
2. ________________ |
||||
|
||||
## 总体评价 |
||||
□优秀 □良好 □一般 □需要改进 |
||||
``` |
||||
|
||||
## 🎉 成功标准 |
||||
|
||||
当您看到以下现象时,说明登录功能已经成功实现: |
||||
|
||||
1. ✅ 点击登录按钮,弹窗优雅滑入 |
||||
2. ✅ 验证码图片正常显示 |
||||
3. ✅ 表单验证提示正确 |
||||
4. ✅ 登录成功后显示欢迎消息 |
||||
5. ✅ 顶部导航栏显示用户头像 |
||||
6. ✅ 首页显示个性化欢迎信息 |
||||
7. ✅ 可以使用所有功能模块 |
||||
|
||||
恭喜!您的 GoFaster 应用已经成功集成了完整的用户登录系统!🎊 |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,554 @@
@@ -0,0 +1,554 @@
|
||||
<template> |
||||
<div v-if="visible" class="login-modal-overlay" @click="handleOverlayClick"> |
||||
<div class="login-modal" @click.stop> |
||||
<div class="login-modal-header"> |
||||
<h2>用户登录</h2> |
||||
<button class="close-btn" @click="closeModal">×</button> |
||||
</div> |
||||
|
||||
<div class="login-modal-body"> |
||||
<form @submit.prevent="handleLogin" class="login-form"> |
||||
<!-- 用户名 --> |
||||
<div class="form-group"> |
||||
<label for="username">用户名</label> |
||||
<input |
||||
id="username" |
||||
v-model="loginForm.username" |
||||
type="text" |
||||
placeholder="请输入用户名" |
||||
required |
||||
:disabled="loading" |
||||
/> |
||||
</div> |
||||
|
||||
<!-- 密码 --> |
||||
<div class="form-group"> |
||||
<label for="password">密码</label> |
||||
<input |
||||
id="password" |
||||
v-model="loginForm.password" |
||||
type="password" |
||||
placeholder="请输入密码" |
||||
required |
||||
:disabled="loading" |
||||
/> |
||||
</div> |
||||
|
||||
<!-- 验证码 --> |
||||
<div class="form-group"> |
||||
<label for="captcha">验证码</label> |
||||
<div class="captcha-container"> |
||||
<input |
||||
id="captcha" |
||||
v-model="loginForm.captcha" |
||||
type="text" |
||||
placeholder="请输入验证码" |
||||
required |
||||
:disabled="loading" |
||||
maxlength="4" |
||||
/> |
||||
<div class="captcha-image-container"> |
||||
<img |
||||
v-if="captchaImage" |
||||
:src="captchaImage" |
||||
alt="验证码" |
||||
@click="refreshCaptcha" |
||||
class="captcha-image" |
||||
/> |
||||
<div v-else class="captcha-placeholder" @click="refreshCaptcha"> |
||||
<span v-if="captchaLoading">验证码加载中...</span> |
||||
<span v-else>点击获取验证码</span> |
||||
<span v-if="captchaLoading" class="captcha-loading-spinner"></span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 登录按钮 --> |
||||
<div class="form-actions"> |
||||
<button |
||||
type="submit" |
||||
class="login-btn" |
||||
:disabled="loading || !isFormValid" |
||||
> |
||||
<span v-if="loading" class="loading-spinner"></span> |
||||
{{ loading ? '登录中...' : '登录' }} |
||||
</button> |
||||
|
||||
<!-- 表单验证提示 --> |
||||
<div v-if="!isFormValid && (loginForm.username || loginForm.password || loginForm.captcha)" class="form-validation-hint"> |
||||
<span v-if="!loginForm.username.trim()">请填写用户名</span> |
||||
<span v-else-if="!loginForm.password.trim()">请填写密码</span> |
||||
<span v-else-if="!loginForm.captcha.trim()">请填写验证码</span> |
||||
<span v-else-if="!captchaImage">请先获取验证码</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 错误提示 --> |
||||
<div v-if="errorMessage" class="error-message"> |
||||
{{ errorMessage }} |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
||||
<div class="login-modal-footer"> |
||||
<p class="login-tips"> |
||||
<span>提示:</span> |
||||
<span>默认管理员账号:sysadmin</span> |
||||
<span>默认密码:sysadmin@123</span> |
||||
</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { userService } from '../services/userService' |
||||
|
||||
export default { |
||||
name: 'LoginModal', |
||||
props: { |
||||
visible: { |
||||
type: Boolean, |
||||
default: false |
||||
} |
||||
}, |
||||
data() { |
||||
return { |
||||
loading: false, |
||||
captchaLoading: false, |
||||
errorMessage: '', |
||||
captchaImage: '', |
||||
captchaId: '', |
||||
loginForm: { |
||||
username: '', |
||||
password: '', |
||||
captcha: '' |
||||
} |
||||
} |
||||
}, |
||||
computed: { |
||||
isFormValid() { |
||||
return this.loginForm.username.trim() && |
||||
this.loginForm.password.trim() && |
||||
this.loginForm.captcha.trim() && |
||||
this.captchaImage // 确保验证码图片已加载 |
||||
} |
||||
}, |
||||
watch: { |
||||
visible(newVal) { |
||||
if (newVal) { |
||||
this.resetForm() |
||||
this.refreshCaptcha() |
||||
} |
||||
} |
||||
}, |
||||
methods: { |
||||
async handleLogin() { |
||||
if (!this.isFormValid) { |
||||
this.errorMessage = '请填写完整的登录信息' |
||||
return |
||||
} |
||||
|
||||
this.loading = true |
||||
this.errorMessage = '' |
||||
|
||||
try { |
||||
// 调用登录接口 |
||||
const response = await userService.login({ |
||||
username: this.loginForm.username, |
||||
password: this.loginForm.password, |
||||
captcha: this.loginForm.captcha, |
||||
captchaId: this.captchaId |
||||
}) |
||||
|
||||
// 登录成功 |
||||
this.$emit('login-success', response) |
||||
this.closeModal() |
||||
|
||||
// 显示成功提示 |
||||
this.$emit('show-message', { |
||||
type: 'success', |
||||
title: '登录成功', |
||||
content: `欢迎回来,${response.user?.username || '用户'}!` |
||||
}) |
||||
|
||||
} catch (error) { |
||||
this.errorMessage = error.message || '登录失败,请重试' |
||||
// 登录失败时刷新验证码 |
||||
this.refreshCaptcha() |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
|
||||
async refreshCaptcha() { |
||||
try { |
||||
this.captchaLoading = true |
||||
this.errorMessage = '' |
||||
// 调用获取验证码接口 |
||||
const response = await userService.getCaptcha() |
||||
this.captchaImage = response.captchaImage |
||||
this.captchaId = response.captchaID |
||||
this.loginForm.captcha = '' |
||||
console.log('验证码获取成功:', response) |
||||
} catch (error) { |
||||
console.error('获取验证码失败:', error) |
||||
this.errorMessage = '获取验证码失败,请重试' |
||||
this.captchaImage = '' |
||||
this.captchaId = '' |
||||
} finally { |
||||
this.captchaLoading = false |
||||
} |
||||
}, |
||||
|
||||
resetForm() { |
||||
this.loginForm = { |
||||
username: '', |
||||
password: '', |
||||
captcha: '' |
||||
} |
||||
this.errorMessage = '' |
||||
this.captchaImage = '' |
||||
this.captchaId = '' |
||||
}, |
||||
|
||||
closeModal() { |
||||
this.$emit('update:visible', false) |
||||
this.resetForm() |
||||
}, |
||||
|
||||
handleOverlayClick() { |
||||
this.closeModal() |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.login-modal-overlay { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background: rgba(0, 0, 0, 0.5); |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
z-index: 1000; |
||||
backdrop-filter: blur(4px); |
||||
} |
||||
|
||||
.login-modal { |
||||
background: var(--bg-primary); |
||||
border-radius: 12px; |
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); |
||||
width: 90%; |
||||
max-width: 400px; |
||||
max-height: 90vh; |
||||
overflow: hidden; |
||||
animation: modalSlideIn 0.3s ease-out; |
||||
} |
||||
|
||||
@keyframes modalSlideIn { |
||||
from { |
||||
opacity: 0; |
||||
transform: translateY(-20px) scale(0.95); |
||||
} |
||||
to { |
||||
opacity: 1; |
||||
transform: translateY(0) scale(1); |
||||
} |
||||
} |
||||
|
||||
.login-modal-header { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
padding: 20px 24px; |
||||
border-bottom: 1px solid var(--border-color); |
||||
background: var(--header-bg); |
||||
} |
||||
|
||||
.login-modal-header h2 { |
||||
margin: 0; |
||||
font-size: 20px; |
||||
font-weight: 600; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.close-btn { |
||||
background: none; |
||||
border: none; |
||||
font-size: 24px; |
||||
color: var(--text-secondary); |
||||
cursor: pointer; |
||||
padding: 4px; |
||||
border-radius: 4px; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.close-btn:hover { |
||||
background: var(--bg-secondary); |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.login-modal-body { |
||||
padding: 24px; |
||||
} |
||||
|
||||
.login-form { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.form-group { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.form-group label { |
||||
font-weight: 500; |
||||
color: var(--text-primary); |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.form-group input { |
||||
padding: 12px 16px; |
||||
border: 2px solid var(--border-color); |
||||
border-radius: 8px; |
||||
font-size: 14px; |
||||
background: var(--bg-secondary); |
||||
color: var(--text-primary); |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.form-group input:focus { |
||||
outline: none; |
||||
border-color: var(--primary-color); |
||||
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1); |
||||
} |
||||
|
||||
.form-group input:disabled { |
||||
opacity: 0.6; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.captcha-container { |
||||
display: flex; |
||||
gap: 12px; |
||||
align-items: stretch; |
||||
} |
||||
|
||||
.captcha-container input { |
||||
flex: 1; |
||||
} |
||||
|
||||
.captcha-image-container { |
||||
width: 100px; |
||||
height: 44px; |
||||
border-radius: 8px; |
||||
overflow: hidden; |
||||
cursor: pointer; |
||||
border: 2px solid var(--border-color); |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.captcha-image-container:hover { |
||||
border-color: var(--primary-color); |
||||
} |
||||
|
||||
.captcha-image { |
||||
width: 100%; |
||||
height: 100%; |
||||
object-fit: cover; |
||||
} |
||||
|
||||
.captcha-placeholder { |
||||
width: 100%; |
||||
height: 100%; |
||||
background: var(--bg-secondary); |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
color: var(--text-secondary); |
||||
font-size: 12px; |
||||
text-align: center; |
||||
padding: 8px; |
||||
cursor: pointer; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.captcha-placeholder:hover { |
||||
background: var(--bg-tertiary); |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.captcha-hint { |
||||
font-size: 10px; |
||||
opacity: 0.7; |
||||
margin-top: 2px; |
||||
} |
||||
|
||||
.captcha-loading-spinner { |
||||
width: 12px; |
||||
height: 12px; |
||||
border: 2px solid transparent; |
||||
border-top: 2px solid currentColor; |
||||
border-radius: 50%; |
||||
animation: spin 1s linear infinite; |
||||
margin-top: 4px; |
||||
} |
||||
|
||||
.form-actions { |
||||
margin-top: 8px; |
||||
} |
||||
|
||||
.login-btn { |
||||
width: 100%; |
||||
padding: 14px 24px; |
||||
background: var(--primary-color); |
||||
color: white; |
||||
border: none; |
||||
border-radius: 8px; |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
cursor: pointer; |
||||
transition: all 0.2s; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
gap: 8px; |
||||
} |
||||
|
||||
/* 深色主题下的登录按钮样式 */ |
||||
.theme-dark .login-btn { |
||||
background: var(--primary-color); |
||||
color: #333333; |
||||
border: 1px solid rgba(255, 255, 255, 0.1); |
||||
} |
||||
|
||||
.theme-dark .login-btn:hover:not(:disabled) { |
||||
background: var(--primary-hover); |
||||
color: #222222; |
||||
} |
||||
|
||||
.login-btn:hover:not(:disabled) { |
||||
background: var(--primary-hover); |
||||
transform: translateY(-1px); |
||||
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3); |
||||
} |
||||
|
||||
.login-btn:disabled { |
||||
opacity: 0.6; |
||||
cursor: not-allowed; |
||||
transform: none; |
||||
box-shadow: none; |
||||
} |
||||
|
||||
/* 表单验证提示样式 */ |
||||
.form-validation-hint { |
||||
margin-top: 8px; |
||||
padding: 8px 12px; |
||||
background: var(--bg-secondary); |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 6px; |
||||
font-size: 12px; |
||||
color: var(--text-secondary); |
||||
text-align: center; |
||||
} |
||||
|
||||
.form-validation-hint span { |
||||
display: block; |
||||
margin-bottom: 2px; |
||||
} |
||||
|
||||
.form-validation-hint span:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
.loading-spinner { |
||||
width: 16px; |
||||
height: 16px; |
||||
border: 2px solid transparent; |
||||
border-top: 2px solid currentColor; |
||||
border-radius: 50%; |
||||
animation: spin 1s linear infinite; |
||||
} |
||||
|
||||
@keyframes spin { |
||||
to { |
||||
transform: rotate(360deg); |
||||
} |
||||
} |
||||
|
||||
.error-message { |
||||
background: var(--error-bg); |
||||
color: var(--error-text); |
||||
padding: 12px 16px; |
||||
border-radius: 8px; |
||||
font-size: 14px; |
||||
text-align: center; |
||||
border: 1px solid var(--error-border); |
||||
} |
||||
|
||||
/* 深色主题下的错误提示样式优化 */ |
||||
.theme-dark .error-message { |
||||
background: rgba(244, 67, 54, 0.2); |
||||
color: #ffab91; |
||||
border: 1px solid rgba(244, 67, 54, 0.5); |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.login-modal-footer { |
||||
padding: 16px 24px; |
||||
border-top: 1px solid var(--border-color); |
||||
background: var(--bg-secondary); |
||||
} |
||||
|
||||
.login-tips { |
||||
margin: 0; |
||||
font-size: 12px; |
||||
color: var(--text-secondary); |
||||
text-align: center; |
||||
line-height: 1.5; |
||||
} |
||||
|
||||
.login-tips span { |
||||
display: block; |
||||
margin-bottom: 4px; |
||||
} |
||||
|
||||
.login-tips span:first-child { |
||||
font-weight: 600; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
/* 响应式设计 */ |
||||
@media (max-width: 480px) { |
||||
.login-modal { |
||||
width: 95%; |
||||
margin: 20px; |
||||
} |
||||
|
||||
.login-modal-header, |
||||
.login-modal-body, |
||||
.login-modal-footer { |
||||
padding: 16px 20px; |
||||
} |
||||
|
||||
.captcha-container { |
||||
flex-direction: column; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.captcha-image-container { |
||||
width: 100%; |
||||
height: 40px; |
||||
} |
||||
} |
||||
</style> |
Binary file not shown.
@ -1,63 +0,0 @@
@@ -1,63 +0,0 @@
|
||||
{"level":"INFO","ts":"2025-08-05T13:45:22.151+0800","caller":"backend/main.go:61","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T13:47:01.441+0800","caller":"backend/main.go:61","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T13:48:39.815+0800","caller":"logger/logger.go:47","msg":"Logger initialized"} |
||||
{"level":"INFO","ts":"2025-08-05T13:48:39.816+0800","caller":"backend/main.go:61","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T13:58:09.521+0800","caller":"logger/logger.go:47","msg":"Logger initialized"} |
||||
{"level":"INFO","ts":"2025-08-05T13:58:09.571+0800","caller":"backend/main.go:68","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T14:02:07.899+0800","caller":"logger/logger.go:47","msg":"Logger initialized"} |
||||
{"level":"INFO","ts":"2025-08-05T14:02:07.958+0800","caller":"backend/main.go:69","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T14:08:25.003+0800","caller":"logger/logger.go:47","msg":"Logger initialized"} |
||||
{"level":"INFO","ts":"2025-08-05T14:08:25.040+0800","caller":"backend/main.go:65","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T14:12:48.479+0800","caller":"logger/logger.go:47","msg":"Logger initialized"} |
||||
{"level":"INFO","ts":"2025-08-05T14:12:48.521+0800","caller":"backend/main.go:65","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T14:14:53.222+0800","caller":"logger/logger.go:51","msg":"Logger initialized"} |
||||
{"level":"INFO","ts":"2025-08-05T14:14:53.236+0800","caller":"backend/main.go:65","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
{"level":"INFO","ts":"2025-08-05T14:22:08.260+0800","caller":"backend/main.go:65","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
test log entry |
||||
{"level":"info","ts":1754375400.4735792,"caller":"backend/main.go:65","msg":"Logger initialized","level":"debug","path":"./logs/app.log"} |
||||
test log entry |
||||
{"level":"info","ts":1754375473.419135,"caller":"backend/main.go:65","msg":"Logger initialized"} |
||||
test log entry |
||||
{"level":"info","ts":1754375492.219314,"caller":"backend/main.go:63","msg":"Logger initialized"} |
||||
{"level":"info","ts":1754375492.2198336,"caller":"backend/main.go:66","msg":"Logger initialized"} |
||||
test log entry |
||||
test log entry |
||||
test log entry |
||||
{"level":"info","ts":1754375654.4087517,"caller":"backend/main.go:66","msg":"test log entry"} |
||||
test log entry |
||||
{"level":"info","ts":1754375919.7963736,"caller":"backend/main.go:66","msg":"test log entry"} |
||||
{"level":"info","ts":1754376073.0591264,"caller":"backend/main.go:73","msg":"test log entry"} |
||||
{"level":"info","ts":1754376073.0844045,"caller":"backend/main.go:79","msg":"test log entry"} |
||||
{"level":"info","ts":1754376169.1850493,"caller":"backend/main.go:73","msg":"test log entry"} |
||||
{"level":"info","ts":1754376169.1991498,"caller":"backend/main.go:78","msg":"test log entry"} |
||||
{"level":"info","ts":1754376316.8622355,"caller":"backend/main.go:73","msg":"test log entry"} |
||||
{"level":"info","ts":1754376316.8788002,"caller":"backend/main.go:77","msg":"Logger Synced"} |
||||
{"level":"info","ts":1754376417.6815078,"caller":"backend/main.go:65","msg":"Logger initialized"} |
||||
{"level":"info","ts":1754376473.3197138,"caller":"backend/main.go:65","msg":"Logger initialized"} |
||||
{"level":"info","ts":1754376473.7748687,"caller":"backend/main.go:78","msg":"Database connected and migrated successfully"} |
||||
{"level":"info","ts":1754376725.2128773,"caller":"backend/main.go:65","msg":"Logger initialized"} |
||||
{"level":"info","ts":1754376725.7027845,"caller":"backend/main.go:78","msg":"Database connected and migrated successfully"} |
||||
{"level":"info","ts":1754376725.7028782,"caller":"database/redis_client.go:23","msg":"Initializing Redis client","host":"localhost","port":"6379","db":0,"has_password":false} |
||||
{"level":"warn","ts":1754376725.7784379,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":1,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754376726.8704906,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":2,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754376727.9956472,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"fatal","ts":1754376728.997498,"caller":"database/redis_client.go:56","msg":"Failed to connect to Redis after retries","retries":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it.","stacktrace":"gofaster/internal/shared/pkg/database.NewRedisClient\n\tD:/aigc/manta/gofaster/backend/internal/shared/pkg/database/redis_client.go:56\nmain.main\n\tD:/aigc/manta/gofaster/backend/main.go:81\nruntime.main\n\tD:/Go/src/runtime/proc.go:283"} |
||||
{"level":"info","ts":1754376840.0660605,"caller":"backend/main.go:65","msg":"Logger initialized"} |
||||
{"level":"info","ts":1754376840.3610125,"caller":"backend/main.go:78","msg":"Database connected and migrated successfully"} |
||||
{"level":"info","ts":1754376840.3615417,"caller":"database/redis_client.go:23","msg":"Initializing Redis client","host":"localhost","port":"6379","db":0,"has_password":false} |
||||
{"level":"warn","ts":1754376840.4347446,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":1,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754376841.5154507,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":2,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754376842.636067,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"fatal","ts":1754376843.637513,"caller":"database/redis_client.go:56","msg":"Failed to connect to Redis after retries","retries":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it.","stacktrace":"gofaster/internal/shared/pkg/database.NewRedisClient\n\tD:/aigc/manta/gofaster/backend/internal/shared/pkg/database/redis_client.go:56\nmain.main\n\tD:/aigc/manta/gofaster/backend/main.go:81\nruntime.main\n\tD:/Go/src/runtime/proc.go:283"} |
||||
{"level":"info","ts":1754376853.4036381,"caller":"backend/main.go:65","msg":"Logger initialized"} |
||||
{"level":"info","ts":1754376853.681494,"caller":"backend/main.go:78","msg":"Database connected and migrated successfully"} |
||||
{"level":"info","ts":1754376853.6816995,"caller":"database/redis_client.go:23","msg":"Initializing Redis client","host":"localhost","port":"6379","db":0,"has_password":false} |
||||
{"level":"warn","ts":1754376853.755178,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":1,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754376854.8357182,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":2,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754376855.9569952,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"fatal","ts":1754376856.9581835,"caller":"database/redis_client.go:56","msg":"Failed to connect to Redis after retries","retries":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it.","stacktrace":"gofaster/internal/shared/pkg/database.NewRedisClient\n\tD:/aigc/manta/gofaster/backend/internal/shared/pkg/database/redis_client.go:56\nmain.main\n\tD:/aigc/manta/gofaster/backend/main.go:81\nruntime.main\n\tD:/Go/src/runtime/proc.go:283"} |
||||
{"level":"info","ts":1754381540.3083673,"caller":"database/redis_client.go:23","msg":"Initializing Redis client","host":"localhost","port":"6379","db":0,"has_password":false} |
||||
{"level":"warn","ts":1754381540.3953142,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":1,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754381541.4798021,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":2,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"warn","ts":1754381542.6024618,"caller":"database/redis_client.go:45","msg":"Redis connection attempt failed","attempt":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it."} |
||||
{"level":"fatal","ts":1754381543.60342,"caller":"database/redis_client.go:56","msg":"Failed to connect to Redis after retries","retries":3,"error":"dial tcp [::1]:6379: connectex: No connection could be made because the target machine actively refused it.","stacktrace":"gofaster/internal/shared/database.NewRedisClient\n\tD:/aigc/manta/gofaster/backend/internal/shared/database/redis_client.go:56\nmain.main\n\tD:/aigc/manta/gofaster/backend/main.go:59\nruntime.main\n\tD:/Go/src/runtime/proc.go:283"} |
Loading…
Reference in new issue