feat(auth): 添加二维码登录功能- 实现了二维码登录的前端组件和API接口
- 添加了二维码登录的后端逻辑和数据库设计 - 编写了详细的使用说明和接口文档 - 提供了演示页面和测试工具
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
VITE_APP_NAME=后台管理(开发环境)
|
VITE_APP_NAME=后台管理(开发环境)
|
||||||
VITE_API_URL=http://127.0.0.1:9200/api
|
#VITE_API_URL=http://127.0.0.1:9200/api
|
||||||
#VITE_SERVER_API_URL=http://127.0.0.1:8000/api
|
#VITE_SERVER_API_URL=http://127.0.0.1:8000/api
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
236
README-QR-LOGIN.md
Normal file
236
README-QR-LOGIN.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# 二维码登录功能
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
基于Vue 3 + TypeScript开发的二维码登录功能,支持APP端和小程序端扫码登录到Web管理后台。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- ✅ **便捷登录**:扫码即可登录,无需输入账号密码
|
||||||
|
- ✅ **实时状态**:支持实时状态更新和用户反馈
|
||||||
|
- ✅ **安全可靠**:二维码具有时效性,支持一次性使用
|
||||||
|
- ✅ **跨平台支持**:兼容APP和小程序扫码
|
||||||
|
- ✅ **响应式设计**:适配各种屏幕尺寸
|
||||||
|
- ✅ **TypeScript支持**:完整的类型定义
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/QrLogin/
|
||||||
|
│ ├── index.vue # 二维码登录主组件
|
||||||
|
│ └── demo.vue # 演示组件
|
||||||
|
├── views/passport/
|
||||||
|
│ ├── login/index.vue # 登录页面(已集成二维码登录)
|
||||||
|
│ └── qrConfirm/index.vue # 移动端确认页面
|
||||||
|
├── api/passport/
|
||||||
|
│ └── qrLogin/index.ts # 二维码登录API
|
||||||
|
└── router/routes.ts # 路由配置
|
||||||
|
|
||||||
|
docs/
|
||||||
|
├── qr-login-api.md # API接口文档
|
||||||
|
└── qr-login-usage.md # 使用说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 查看演示
|
||||||
|
|
||||||
|
访问演示页面查看功能效果:
|
||||||
|
```
|
||||||
|
http://localhost:3000/qr-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在登录页面使用
|
||||||
|
|
||||||
|
登录页面已经集成了二维码登录功能:
|
||||||
|
```
|
||||||
|
http://localhost:3000/login
|
||||||
|
```
|
||||||
|
点击右上角的二维码图标即可切换到扫码登录模式。
|
||||||
|
|
||||||
|
### 3. 移动端确认页面
|
||||||
|
|
||||||
|
扫码后会跳转到确认页面:
|
||||||
|
```
|
||||||
|
http://localhost:3000/qr-confirm?qrCodeKey=xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件使用
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<QrLogin
|
||||||
|
@loginSuccess="handleLoginSuccess"
|
||||||
|
@loginError="handleLoginError"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import QrLogin from '@/components/QrLogin/index.vue';
|
||||||
|
|
||||||
|
const handleLoginSuccess = (token) => {
|
||||||
|
console.log('登录成功,token:', token);
|
||||||
|
// 处理登录成功逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginError = (error) => {
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
// 处理登录失败逻辑
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件说明
|
||||||
|
|
||||||
|
| 事件名 | 参数 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| loginSuccess | token: string | 登录成功时触发,返回登录token |
|
||||||
|
| loginError | error: string | 登录失败时触发,返回错误信息 |
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 需要实现的后端接口
|
||||||
|
|
||||||
|
1. **生成二维码**: `POST /api/qr-login/generate`
|
||||||
|
2. **检查状态**: `GET /api/qr-login/status`
|
||||||
|
3. **扫码标记**: `POST /api/qr-login/scan`
|
||||||
|
4. **确认登录**: `POST /api/qr-login/confirm`
|
||||||
|
5. **取消登录**: `POST /api/qr-login/cancel`
|
||||||
|
|
||||||
|
详细的API文档请查看:[docs/qr-login-api.md](docs/qr-login-api.md)
|
||||||
|
|
||||||
|
## 状态流转
|
||||||
|
|
||||||
|
```
|
||||||
|
loading → active → scanned → confirmed ✅
|
||||||
|
↓ ↓ ↓
|
||||||
|
error expired cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
- **loading**: 正在生成二维码
|
||||||
|
- **active**: 二维码有效,等待扫码
|
||||||
|
- **scanned**: 已扫码,等待用户确认
|
||||||
|
- **confirmed**: 用户确认,登录成功
|
||||||
|
- **expired**: 二维码过期
|
||||||
|
- **error**: 生成失败
|
||||||
|
- **cancelled**: 用户取消登录
|
||||||
|
|
||||||
|
## 安全机制
|
||||||
|
|
||||||
|
1. **时效控制**:二维码默认5分钟过期
|
||||||
|
2. **一次性使用**:每个二维码只能使用一次
|
||||||
|
3. **状态验证**:严格的状态流转控制
|
||||||
|
4. **用户验证**:移动端需要用户登录状态
|
||||||
|
5. **HTTPS传输**:敏感数据加密传输
|
||||||
|
|
||||||
|
## 自定义配置
|
||||||
|
|
||||||
|
### 样式自定义
|
||||||
|
|
||||||
|
```less
|
||||||
|
.qr-login-container {
|
||||||
|
// 自定义容器样式
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
// 自定义二维码区域样式
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 二维码大小
|
||||||
|
const QR_CODE_SIZE = 200;
|
||||||
|
|
||||||
|
// 过期时间(秒)
|
||||||
|
const EXPIRE_TIME = 300;
|
||||||
|
|
||||||
|
// 状态检查间隔(毫秒)
|
||||||
|
const CHECK_INTERVAL = 2000;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发调试
|
||||||
|
|
||||||
|
### 启用调试模式
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在浏览器控制台执行
|
||||||
|
localStorage.setItem('debug', 'qr-login');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看网络请求
|
||||||
|
|
||||||
|
使用浏览器开发者工具的Network面板监控API请求。
|
||||||
|
|
||||||
|
### 模拟测试
|
||||||
|
|
||||||
|
访问演示页面 `/qr-demo` 可以模拟各种状态和场景。
|
||||||
|
|
||||||
|
## 部署注意事项
|
||||||
|
|
||||||
|
1. **HTTPS要求**:生产环境必须使用HTTPS
|
||||||
|
2. **跨域配置**:确保API接口支持跨域请求
|
||||||
|
3. **移动端适配**:确保移动端页面正常显示
|
||||||
|
4. **性能优化**:合理设置轮询间隔和缓存策略
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **二维码不显示**
|
||||||
|
- 检查网络连接
|
||||||
|
- 确认API接口正常
|
||||||
|
- 查看控制台错误信息
|
||||||
|
|
||||||
|
2. **扫码无响应**
|
||||||
|
- 检查二维码是否过期
|
||||||
|
- 确认移动端网络正常
|
||||||
|
- 验证用户登录状态
|
||||||
|
|
||||||
|
3. **登录失败**
|
||||||
|
- 检查token有效性
|
||||||
|
- 确认用户权限
|
||||||
|
- 查看后端日志
|
||||||
|
|
||||||
|
### 调试步骤
|
||||||
|
|
||||||
|
1. 打开浏览器开发者工具
|
||||||
|
2. 查看Console面板的错误信息
|
||||||
|
3. 监控Network面板的API请求
|
||||||
|
4. 检查Application面板的本地存储
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2024-01-XX)
|
||||||
|
- ✅ 完成基础二维码登录功能
|
||||||
|
- ✅ 支持实时状态更新
|
||||||
|
- ✅ 集成到登录页面
|
||||||
|
- ✅ 创建移动端确认页面
|
||||||
|
- ✅ 完善文档和演示
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端框架**: Vue 3 + TypeScript
|
||||||
|
- **UI组件库**: Ant Design Vue
|
||||||
|
- **二维码生成**: qrcode + ele-admin-pro
|
||||||
|
- **状态管理**: Pinia
|
||||||
|
- **路由管理**: Vue Router
|
||||||
|
- **HTTP客户端**: Axios
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
1. Fork 项目
|
||||||
|
2. 创建功能分支
|
||||||
|
3. 提交代码变更
|
||||||
|
4. 推送到分支
|
||||||
|
5. 创建 Pull Request
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
212
docs/qr-login-api.md
Normal file
212
docs/qr-login-api.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# 二维码登录API接口文档(已适配后端)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
二维码登录功能允许用户通过手机APP或小程序扫描Web端生成的二维码来完成登录,提供更便捷和安全的登录体验。
|
||||||
|
|
||||||
|
**后端实现状态:** ✅ 已完成
|
||||||
|
**前端适配状态:** ✅ 已完成
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 生成登录二维码
|
||||||
|
|
||||||
|
**接口地址:** `POST /api/qr-login/generate`
|
||||||
|
|
||||||
|
**请求参数:** 无
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "生成成功",
|
||||||
|
"data": {
|
||||||
|
"token": "abc123def456",
|
||||||
|
"qrCode": "qr-login:abc123def456",
|
||||||
|
"expiresIn": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明:**
|
||||||
|
- `token`: 二维码唯一标识token,用于后续状态查询
|
||||||
|
- `qrCode`: 二维码内容(后端生成的原始内容)
|
||||||
|
- `expiresIn`: 过期时间(秒),默认300秒(5分钟)
|
||||||
|
|
||||||
|
### 2. 检查二维码状态
|
||||||
|
|
||||||
|
**接口地址:** `GET /api/qr-login/status/{token}`
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
- `token`: 二维码token(路径参数)
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "查询成功",
|
||||||
|
"data": {
|
||||||
|
"status": "pending",
|
||||||
|
"expiresIn": 280
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态说明:**
|
||||||
|
- `pending`: 等待扫码
|
||||||
|
- `scanned`: 已扫码,等待确认
|
||||||
|
- `confirmed`: 已确认登录
|
||||||
|
- `expired`: 已过期
|
||||||
|
|
||||||
|
**登录成功时的响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"status": "confirmed",
|
||||||
|
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"userInfo": {
|
||||||
|
"userId": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"nickname": "管理员",
|
||||||
|
"avatar": "https://example.com/avatar.jpg"
|
||||||
|
},
|
||||||
|
"expiresIn": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 扫码标记
|
||||||
|
|
||||||
|
**接口地址:** `POST /api/qr-login/scan/{token}`
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
- `token`: 二维码token(路径参数)
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "操作成功",
|
||||||
|
"data": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 确认登录
|
||||||
|
|
||||||
|
**接口地址:** `POST /api/qr-login/confirm`
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "abc123def456",
|
||||||
|
"userId": 1,
|
||||||
|
"platform": "web"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "确认成功",
|
||||||
|
"data": {
|
||||||
|
"status": "confirmed",
|
||||||
|
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"userInfo": {
|
||||||
|
"userId": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"nickname": "管理员"
|
||||||
|
},
|
||||||
|
"expiresIn": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 微信小程序确认登录
|
||||||
|
|
||||||
|
**接口地址:** `POST /api/qr-login/wechat-confirm`
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "abc123def456",
|
||||||
|
"userId": 1,
|
||||||
|
"platform": "miniprogram",
|
||||||
|
"wechatInfo": {
|
||||||
|
"openid": "wx_openid_123",
|
||||||
|
"unionid": "wx_unionid_456",
|
||||||
|
"nickname": "微信用户",
|
||||||
|
"avatar": "https://wx.qlogo.cn/..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "微信小程序登录确认成功",
|
||||||
|
"data": {
|
||||||
|
"status": "confirmed",
|
||||||
|
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"userInfo": {...},
|
||||||
|
"expiresIn": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现流程
|
||||||
|
|
||||||
|
### Web端流程:
|
||||||
|
1. 用户点击扫码登录
|
||||||
|
2. 前端调用 `/qr-login/generate` 生成二维码
|
||||||
|
3. 显示二维码给用户
|
||||||
|
4. 前端轮询调用 `/qr-login/status` 检查状态
|
||||||
|
5. 当状态为 `confirmed` 时,获取token完成登录
|
||||||
|
|
||||||
|
### 移动端流程:
|
||||||
|
1. 用户扫描二维码,跳转到确认页面
|
||||||
|
2. 页面加载时调用 `/qr-login/scan` 标记已扫码
|
||||||
|
3. 用户点击确认后调用 `/qr-login/confirm` 确认登录
|
||||||
|
4. 或用户点击取消后调用 `/qr-login/cancel` 取消登录
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
1. **二维码有效期**:建议设置5分钟有效期,过期后需要重新生成
|
||||||
|
2. **一次性使用**:每个二维码只能使用一次,确认或取消后立即失效
|
||||||
|
3. **用户验证**:移动端需要验证用户的登录状态
|
||||||
|
4. **IP限制**:可以记录生成二维码的IP,限制异地登录
|
||||||
|
5. **频率限制**:限制同一IP生成二维码的频率
|
||||||
|
|
||||||
|
## 数据库设计建议
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE qr_login_records (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
qr_code_key VARCHAR(64) UNIQUE NOT NULL COMMENT '二维码唯一标识',
|
||||||
|
status ENUM('waiting', 'scanned', 'confirmed', 'expired', 'cancelled') DEFAULT 'waiting' COMMENT '状态',
|
||||||
|
user_id BIGINT NULL COMMENT '扫码用户ID',
|
||||||
|
client_ip VARCHAR(45) COMMENT '客户端IP',
|
||||||
|
user_agent TEXT COMMENT '用户代理',
|
||||||
|
expire_time DATETIME NOT NULL COMMENT '过期时间',
|
||||||
|
scan_time DATETIME NULL COMMENT '扫码时间',
|
||||||
|
confirm_time DATETIME NULL COMMENT '确认时间',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_qr_code_key (qr_code_key),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_expire_time (expire_time)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误码说明
|
||||||
|
|
||||||
|
- `0`: 成功
|
||||||
|
- `400`: 参数错误
|
||||||
|
- `401`: 未授权
|
||||||
|
- `404`: 二维码不存在
|
||||||
|
- `410`: 二维码已过期
|
||||||
|
- `429`: 请求过于频繁
|
||||||
|
- `500`: 服务器内部错误
|
||||||
262
docs/qr-login-setup-guide.md
Normal file
262
docs/qr-login-setup-guide.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# 二维码登录功能设置指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本指南将帮助您设置和测试二维码登录功能。后端Java代码已经完成,前端Vue代码已经适配完成。
|
||||||
|
|
||||||
|
## 🎯 功能状态
|
||||||
|
|
||||||
|
- ✅ **后端实现**: Java Spring Boot (已完成)
|
||||||
|
- ✅ **前端适配**: Vue 3 + TypeScript (已完成)
|
||||||
|
- ✅ **接口对接**: API接口已适配
|
||||||
|
- ✅ **测试页面**: 提供完整的测试工具
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 启动后端服务
|
||||||
|
|
||||||
|
确保您的Java后端服务正在运行,并且包含以下文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
java/auto/
|
||||||
|
├── controller/QrLoginController.java
|
||||||
|
├── service/QrLoginService.java
|
||||||
|
├── service/impl/QrLoginServiceImpl.java
|
||||||
|
└── dto/
|
||||||
|
├── QrLoginGenerateResponse.java
|
||||||
|
├── QrLoginStatusResponse.java
|
||||||
|
├── QrLoginConfirmRequest.java
|
||||||
|
└── QrLoginData.java
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动前端服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# 或
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试接口连通性
|
||||||
|
|
||||||
|
访问接口测试页面:
|
||||||
|
```
|
||||||
|
http://localhost:3000/qr-test
|
||||||
|
```
|
||||||
|
|
||||||
|
按照以下步骤测试:
|
||||||
|
|
||||||
|
1. **生成二维码** - 点击"生成二维码"按钮
|
||||||
|
2. **检查状态** - 开启"自动检查"开关
|
||||||
|
3. **模拟扫码** - 点击"模拟扫码"按钮
|
||||||
|
4. **确认登录** - 输入用户ID,点击"确认登录"
|
||||||
|
|
||||||
|
## 📱 使用流程
|
||||||
|
|
||||||
|
### Web端操作
|
||||||
|
|
||||||
|
1. **进入登录页面**
|
||||||
|
```
|
||||||
|
http://localhost:3000/login
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **切换到扫码登录**
|
||||||
|
- 点击右上角的二维码图标
|
||||||
|
- 系统自动生成二维码
|
||||||
|
|
||||||
|
3. **等待扫码**
|
||||||
|
- 二维码有效期5分钟
|
||||||
|
- 系统每2秒检查一次状态
|
||||||
|
|
||||||
|
### 移动端操作
|
||||||
|
|
||||||
|
1. **扫描二维码**
|
||||||
|
- 使用手机扫描Web端的二维码
|
||||||
|
- 或直接访问二维码中的URL
|
||||||
|
|
||||||
|
2. **确认登录**
|
||||||
|
```
|
||||||
|
http://localhost:3000/qr-confirm?qrCodeKey=abc123def456
|
||||||
|
```
|
||||||
|
- 显示用户信息和设备信息
|
||||||
|
- 点击"确认登录"完成登录
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### 后端配置
|
||||||
|
|
||||||
|
在 `application.yml` 中配置JWT相关参数:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
config:
|
||||||
|
jwt:
|
||||||
|
secret: websoft-jwt-secret-key-2025
|
||||||
|
expire: 86400 # 24小时
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端配置
|
||||||
|
|
||||||
|
在 `src/config/setting.ts` 中确认API地址:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const SERVER_API_URL = 'http://localhost:8080';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 测试场景
|
||||||
|
|
||||||
|
### 1. 正常登录流程
|
||||||
|
|
||||||
|
1. Web端生成二维码
|
||||||
|
2. 移动端扫码
|
||||||
|
3. 移动端确认登录
|
||||||
|
4. Web端自动登录成功
|
||||||
|
|
||||||
|
### 2. 过期场景
|
||||||
|
|
||||||
|
1. 生成二维码后等待5分钟
|
||||||
|
2. 二维码自动过期
|
||||||
|
3. 点击刷新重新生成
|
||||||
|
|
||||||
|
### 3. 取消场景
|
||||||
|
|
||||||
|
1. 移动端扫码后点击取消
|
||||||
|
2. Web端继续等待新的扫码
|
||||||
|
|
||||||
|
## 🔍 调试方法
|
||||||
|
|
||||||
|
### 1. 查看网络请求
|
||||||
|
|
||||||
|
打开浏览器开发者工具 → Network面板:
|
||||||
|
|
||||||
|
- `POST /api/qr-login/generate` - 生成二维码
|
||||||
|
- `GET /api/qr-login/status/{token}` - 检查状态
|
||||||
|
- `POST /api/qr-login/scan/{token}` - 扫码标记
|
||||||
|
- `POST /api/qr-login/confirm` - 确认登录
|
||||||
|
|
||||||
|
### 2. 查看控制台日志
|
||||||
|
|
||||||
|
前端会输出详细的调试信息:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 开启调试模式
|
||||||
|
localStorage.setItem('debug', 'qr-login');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 后端日志
|
||||||
|
|
||||||
|
查看后端控制台输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
生成扫码登录token: abc123def456
|
||||||
|
用户 admin 确认扫码登录,token: abc123def456
|
||||||
|
扫码登录token abc123def456 状态更新为已扫码
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 常见问题
|
||||||
|
|
||||||
|
### 1. 二维码生成失败
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
- 后端服务未启动
|
||||||
|
- Redis服务未启动
|
||||||
|
- 网络连接问题
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 检查后端服务状态
|
||||||
|
- 确认Redis连接正常
|
||||||
|
- 查看控制台错误信息
|
||||||
|
|
||||||
|
### 2. 扫码后无响应
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
- 二维码已过期
|
||||||
|
- 用户未登录
|
||||||
|
- 网络请求失败
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 刷新二维码
|
||||||
|
- 确认用户登录状态
|
||||||
|
- 检查网络连接
|
||||||
|
|
||||||
|
### 3. 确认登录失败
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
- 用户ID不存在
|
||||||
|
- 用户状态异常
|
||||||
|
- JWT配置错误
|
||||||
|
|
||||||
|
**解决方法:**
|
||||||
|
- 检查用户数据
|
||||||
|
- 确认用户状态正常
|
||||||
|
- 验证JWT配置
|
||||||
|
|
||||||
|
## 📋 API接口清单
|
||||||
|
|
||||||
|
| 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 生成二维码 | POST | `/api/qr-login/generate` | 生成登录二维码 |
|
||||||
|
| 检查状态 | GET | `/api/qr-login/status/{token}` | 检查二维码状态 |
|
||||||
|
| 扫码标记 | POST | `/api/qr-login/scan/{token}` | 标记已扫码 |
|
||||||
|
| 确认登录 | POST | `/api/qr-login/confirm` | 确认登录 |
|
||||||
|
| 微信确认 | POST | `/api/qr-login/wechat-confirm` | 微信小程序确认 |
|
||||||
|
|
||||||
|
## 🔐 安全特性
|
||||||
|
|
||||||
|
1. **时效控制**: 二维码5分钟自动过期
|
||||||
|
2. **一次性使用**: 每个二维码只能使用一次
|
||||||
|
3. **状态验证**: 严格的状态流转控制
|
||||||
|
4. **JWT安全**: 使用JWT进行身份验证
|
||||||
|
5. **Redis存储**: 使用Redis存储临时数据
|
||||||
|
|
||||||
|
## 📈 性能优化
|
||||||
|
|
||||||
|
1. **轮询间隔**: 前端每2秒检查一次状态
|
||||||
|
2. **缓存策略**: Redis自动过期清理
|
||||||
|
3. **并发控制**: 支持多用户同时使用
|
||||||
|
4. **资源清理**: 及时清理过期数据
|
||||||
|
|
||||||
|
## 🎨 自定义配置
|
||||||
|
|
||||||
|
### 修改过期时间
|
||||||
|
|
||||||
|
在后端常量中修改:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 默认5分钟 = 300秒
|
||||||
|
private static final Long QR_LOGIN_TOKEN_TTL = 300L;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改检查间隔
|
||||||
|
|
||||||
|
在前端组件中修改:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 默认2秒检查一次
|
||||||
|
}, 2000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改二维码样式
|
||||||
|
|
||||||
|
在前端组件中修改:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<ele-qr-code-svg :value="qrCodeData" :size="200" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
如果遇到问题,请:
|
||||||
|
|
||||||
|
1. 查看控制台错误信息
|
||||||
|
2. 检查网络请求状态
|
||||||
|
3. 确认后端服务正常
|
||||||
|
4. 查看本文档的常见问题部分
|
||||||
|
|
||||||
|
## 🔄 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (当前版本)
|
||||||
|
- ✅ 完成后端Java实现
|
||||||
|
- ✅ 完成前端Vue适配
|
||||||
|
- ✅ 提供完整测试工具
|
||||||
|
- ✅ 支持Web端和移动端
|
||||||
|
- ✅ 支持微信小程序登录
|
||||||
234
docs/qr-login-usage.md
Normal file
234
docs/qr-login-usage.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# 二维码登录功能使用说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
二维码登录功能为用户提供了一种便捷的登录方式,用户可以通过手机APP或小程序扫描Web端生成的二维码来快速登录管理后台,无需输入用户名和密码。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
1. **便捷性**:无需输入账号密码,扫码即可登录
|
||||||
|
2. **安全性**:二维码具有时效性,过期自动失效
|
||||||
|
3. **实时性**:支持实时状态更新,用户体验流畅
|
||||||
|
4. **跨平台**:支持APP和小程序扫码登录
|
||||||
|
|
||||||
|
## 使用流程
|
||||||
|
|
||||||
|
### Web端操作流程
|
||||||
|
|
||||||
|
1. **进入登录页面**
|
||||||
|
- 访问系统登录页面
|
||||||
|
- 点击右上角的二维码图标切换到扫码登录模式
|
||||||
|
|
||||||
|
2. **生成二维码**
|
||||||
|
- 系统自动生成登录二维码
|
||||||
|
- 二维码有效期为5分钟
|
||||||
|
|
||||||
|
3. **等待扫码**
|
||||||
|
- 使用手机APP或小程序扫描二维码
|
||||||
|
- 系统实时检测扫码状态
|
||||||
|
|
||||||
|
4. **完成登录**
|
||||||
|
- 用户在手机端确认登录后,Web端自动完成登录
|
||||||
|
- 跳转到系统首页
|
||||||
|
|
||||||
|
### 移动端操作流程
|
||||||
|
|
||||||
|
1. **扫描二维码**
|
||||||
|
- 打开手机APP或小程序
|
||||||
|
- 使用扫码功能扫描Web端的二维码
|
||||||
|
|
||||||
|
2. **确认登录**
|
||||||
|
- 跳转到登录确认页面
|
||||||
|
- 显示用户信息和设备信息
|
||||||
|
- 点击"确认登录"按钮
|
||||||
|
|
||||||
|
3. **完成登录**
|
||||||
|
- 系统完成登录验证
|
||||||
|
- Web端自动登录成功
|
||||||
|
|
||||||
|
## 组件使用
|
||||||
|
|
||||||
|
### 在登录页面中集成
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div v-if="loginType === 'scan'">
|
||||||
|
<QrLogin
|
||||||
|
@loginSuccess="onQrLoginSuccess"
|
||||||
|
@loginError="onQrLoginError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import QrLogin from '@/components/QrLogin/index.vue';
|
||||||
|
|
||||||
|
const onQrLoginSuccess = (token) => {
|
||||||
|
// 处理登录成功
|
||||||
|
localStorage.setItem('access_token', token);
|
||||||
|
// 跳转到首页
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQrLoginError = (error) => {
|
||||||
|
// 处理登录错误
|
||||||
|
message.error(error);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 独立使用二维码组件
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<QrLogin
|
||||||
|
@loginSuccess="handleLoginSuccess"
|
||||||
|
@loginError="handleLoginError"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import QrLogin from '@/components/QrLogin/index.vue';
|
||||||
|
|
||||||
|
const handleLoginSuccess = (token) => {
|
||||||
|
console.log('登录成功,token:', token);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginError = (error) => {
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 前端API调用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
generateQrCode,
|
||||||
|
checkQrCodeStatus,
|
||||||
|
confirmQrLogin,
|
||||||
|
cancelQrLogin
|
||||||
|
} from '@/api/passport/qrLogin';
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
const qrData = await generateQrCode();
|
||||||
|
|
||||||
|
// 检查状态
|
||||||
|
const status = await checkQrCodeStatus(qrCodeKey);
|
||||||
|
|
||||||
|
// 确认登录(移动端)
|
||||||
|
await confirmQrLogin(qrCodeKey, userToken);
|
||||||
|
|
||||||
|
// 取消登录(移动端)
|
||||||
|
await cancelQrLogin(qrCodeKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 状态说明
|
||||||
|
|
||||||
|
| 状态 | 说明 | 显示内容 |
|
||||||
|
|------|------|----------|
|
||||||
|
| loading | 正在生成二维码 | 加载动画 + "正在生成二维码..." |
|
||||||
|
| active | 二维码有效,等待扫码 | 二维码 + "请使用手机APP或小程序扫码登录" |
|
||||||
|
| scanned | 已扫码,等待确认 | 成功图标 + "扫码成功,请在手机上确认登录" |
|
||||||
|
| expired | 二维码已过期 | 过期图标 + "二维码已过期" + 刷新按钮 |
|
||||||
|
| error | 生成失败 | 错误图标 + 错误信息 + 重新生成按钮 |
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 二维码配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 二维码大小
|
||||||
|
const qrCodeSize = 200; // 像素
|
||||||
|
|
||||||
|
// 过期时间
|
||||||
|
const expireTime = 300; // 5分钟
|
||||||
|
|
||||||
|
// 检查间隔
|
||||||
|
const checkInterval = 2000; // 2秒
|
||||||
|
```
|
||||||
|
|
||||||
|
### 样式自定义
|
||||||
|
|
||||||
|
```less
|
||||||
|
// 自定义二维码容器样式
|
||||||
|
.qr-login-container {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
min-height: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
1. **二维码时效性**
|
||||||
|
- 二维码默认5分钟过期
|
||||||
|
- 过期后需要重新生成
|
||||||
|
|
||||||
|
2. **一次性使用**
|
||||||
|
- 每个二维码只能使用一次
|
||||||
|
- 登录成功或取消后立即失效
|
||||||
|
|
||||||
|
3. **用户验证**
|
||||||
|
- 移动端需要用户已登录状态
|
||||||
|
- 验证用户身份后才能确认登录
|
||||||
|
|
||||||
|
4. **网络安全**
|
||||||
|
- 使用HTTPS协议传输
|
||||||
|
- 敏感信息加密处理
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **二维码生成失败**
|
||||||
|
- 检查网络连接
|
||||||
|
- 确认后端API接口正常
|
||||||
|
- 查看浏览器控制台错误信息
|
||||||
|
|
||||||
|
2. **扫码后无响应**
|
||||||
|
- 检查移动端网络连接
|
||||||
|
- 确认二维码未过期
|
||||||
|
- 检查用户登录状态
|
||||||
|
|
||||||
|
3. **登录确认失败**
|
||||||
|
- 检查用户权限
|
||||||
|
- 确认token有效性
|
||||||
|
- 查看后端日志
|
||||||
|
|
||||||
|
### 调试方法
|
||||||
|
|
||||||
|
1. **开启控制台调试**
|
||||||
|
```javascript
|
||||||
|
// 在浏览器控制台查看详细日志
|
||||||
|
localStorage.setItem('debug', 'qr-login');
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **网络请求监控**
|
||||||
|
- 使用浏览器开发者工具监控网络请求
|
||||||
|
- 检查API响应状态和数据
|
||||||
|
|
||||||
|
3. **状态跟踪**
|
||||||
|
- 观察二维码状态变化
|
||||||
|
- 记录状态转换时间点
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- 初始版本发布
|
||||||
|
- 支持基本的二维码登录功能
|
||||||
|
- 包含Web端和移动端完整流程
|
||||||
|
|
||||||
|
### 后续计划
|
||||||
|
- 支持多设备同时登录
|
||||||
|
- 添加登录设备管理
|
||||||
|
- 优化用户体验和界面设计
|
||||||
109
src/api/passport/qrLogin/index.ts
Normal file
109
src/api/passport/qrLogin/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult } from '@/api';
|
||||||
|
import { SERVER_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码生成响应数据
|
||||||
|
*/
|
||||||
|
export interface QrCodeResponse {
|
||||||
|
token: string; // 二维码唯一标识token
|
||||||
|
qrCode: string; // 二维码内容
|
||||||
|
expiresIn: number; // 过期时间(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码状态响应
|
||||||
|
*/
|
||||||
|
export interface QrCodeStatusResponse {
|
||||||
|
status: 'pending' | 'scanned' | 'confirmed' | 'expired';
|
||||||
|
accessToken?: string; // 登录成功时返回的JWT token
|
||||||
|
userInfo?: any; // 用户信息
|
||||||
|
expiresIn?: number; // 剩余过期时间(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认登录请求参数
|
||||||
|
*/
|
||||||
|
export interface QrLoginConfirmRequest {
|
||||||
|
token: string; // 二维码token
|
||||||
|
userId?: number; // 用户ID
|
||||||
|
platform?: string; // 登录平台
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成登录二维码
|
||||||
|
*/
|
||||||
|
export async function generateQrCode(): Promise<QrCodeResponse> {
|
||||||
|
const res = await request.post<ApiResult<QrCodeResponse>>(
|
||||||
|
SERVER_API_URL + '/qr-login/generate',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(res.data.message || '生成二维码失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查二维码状态
|
||||||
|
*/
|
||||||
|
export async function checkQrCodeStatus(token: string): Promise<QrCodeStatusResponse> {
|
||||||
|
const res = await request.get<ApiResult<QrCodeStatusResponse>>(
|
||||||
|
SERVER_API_URL + `/qr-login/status/${token}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(res.data.message || '检查二维码状态失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码确认登录(移动端调用)
|
||||||
|
*/
|
||||||
|
export async function confirmQrLogin(requestData: QrLoginConfirmRequest): Promise<QrCodeStatusResponse> {
|
||||||
|
const res = await request.post<ApiResult<QrCodeStatusResponse>>(
|
||||||
|
SERVER_API_URL + '/qr-login/confirm',
|
||||||
|
requestData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(res.data.message || '确认登录失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码标记(移动端扫码时调用)
|
||||||
|
*/
|
||||||
|
export async function scanQrCode(token: string): Promise<boolean> {
|
||||||
|
const res = await request.post<ApiResult<boolean>>(
|
||||||
|
SERVER_API_URL + `/qr-login/scan/${token}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data || true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(res.data.message || '扫码失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序扫码登录确认
|
||||||
|
*/
|
||||||
|
export async function wechatMiniProgramConfirm(requestData: QrLoginConfirmRequest): Promise<QrCodeStatusResponse> {
|
||||||
|
const res = await request.post<ApiResult<QrCodeStatusResponse>>(
|
||||||
|
SERVER_API_URL + '/qr-login/wechat-confirm',
|
||||||
|
requestData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(res.data.message || '微信小程序登录确认失败'));
|
||||||
|
}
|
||||||
258
src/components/QrLogin/demo.vue
Normal file
258
src/components/QrLogin/demo.vue
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<div class="qr-login-demo">
|
||||||
|
<a-card title="二维码登录演示" :bordered="false">
|
||||||
|
<div class="demo-content">
|
||||||
|
<!-- 二维码登录组件 -->
|
||||||
|
<QrLogin
|
||||||
|
@loginSuccess="handleLoginSuccess"
|
||||||
|
@loginError="handleLoginError"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 演示控制面板 -->
|
||||||
|
<div class="demo-controls">
|
||||||
|
<h4>演示控制</h4>
|
||||||
|
<a-space direction="vertical" style="width: 100%;">
|
||||||
|
<a-button @click="simulateScanned" type="primary" ghost>
|
||||||
|
模拟扫码
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="simulateConfirmed" type="primary">
|
||||||
|
模拟确认登录
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="simulateExpired" type="default">
|
||||||
|
模拟过期
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="simulateError" danger>
|
||||||
|
模拟错误
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态显示 -->
|
||||||
|
<div class="demo-status">
|
||||||
|
<h4>当前状态</h4>
|
||||||
|
<a-descriptions :column="1" size="small">
|
||||||
|
<a-descriptions-item label="二维码Key">
|
||||||
|
{{ currentQrKey || '未生成' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag :color="getStatusColor(currentStatus)">
|
||||||
|
{{ getStatusText(currentStatus) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="剩余时间">
|
||||||
|
{{ remainingTime }}秒
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 日志显示 -->
|
||||||
|
<a-card title="操作日志" :bordered="false" style="margin-top: 16px;">
|
||||||
|
<div class="demo-logs">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in logs"
|
||||||
|
:key="index"
|
||||||
|
class="log-item"
|
||||||
|
:class="log.type"
|
||||||
|
>
|
||||||
|
<span class="log-time">{{ log.time }}</span>
|
||||||
|
<span class="log-message">{{ log.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import QrLogin from './index.vue';
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const currentQrKey = ref<string>('');
|
||||||
|
const currentStatus = ref<string>('loading');
|
||||||
|
const remainingTime = ref<number>(300);
|
||||||
|
const logs = ref<Array<{time: string, message: string, type: string}>>([]);
|
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
const addLog = (message: string, type: 'info' | 'success' | 'error' | 'warning' = 'info') => {
|
||||||
|
const now = new Date();
|
||||||
|
const time = now.toLocaleTimeString();
|
||||||
|
logs.value.unshift({ time, message, type });
|
||||||
|
|
||||||
|
// 限制日志数量
|
||||||
|
if (logs.value.length > 50) {
|
||||||
|
logs.value = logs.value.slice(0, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors = {
|
||||||
|
loading: 'blue',
|
||||||
|
active: 'green',
|
||||||
|
scanned: 'orange',
|
||||||
|
expired: 'red',
|
||||||
|
error: 'red'
|
||||||
|
};
|
||||||
|
return colors[status] || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const texts = {
|
||||||
|
loading: '正在生成',
|
||||||
|
active: '等待扫码',
|
||||||
|
scanned: '已扫码',
|
||||||
|
expired: '已过期',
|
||||||
|
error: '生成失败'
|
||||||
|
};
|
||||||
|
return texts[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理登录成功
|
||||||
|
const handleLoginSuccess = (token: string) => {
|
||||||
|
addLog(`登录成功,获得token: ${token.substring(0, 20)}...`, 'success');
|
||||||
|
message.success('二维码登录成功!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理登录错误
|
||||||
|
const handleLoginError = (error: string) => {
|
||||||
|
addLog(`登录失败: ${error}`, 'error');
|
||||||
|
message.error(`登录失败: ${error}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟扫码
|
||||||
|
const simulateScanned = () => {
|
||||||
|
currentStatus.value = 'scanned';
|
||||||
|
addLog('模拟用户扫码', 'info');
|
||||||
|
message.info('模拟扫码成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟确认登录
|
||||||
|
const simulateConfirmed = () => {
|
||||||
|
const mockToken = 'mock_token_' + Date.now();
|
||||||
|
handleLoginSuccess(mockToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟过期
|
||||||
|
const simulateExpired = () => {
|
||||||
|
currentStatus.value = 'expired';
|
||||||
|
remainingTime.value = 0;
|
||||||
|
addLog('模拟二维码过期', 'warning');
|
||||||
|
message.warning('二维码已过期');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟错误
|
||||||
|
const simulateError = () => {
|
||||||
|
currentStatus.value = 'error';
|
||||||
|
addLog('模拟生成二维码失败', 'error');
|
||||||
|
handleLoginError('网络连接失败');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载
|
||||||
|
onMounted(() => {
|
||||||
|
addLog('二维码登录演示组件已加载', 'info');
|
||||||
|
|
||||||
|
// 模拟生成二维码
|
||||||
|
setTimeout(() => {
|
||||||
|
currentQrKey.value = 'demo_qr_' + Date.now();
|
||||||
|
currentStatus.value = 'active';
|
||||||
|
addLog(`生成二维码成功,Key: ${currentQrKey.value}`, 'success');
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (remainingTime.value > 0) {
|
||||||
|
remainingTime.value--;
|
||||||
|
} else {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (currentStatus.value === 'active') {
|
||||||
|
simulateExpired();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.qr-login-demo {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 200px 200px;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-controls {
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-status {
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-logs {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #999;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info .log-message {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success .log-message {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error .log-message {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning .log-message {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.demo-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
310
src/components/QrLogin/index.vue
Normal file
310
src/components/QrLogin/index.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<div class="qr-login-container">
|
||||||
|
<div class="qr-code-wrapper">
|
||||||
|
<!-- 二维码显示区域 -->
|
||||||
|
<div v-if="qrCodeStatus === 'loading'" class="qr-loading">
|
||||||
|
<a-spin size="large" />
|
||||||
|
<p class="loading-text">正在生成二维码...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="qrCodeStatus === 'active'" class="qr-active">
|
||||||
|
<ele-qr-code-svg :value="qrCodeData" :size="200" />
|
||||||
|
<p class="qr-tip">请使用手机APP或小程序扫码登录</p>
|
||||||
|
<p class="qr-expire-tip">二维码有效期:{{ formatTime(expireTime) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="qrCodeStatus === 'scanned'" class="qr-scanned">
|
||||||
|
<div class="scanned-icon">
|
||||||
|
<CheckCircleOutlined style="font-size: 48px; color: #52c41a;" />
|
||||||
|
</div>
|
||||||
|
<p class="scanned-text">扫码成功,请在手机上确认登录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="qrCodeStatus === 'expired'" class="qr-expired">
|
||||||
|
<div class="expired-icon">
|
||||||
|
<ClockCircleOutlined style="font-size: 48px; color: #ff4d4f;" />
|
||||||
|
</div>
|
||||||
|
<p class="expired-text">二维码已过期</p>
|
||||||
|
<a-button type="primary" @click="refreshQrCode">刷新二维码</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="qrCodeStatus === 'error'" class="qr-error">
|
||||||
|
<div class="error-icon">
|
||||||
|
<ExclamationCircleOutlined style="font-size: 48px; color: #ff4d4f;" />
|
||||||
|
</div>
|
||||||
|
<p class="error-text">{{ errorMessage || '生成二维码失败' }}</p>
|
||||||
|
<a-button type="primary" @click="refreshQrCode">重新生成</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
|
<div class="qr-actions" v-if="qrCodeStatus === 'active'">
|
||||||
|
<a-button type="link" @click="refreshQrCode" :loading="refreshing">
|
||||||
|
<ReloadOutlined /> 刷新二维码
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
ReloadOutlined
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import { generateQrCode, checkQrCodeStatus, type QrCodeResponse } from '@/api/passport/qrLogin';
|
||||||
|
|
||||||
|
// 定义组件事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'loginSuccess', token: string): void;
|
||||||
|
(e: 'loginError', error: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const qrCodeData = ref<string>('');
|
||||||
|
const qrCodeToken = ref<string>('');
|
||||||
|
const qrCodeStatus = ref<'loading' | 'active' | 'scanned' | 'expired' | 'error'>('loading');
|
||||||
|
const expireTime = ref<number>(0);
|
||||||
|
const errorMessage = ref<string>('');
|
||||||
|
const refreshing = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 定时器
|
||||||
|
let statusCheckTimer: number | null = null;
|
||||||
|
let expireTimer: number | null = null;
|
||||||
|
|
||||||
|
// 计算属性:格式化剩余时间
|
||||||
|
const formatTime = computed(() => {
|
||||||
|
return (seconds: number) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成二维码
|
||||||
|
*/
|
||||||
|
const generateQrCodeData = async () => {
|
||||||
|
try {
|
||||||
|
qrCodeStatus.value = 'loading';
|
||||||
|
const response: QrCodeResponse = await generateQrCode();
|
||||||
|
|
||||||
|
// 后端返回的qrCode是二维码内容,我们需要构造完整的URL
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const qrCodeUrl = `${baseUrl}/qr-confirm?qrCodeKey=${response.token}`;
|
||||||
|
|
||||||
|
qrCodeData.value = qrCodeUrl;
|
||||||
|
qrCodeToken.value = response.token;
|
||||||
|
qrCodeStatus.value = 'active';
|
||||||
|
expireTime.value = response.expiresIn || 300; // 默认5分钟过期
|
||||||
|
|
||||||
|
// 开始检查二维码状态
|
||||||
|
startStatusCheck();
|
||||||
|
// 开始倒计时
|
||||||
|
startExpireCountdown();
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
qrCodeStatus.value = 'error';
|
||||||
|
errorMessage.value = error.message || '生成二维码失败';
|
||||||
|
message.error(errorMessage.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始检查二维码状态
|
||||||
|
*/
|
||||||
|
const startStatusCheck = () => {
|
||||||
|
if (statusCheckTimer) {
|
||||||
|
clearInterval(statusCheckTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCheckTimer = window.setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const status = await checkQrCodeStatus(qrCodeToken.value);
|
||||||
|
|
||||||
|
switch (status.status) {
|
||||||
|
case 'scanned':
|
||||||
|
qrCodeStatus.value = 'scanned';
|
||||||
|
break;
|
||||||
|
case 'confirmed':
|
||||||
|
// 登录成功
|
||||||
|
qrCodeStatus.value = 'active';
|
||||||
|
stopAllTimers();
|
||||||
|
emit('loginSuccess', status.accessToken || '');
|
||||||
|
message.success('登录成功');
|
||||||
|
break;
|
||||||
|
case 'expired':
|
||||||
|
qrCodeStatus.value = 'expired';
|
||||||
|
stopAllTimers();
|
||||||
|
break;
|
||||||
|
case 'pending':
|
||||||
|
// 继续等待
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新剩余时间
|
||||||
|
if (status.expiresIn !== undefined) {
|
||||||
|
expireTime.value = status.expiresIn;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('检查二维码状态失败:', error);
|
||||||
|
// 继续检查,不中断流程
|
||||||
|
}
|
||||||
|
}, 2000); // 每2秒检查一次
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始过期倒计时
|
||||||
|
*/
|
||||||
|
const startExpireCountdown = () => {
|
||||||
|
if (expireTimer) {
|
||||||
|
clearInterval(expireTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
expireTimer = window.setInterval(() => {
|
||||||
|
if (expireTime.value <= 0) {
|
||||||
|
qrCodeStatus.value = 'expired';
|
||||||
|
stopAllTimers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expireTime.value--;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止所有定时器
|
||||||
|
*/
|
||||||
|
const stopAllTimers = () => {
|
||||||
|
if (statusCheckTimer) {
|
||||||
|
clearInterval(statusCheckTimer);
|
||||||
|
statusCheckTimer = null;
|
||||||
|
}
|
||||||
|
if (expireTimer) {
|
||||||
|
clearInterval(expireTimer);
|
||||||
|
expireTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新二维码
|
||||||
|
*/
|
||||||
|
const refreshQrCode = async () => {
|
||||||
|
refreshing.value = true;
|
||||||
|
stopAllTimers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await generateQrCodeData();
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时生成二维码
|
||||||
|
onMounted(() => {
|
||||||
|
generateQrCodeData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAllTimers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.qr-login-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 250px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.qr-tip {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-expire-tip {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-scanned {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.scanned-text {
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-expired {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.expired-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,6 +35,21 @@ export const routes = [
|
|||||||
component: () => import('@/views/passport/dealer/register.vue'),
|
component: () => import('@/views/passport/dealer/register.vue'),
|
||||||
meta: { title: '邀请注册' }
|
meta: { title: '邀请注册' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/qr-confirm',
|
||||||
|
component: () => import('@/views/passport/qrConfirm/index.vue'),
|
||||||
|
meta: { title: '扫码登录确认' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/qr-demo',
|
||||||
|
component: () => import('@/components/QrLogin/demo.vue'),
|
||||||
|
meta: { title: '二维码登录演示' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/qr-test',
|
||||||
|
component: () => import('@/views/test/qrLoginTest.vue'),
|
||||||
|
meta: { title: '二维码登录接口测试' }
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/forget',
|
// path: '/forget',
|
||||||
// component: () => import('@/views/passport/forget/index.vue'),
|
// component: () => import('@/views/passport/forget/index.vue'),
|
||||||
|
|||||||
@@ -182,7 +182,10 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="loginType == 'scan'">
|
<template v-if="loginType == 'scan'">
|
||||||
二维码
|
<QrLogin
|
||||||
|
@loginSuccess="onQrLoginSuccess"
|
||||||
|
@loginError="onQrLoginError"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="login-copyright">
|
<div class="login-copyright">
|
||||||
@@ -267,6 +270,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import {goHomeRoute, cleanPageTabs} from '@/utils/page-tab-util';
|
import {goHomeRoute, cleanPageTabs} from '@/utils/page-tab-util';
|
||||||
import {login, loginBySms, getCaptcha} from '@/api/passport/login';
|
import {login, loginBySms, getCaptcha} from '@/api/passport/login';
|
||||||
|
import QrLogin from '@/components/QrLogin/index.vue';
|
||||||
|
|
||||||
import {User} from '@/api/system/user/model';
|
import {User} from '@/api/system/user/model';
|
||||||
import {TEMPLATE_ID, THEME_STORE_NAME} from '@/config/setting';
|
import {TEMPLATE_ID, THEME_STORE_NAME} from '@/config/setting';
|
||||||
@@ -521,6 +525,20 @@ const onScan = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 二维码登录成功处理 */
|
||||||
|
const onQrLoginSuccess = (token: string) => {
|
||||||
|
// 设置token到localStorage或其他存储
|
||||||
|
localStorage.setItem('access_token', token);
|
||||||
|
message.success('扫码登录成功');
|
||||||
|
cleanPageTabs();
|
||||||
|
goHome();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 二维码登录错误处理 */
|
||||||
|
const onQrLoginError = (error: string) => {
|
||||||
|
message.error(error || '扫码登录失败');
|
||||||
|
};
|
||||||
|
|
||||||
// const goBack = () => {
|
// const goBack = () => {
|
||||||
// openUrl(getDomain());
|
// openUrl(getDomain());
|
||||||
// return;
|
// return;
|
||||||
|
|||||||
371
src/views/passport/qrConfirm/index.vue
Normal file
371
src/views/passport/qrConfirm/index.vue
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<template>
|
||||||
|
<div class="qr-confirm-container">
|
||||||
|
<a-card :bordered="false" class="confirm-card">
|
||||||
|
<!-- 头部信息 -->
|
||||||
|
<div class="header-info">
|
||||||
|
<div class="app-info">
|
||||||
|
<img :src="appLogo" alt="应用图标" class="app-logo" />
|
||||||
|
<h3 class="app-name">{{ appName }}</h3>
|
||||||
|
</div>
|
||||||
|
<p class="confirm-tip">确认登录到Web端管理后台?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<div class="user-info" v-if="userInfo">
|
||||||
|
<a-avatar :size="64" :src="userInfo.avatar">
|
||||||
|
<template v-if="!userInfo.avatar" #icon>
|
||||||
|
<UserOutlined />
|
||||||
|
</template>
|
||||||
|
</a-avatar>
|
||||||
|
<div class="user-details">
|
||||||
|
<h4 class="username">{{ userInfo.nickname || userInfo.username }}</h4>
|
||||||
|
<p class="user-phone">{{ userInfo.phone || userInfo.mobile }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设备信息 -->
|
||||||
|
<div class="device-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">登录设备:</span>
|
||||||
|
<span class="value">{{ deviceInfo.browser }} {{ deviceInfo.version }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">操作系统:</span>
|
||||||
|
<span class="value">{{ deviceInfo.os }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">IP地址:</span>
|
||||||
|
<span class="value">{{ deviceInfo.ip }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">登录时间:</span>
|
||||||
|
<span class="value">{{ formatTime(new Date()) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a-button
|
||||||
|
size="large"
|
||||||
|
@click="handleCancel"
|
||||||
|
:loading="cancelLoading"
|
||||||
|
class="cancel-btn"
|
||||||
|
>
|
||||||
|
取消登录
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="handleConfirm"
|
||||||
|
:loading="confirmLoading"
|
||||||
|
class="confirm-btn"
|
||||||
|
>
|
||||||
|
确认登录
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安全提示 -->
|
||||||
|
<div class="security-tip">
|
||||||
|
<ExclamationCircleOutlined style="color: #faad14; margin-right: 8px;" />
|
||||||
|
<span>请确认是您本人操作,如非本人操作请点击"取消登录"</span>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { UserOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { confirmQrLogin, scanQrCode, type QrLoginConfirmRequest } from '@/api/passport/qrLogin';
|
||||||
|
import { getUserInfo } from '@/api/system/user';
|
||||||
|
import { getToken } from '@/utils/token-util';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const qrCodeKey = ref<string>('');
|
||||||
|
const userInfo = ref<any>(null);
|
||||||
|
const confirmLoading = ref<boolean>(false);
|
||||||
|
const cancelLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 应用信息
|
||||||
|
const appName = ref<string>('唐九运售电云');
|
||||||
|
const appLogo = ref<string>('/logo.png');
|
||||||
|
|
||||||
|
// 设备信息(这里可以从后端获取或前端检测)
|
||||||
|
const deviceInfo = ref({
|
||||||
|
browser: 'Chrome',
|
||||||
|
version: '120.0',
|
||||||
|
os: 'Windows 10',
|
||||||
|
ip: '192.168.1.100'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间
|
||||||
|
*/
|
||||||
|
const formatTime = (date: Date): string => {
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
message.error('请先登录');
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await getUserInfo();
|
||||||
|
userInfo.value = info;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
message.error('获取用户信息失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记二维码已扫描
|
||||||
|
*/
|
||||||
|
const markScanned = async () => {
|
||||||
|
try {
|
||||||
|
if (!qrCodeKey.value) return;
|
||||||
|
|
||||||
|
await scanQrCode(qrCodeKey.value);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('标记扫描失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认登录
|
||||||
|
*/
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!qrCodeKey.value) {
|
||||||
|
message.error('二维码参数错误');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmLoading.value = true;
|
||||||
|
try {
|
||||||
|
if (!userInfo.value || !userInfo.value.userId) {
|
||||||
|
message.error('用户信息获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData: QrLoginConfirmRequest = {
|
||||||
|
token: qrCodeKey.value,
|
||||||
|
userId: userInfo.value.userId,
|
||||||
|
platform: 'web'
|
||||||
|
};
|
||||||
|
|
||||||
|
await confirmQrLogin(requestData);
|
||||||
|
message.success('登录确认成功');
|
||||||
|
|
||||||
|
// 可以跳转到成功页面或关闭页面
|
||||||
|
setTimeout(() => {
|
||||||
|
// 如果是在APP内的webview中,可以调用原生方法关闭页面
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.go(-1);
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '确认登录失败');
|
||||||
|
} finally {
|
||||||
|
confirmLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消登录
|
||||||
|
*/
|
||||||
|
const handleCancel = async () => {
|
||||||
|
cancelLoading.value = true;
|
||||||
|
try {
|
||||||
|
// 直接返回,不调用后端接口
|
||||||
|
message.info('已取消登录');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.go(-1);
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '取消登录失败');
|
||||||
|
} finally {
|
||||||
|
cancelLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
// 从URL参数获取二维码key
|
||||||
|
qrCodeKey.value = route.query.qrCodeKey as string;
|
||||||
|
|
||||||
|
if (!qrCodeKey.value) {
|
||||||
|
message.error('二维码参数错误');
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
await fetchUserInfo();
|
||||||
|
|
||||||
|
// 标记二维码已扫描
|
||||||
|
await markScanned();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.qr-confirm-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.app-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.app-logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-tip {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
margin-left: 16px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-phone {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.cancel-btn,
|
||||||
|
.confirm-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fffbe6;
|
||||||
|
border: 1px solid #ffe58f;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
323
src/views/test/qrLoginTest.vue
Normal file
323
src/views/test/qrLoginTest.vue
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<template>
|
||||||
|
<div class="qr-login-test">
|
||||||
|
<a-card title="二维码登录接口测试" :bordered="false">
|
||||||
|
<a-space direction="vertical" style="width: 100%;" size="large">
|
||||||
|
|
||||||
|
<!-- 生成二维码测试 -->
|
||||||
|
<a-card size="small" title="1. 生成二维码">
|
||||||
|
<a-button type="primary" @click="testGenerateQrCode" :loading="generateLoading">
|
||||||
|
生成二维码
|
||||||
|
</a-button>
|
||||||
|
<div v-if="qrCodeData" style="margin-top: 16px;">
|
||||||
|
<a-descriptions :column="1" size="small">
|
||||||
|
<a-descriptions-item label="Token">{{ qrCodeData.token }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="二维码内容">{{ qrCodeData.qrCode }}</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="过期时间">{{ qrCodeData.expiresIn }}秒</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<ele-qr-code-svg :value="qrCodeUrl" :size="200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 检查状态测试 -->
|
||||||
|
<a-card size="small" title="2. 检查二维码状态">
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="testCheckStatus" :loading="checkLoading" :disabled="!qrCodeData">
|
||||||
|
检查状态
|
||||||
|
</a-button>
|
||||||
|
<a-switch
|
||||||
|
v-model:checked="autoCheck"
|
||||||
|
checked-children="自动检查"
|
||||||
|
un-checked-children="手动检查"
|
||||||
|
@change="onAutoCheckChange"
|
||||||
|
/>
|
||||||
|
</a-space>
|
||||||
|
<div v-if="statusData" style="margin-top: 16px;">
|
||||||
|
<a-descriptions :column="1" size="small">
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag :color="getStatusColor(statusData.status)">
|
||||||
|
{{ getStatusText(statusData.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="剩余时间" v-if="statusData.expiresIn">
|
||||||
|
{{ statusData.expiresIn }}秒
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="访问令牌" v-if="statusData.accessToken">
|
||||||
|
{{ statusData.accessToken.substring(0, 50) }}...
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 扫码标记测试 -->
|
||||||
|
<a-card size="small" title="3. 扫码标记">
|
||||||
|
<a-button @click="testScanQrCode" :loading="scanLoading" :disabled="!qrCodeData">
|
||||||
|
模拟扫码
|
||||||
|
</a-button>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 确认登录测试 -->
|
||||||
|
<a-card size="small" title="4. 确认登录">
|
||||||
|
<a-space>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="testUserId"
|
||||||
|
placeholder="用户ID"
|
||||||
|
:min="1"
|
||||||
|
style="width: 120px;"
|
||||||
|
/>
|
||||||
|
<a-button @click="testConfirmLogin" :loading="confirmLoading" :disabled="!qrCodeData">
|
||||||
|
确认登录
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 操作日志 -->
|
||||||
|
<a-card size="small" title="操作日志">
|
||||||
|
<div class="log-container">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in logs"
|
||||||
|
:key="index"
|
||||||
|
class="log-item"
|
||||||
|
:class="log.type"
|
||||||
|
>
|
||||||
|
<span class="log-time">{{ log.time }}</span>
|
||||||
|
<span class="log-message">{{ log.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
</a-space>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, onUnmounted } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
generateQrCode,
|
||||||
|
checkQrCodeStatus,
|
||||||
|
scanQrCode,
|
||||||
|
confirmQrLogin,
|
||||||
|
type QrCodeResponse,
|
||||||
|
type QrCodeStatusResponse,
|
||||||
|
type QrLoginConfirmRequest
|
||||||
|
} from '@/api/passport/qrLogin';
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const generateLoading = ref(false);
|
||||||
|
const checkLoading = ref(false);
|
||||||
|
const scanLoading = ref(false);
|
||||||
|
const confirmLoading = ref(false);
|
||||||
|
const autoCheck = ref(false);
|
||||||
|
const testUserId = ref<number>(1);
|
||||||
|
|
||||||
|
const qrCodeData = ref<QrCodeResponse | null>(null);
|
||||||
|
const statusData = ref<QrCodeStatusResponse | null>(null);
|
||||||
|
const logs = ref<Array<{time: string, message: string, type: string}>>([]);
|
||||||
|
|
||||||
|
// 自动检查定时器
|
||||||
|
let autoCheckTimer: number | null = null;
|
||||||
|
|
||||||
|
// 计算二维码URL
|
||||||
|
const qrCodeUrl = computed(() => {
|
||||||
|
if (!qrCodeData.value) return '';
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
return `${baseUrl}/qr-confirm?qrCodeKey=${qrCodeData.value.token}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
const addLog = (message: string, type: 'info' | 'success' | 'error' | 'warning' = 'info') => {
|
||||||
|
const now = new Date();
|
||||||
|
const time = now.toLocaleTimeString();
|
||||||
|
logs.value.unshift({ time, message, type });
|
||||||
|
|
||||||
|
if (logs.value.length > 100) {
|
||||||
|
logs.value = logs.value.slice(0, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'blue',
|
||||||
|
scanned: 'orange',
|
||||||
|
confirmed: 'green',
|
||||||
|
expired: 'red'
|
||||||
|
};
|
||||||
|
return colors[status] || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const texts = {
|
||||||
|
pending: '等待扫码',
|
||||||
|
scanned: '已扫码',
|
||||||
|
confirmed: '已确认',
|
||||||
|
expired: '已过期'
|
||||||
|
};
|
||||||
|
return texts[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试生成二维码
|
||||||
|
const testGenerateQrCode = async () => {
|
||||||
|
generateLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await generateQrCode();
|
||||||
|
qrCodeData.value = response;
|
||||||
|
statusData.value = null;
|
||||||
|
addLog(`生成二维码成功: ${response.token}`, 'success');
|
||||||
|
message.success('生成二维码成功');
|
||||||
|
} catch (error: any) {
|
||||||
|
addLog(`生成二维码失败: ${error.message}`, 'error');
|
||||||
|
message.error(error.message);
|
||||||
|
} finally {
|
||||||
|
generateLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试检查状态
|
||||||
|
const testCheckStatus = async () => {
|
||||||
|
if (!qrCodeData.value) return;
|
||||||
|
|
||||||
|
checkLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await checkQrCodeStatus(qrCodeData.value.token);
|
||||||
|
statusData.value = response;
|
||||||
|
addLog(`检查状态成功: ${response.status}`, 'info');
|
||||||
|
} catch (error: any) {
|
||||||
|
addLog(`检查状态失败: ${error.message}`, 'error');
|
||||||
|
message.error(error.message);
|
||||||
|
} finally {
|
||||||
|
checkLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试扫码标记
|
||||||
|
const testScanQrCode = async () => {
|
||||||
|
if (!qrCodeData.value) return;
|
||||||
|
|
||||||
|
scanLoading.value = true;
|
||||||
|
try {
|
||||||
|
await scanQrCode(qrCodeData.value.token);
|
||||||
|
addLog('扫码标记成功', 'success');
|
||||||
|
message.success('扫码标记成功');
|
||||||
|
|
||||||
|
// 自动检查状态
|
||||||
|
setTimeout(() => {
|
||||||
|
testCheckStatus();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error: any) {
|
||||||
|
addLog(`扫码标记失败: ${error.message}`, 'error');
|
||||||
|
message.error(error.message);
|
||||||
|
} finally {
|
||||||
|
scanLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试确认登录
|
||||||
|
const testConfirmLogin = async () => {
|
||||||
|
if (!qrCodeData.value || !testUserId.value) return;
|
||||||
|
|
||||||
|
confirmLoading.value = true;
|
||||||
|
try {
|
||||||
|
const requestData: QrLoginConfirmRequest = {
|
||||||
|
token: qrCodeData.value.token,
|
||||||
|
userId: testUserId.value,
|
||||||
|
platform: 'web'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await confirmQrLogin(requestData);
|
||||||
|
statusData.value = response;
|
||||||
|
addLog(`确认登录成功: ${response.status}`, 'success');
|
||||||
|
message.success('确认登录成功');
|
||||||
|
} catch (error: any) {
|
||||||
|
addLog(`确认登录失败: ${error.message}`, 'error');
|
||||||
|
message.error(error.message);
|
||||||
|
} finally {
|
||||||
|
confirmLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动检查状态变化
|
||||||
|
const onAutoCheckChange = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
autoCheckTimer = window.setInterval(() => {
|
||||||
|
if (qrCodeData.value) {
|
||||||
|
testCheckStatus();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
addLog('开启自动检查状态', 'info');
|
||||||
|
} else {
|
||||||
|
if (autoCheckTimer) {
|
||||||
|
clearInterval(autoCheckTimer);
|
||||||
|
autoCheckTimer = null;
|
||||||
|
}
|
||||||
|
addLog('关闭自动检查状态', 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (autoCheckTimer) {
|
||||||
|
clearInterval(autoCheckTimer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
addLog('二维码登录接口测试页面已加载', 'info');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.qr-login-test {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #999;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info .log-message {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success .log-message {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error .log-message {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning .log-message {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user