diff --git a/README_QR_LOGIN.md b/README_QR_LOGIN.md new file mode 100644 index 0000000..f1f4ba1 --- /dev/null +++ b/README_QR_LOGIN.md @@ -0,0 +1,209 @@ +# 微信小程序扫码登录功能实现 + +## 项目概述 + +本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。 + +## 🎯 功能特性 + +- ✅ **完整的后端API** - Java Spring Boot实现 +- ✅ **多种前端集成方式** - 按钮、弹窗、页面 +- ✅ **智能二维码解析** - 支持URL、JSON、纯token格式 +- ✅ **安全可靠** - Token有效期控制,防重复使用 +- ✅ **用户体验优秀** - 实时状态反馈,错误处理完善 +- ✅ **微信深度集成** - 自动获取用户信息 + +## 📁 项目结构 + +### 后端 (Java) +``` +auto/ +├── controller/QrLoginController.java # REST API控制器 +├── service/QrLoginService.java # 业务接口 +├── service/impl/QrLoginServiceImpl.java # 业务实现 +└── dto/ # 数据传输对象 + ├── QrLoginData.java + ├── QrLoginConfirmRequest.java + ├── QrLoginStatusResponse.java + └── QrLoginGenerateResponse.java +``` + +### 前端 (小程序) +``` +src/ +├── api/qr-login/index.ts # API接口层 +├── hooks/useQRLogin.ts # 业务逻辑Hook +├── components/ # 组件层 +│ ├── QRLoginButton.tsx # 扫码按钮组件 +│ ├── QRLoginScanner.tsx # 扫码器组件 +│ ├── QRScanModal.tsx # 扫码弹窗组件 +│ └── QRLoginDemo.tsx # 演示组件 +└── pages/ # 页面层 + ├── qr-login/index.tsx # 扫码登录页面 + ├── qr-confirm/index.tsx # 登录确认页面 + └── qr-test/index.tsx # 功能测试页面 +``` + +## 🚀 快速开始 + +### 1. 后端配置 + +确保Java后端服务正常运行,API接口可访问: +- `POST /api/qr-login/generate` - 生成扫码token +- `GET /api/qr-login/status/{token}` - 检查登录状态 +- `POST /api/qr-login/confirm` - 确认登录 +- `POST /api/qr-login/scan/{token}` - 扫码操作 + +### 2. 前端使用 + +#### 最简单的使用方式: +```tsx +import QRLoginButton from '@/components/QRLoginButton'; + + +``` + +#### 弹窗方式: +```tsx +import QRScanModal from '@/components/QRScanModal'; + + setShowModal(false)} + onSuccess={(result) => console.log('登录成功', result)} +/> +``` + +#### 页面跳转方式: +```tsx +import Taro from '@tarojs/taro'; + +Taro.navigateTo({ + url: '/pages/qr-login/index' +}); +``` + +## 🔧 支持的二维码格式 + +系统智能识别多种二维码格式: + +1. **URL格式**:`https://mp.websoft.top/qr-confirm?qrCodeKey=token123` +2. **JSON格式**:`{"token": "token123", "type": "qr-login"}` +3. **简单格式**:`qr-login:token123` 或直接 `token123` + +## 📱 页面说明 + +### 1. 扫码登录页面 (`/pages/qr-login/index`) +- 完整的扫码登录功能 +- 用户信息显示 +- 登录历史记录 +- 使用说明和安全提示 + +### 2. 登录确认页面 (`/pages/qr-confirm/index`) +- 处理二维码跳转确认 +- 支持URL参数:`qrCodeKey` 或 `token` +- 用户确认界面 + +### 3. 功能测试页面 (`/pages/qr-test/index`) +- 演示各种集成方式 +- 功能测试和调试 + +## 🛠️ 开发指南 + +### 1. 添加扫码按钮到现有页面 + +```tsx +import QRLoginButton from '@/components/QRLoginButton'; + +const MyPage = () => { + return ( + + { + // 处理登录成功 + console.log('用户登录成功:', result); + }} + onError={(error) => { + // 处理登录失败 + console.error('登录失败:', error); + }} + /> + + ); +}; +``` + +### 2. 自定义扫码逻辑 + +```tsx +import { useQRLogin } from '@/hooks/useQRLogin'; + +const MyComponent = () => { + const { + startScan, + isLoading, + isSuccess, + result, + error + } = useQRLogin(); + + return ( + + ); +}; +``` + +## 🔒 安全注意事项 + +1. **用户登录验证**:使用前确保用户已在小程序中登录 +2. **Token有效期**:二维码5分钟有效期,过期自动失效 +3. **权限申请**:确保小程序已申请摄像头权限 +4. **来源验证**:只扫描来自官方网站的登录二维码 + +## 🐛 常见问题 + +### Q: 提示"请先登录小程序" +A: 用户需要先在小程序中完成登录,获取用户ID和访问令牌。 + +### Q: 提示"无效的登录二维码" +A: 检查二维码格式是否正确,或者二维码是否已过期。 + +### Q: 扫码失败 +A: 检查摄像头权限,确保二维码清晰可见。 + +### Q: 网络请求失败 +A: 检查网络连接和API接口地址配置。 + +## 📚 相关文档 + +- [详细使用指南](docs/QR_LOGIN_USAGE.md) +- [API接口文档](src/api/qr-login/index.ts) +- [组件API文档](docs/QR_LOGIN_USAGE.md#组件api) + +## 🎉 测试功能 + +访问测试页面验证功能: +``` +/pages/qr-test/index +``` + +该页面包含所有集成方式的演示和测试功能。 + +## 📞 技术支持 + +如有问题,请检查: +1. 后端API服务是否正常运行 +2. 小程序用户是否已登录 +3. 网络连接是否正常 +4. 二维码格式是否正确 + +--- + +**开发者**: 科技小王子 +**更新时间**: 2025-09-20 diff --git a/docs/QR_LOGIN_USAGE.md b/docs/QR_LOGIN_USAGE.md new file mode 100644 index 0000000..81699bf --- /dev/null +++ b/docs/QR_LOGIN_USAGE.md @@ -0,0 +1,284 @@ +# 微信小程序扫码登录使用指南 + +## 概述 + +本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。 + +## 功能特性 + +- ✅ **多种集成方式** - 按钮组件、弹窗组件、专门页面 +- ✅ **自动解析二维码** - 支持多种二维码格式(URL、JSON、纯token) +- ✅ **安全可靠** - Token有效期控制,防重复使用 +- ✅ **用户体验好** - 实时状态反馈,错误处理完善 +- ✅ **微信集成** - 自动获取微信用户信息 + +## 后端接口 + +### 1. 生成扫码token +```http +POST /api/qr-login/generate +``` + +### 2. 检查登录状态 +```http +GET /api/qr-login/status/{token} +``` + +### 3. 确认登录 +```http +POST /api/qr-login/confirm +``` + +### 4. 扫码操作(可选) +```http +POST /api/qr-login/scan/{token} +``` + +## 前端使用方式 + +### 方式1: 直接扫码按钮 + +最简单的使用方式,点击按钮直接调用扫码API: + +```tsx +import QRLoginButton from '@/components/QRLoginButton'; + +// 基础使用 + + +// 自定义配置 + console.log('登录成功', result)} + onError={(error) => console.log('登录失败', error)} +/> +``` + +### 方式2: 弹窗扫码 + +在当前页面弹出扫码弹窗: + +```tsx +import { useState } from 'react'; +import QRScanModal from '@/components/QRScanModal'; + +const MyComponent = () => { + const [showScan, setShowScan] = useState(false); + + return ( + <> + + + setShowScan(false)} + onSuccess={(result) => { + console.log('登录成功', result); + setShowScan(false); + }} + onError={(error) => { + console.log('登录失败', error); + }} + /> + + ); +}; +``` + +### 方式3: 跳转到专门页面 + +跳转到专门的扫码登录页面: + +```tsx +import QRLoginButton from '@/components/QRLoginButton'; + +// 使用页面模式 + + +// 或者自定义跳转 + +``` + +### 方式4: 使用Hook + +直接使用Hook进行更灵活的控制: + +```tsx +import { useQRLogin } from '@/hooks/useQRLogin'; + +const MyComponent = () => { + const { + startScan, + isLoading, + isSuccess, + isError, + error, + result, + canScan + } = useQRLogin(); + + return ( + + ); +}; +``` + +## 二维码格式支持 + +系统支持多种二维码格式: + +### 1. URL格式 +``` +https://mp.websoft.top/qr-confirm?qrCodeKey=02278c578d3e4aad87dece6aab2f0296 +https://example.com/login?token=abc123 +``` + +### 2. JSON格式 +```json +{ + "token": "02278c578d3e4aad87dece6aab2f0296", + "type": "qr-login" +} +``` + +### 3. 简单格式 +``` +qr-login:02278c578d3e4aad87dece6aab2f0296 +02278c578d3e4aad87dece6aab2f0296 +``` + +## 页面配置 + +确保在 `app.config.ts` 中添加了相关页面: + +```typescript +export default { + pages: [ + // ... 其他页面 + 'pages/qr-login/index', // 扫码登录页面 + 'pages/qr-confirm/index', // 登录确认页面 + ], + // ... +} +``` + +## 使用示例 + +### 完整示例组件 + +```tsx +import React, { useState } from 'react'; +import { View } from '@tarojs/components'; +import { Button } from '@nutui/nutui-react-taro'; +import QRLoginButton from '@/components/QRLoginButton'; +import QRScanModal from '@/components/QRScanModal'; + +const LoginPage = () => { + const [showModal, setShowModal] = useState(false); + + const handleSuccess = (result) => { + console.log('登录成功:', result); + // 处理登录成功逻辑 + }; + + const handleError = (error) => { + console.error('登录失败:', error); + // 处理登录失败逻辑 + }; + + return ( + + {/* 方式1: 直接扫码 */} + + + {/* 方式2: 弹窗扫码 */} + + + {/* 方式3: 跳转页面 */} + + + setShowModal(false)} + onSuccess={handleSuccess} + onError={handleError} + /> + + ); +}; +``` + +## 注意事项 + +1. **用户登录状态**: 使用扫码登录功能前,用户必须已在小程序中登录 +2. **权限申请**: 确保小程序已申请摄像头权限 +3. **网络环境**: 确保网络连接正常,API接口可访问 +4. **二维码有效期**: 二维码有5分钟有效期,过期需重新生成 +5. **安全性**: 只扫描来自官方网站的登录二维码 + +## 错误处理 + +常见错误及解决方案: + +- `请先登录小程序`: 用户未登录,需要先完成小程序登录 +- `无效的登录二维码`: 二维码格式不正确或已过期 +- `登录确认失败`: 网络问题或服务器错误,可重试 +- `扫码失败`: 摄像头权限问题或二维码不清晰 + +## API接口详情 + +详细的API接口文档请参考:`src/api/qr-login/index.ts` + +## 组件API + +### QRLoginButton Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| type | string | 'primary' | 按钮类型 | +| size | string | 'normal' | 按钮大小 | +| text | string | '扫码登录' | 按钮文本 | +| showIcon | boolean | true | 是否显示图标 | +| usePageMode | boolean | false | 是否使用页面模式 | +| onSuccess | function | - | 成功回调 | +| onError | function | - | 失败回调 | + +### QRScanModal Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| visible | boolean | false | 是否显示弹窗 | +| title | string | '扫描登录二维码' | 弹窗标题 | +| description | string | '扫描网页端显示的登录二维码' | 描述文本 | +| autoConfirm | boolean | true | 是否自动确认登录 | +| onClose | function | - | 关闭回调 | +| onSuccess | function | - | 成功回调 | +| onError | function | - | 失败回调 | diff --git a/src/api/qr-login/index.ts b/src/api/qr-login/index.ts index ada3bf8..291ce16 100644 --- a/src/api/qr-login/index.ts +++ b/src/api/qr-login/index.ts @@ -1,6 +1,8 @@ import request from '@/utils/request'; import type { ApiResult } from '@/api'; import Taro from '@tarojs/taro'; +import {SERVER_API_URL} from "@/utils/server"; +import {getUserInfo} from "@/api/layout"; /** * 扫码登录相关接口 @@ -70,7 +72,7 @@ export interface ConfirmLoginParam { unionid?: string; nickname?: string; avatar?: string; - gender?: number; + gender?: string; }; // 设备信息 deviceInfo?: string; @@ -96,7 +98,7 @@ export interface ConfirmLoginResult { */ export async function generateQRToken(data?: GenerateQRTokenParam) { const res = await request.post>( - 'http://127.0.0.1:9200/api/qr-login/generate', + SERVER_API_URL + '/qr-login/generate', { clientType: 'wechat', expireMinutes: 5, @@ -114,7 +116,7 @@ export async function generateQRToken(data?: GenerateQRTokenParam) { */ export async function checkQRLoginStatus(token: string) { const res = await request.get>( - `http://127.0.0.1:9200/api/qr-login/status/${token}` + SERVER_API_URL + `/qr-login/status/${token}` ); if (res.code === 0 && res.data) { return res.data; @@ -127,7 +129,7 @@ export async function checkQRLoginStatus(token: string) { */ export async function confirmQRLogin(data: ConfirmLoginParam) { const res = await request.post>( - 'http://127.0.0.1:9200/api/qr-login/confirm', + SERVER_API_URL + '/qr-login/confirm', data ); if (res.code === 0 && res.data) { @@ -142,25 +144,26 @@ export async function confirmQRLogin(data: ConfirmLoginParam) { export async function confirmWechatQRLogin(token: string, userId: number) { try { // 获取微信用户信息 - const userInfo = await getUserProfile(); + const userInfo = await getUserInfo(); + console.log('获取微信用户信息3:', userInfo) const data: ConfirmLoginParam = { token, userId, platform: 'wechat', wechatInfo: { - nickname: userInfo?.nickName, - avatar: userInfo?.avatarUrl, - gender: userInfo?.gender + nickname: userInfo?.nickname, + avatar: userInfo?.avatar, + gender: userInfo?.sex }, deviceInfo: await getDeviceInfo() }; const res = await request.post>( - 'http://127.0.0.1:9200/api/qr-login/wechat-confirm', + SERVER_API_URL + '/qr-login/confirm', data ); - + console.log(res,'ConfirmLoginParamResult>') if (res.code === 0 && res.data) { return res.data; } @@ -227,20 +230,44 @@ async function getDeviceInfo() { */ export function parseQRContent(qrContent: string): string | null { try { + console.log('解析二维码内容1:', qrContent); + // 尝试解析JSON格式 if (qrContent.startsWith('{')) { const parsed = JSON.parse(qrContent); - return parsed.token || null; + return parsed.token || parsed.qrCodeKey || null; } // 尝试解析URL格式 - if (qrContent.includes('token=')) { + if (qrContent.includes('http')) { const url = new URL(qrContent); - return url.searchParams.get('token'); + // 支持多种参数名 + return url.searchParams.get('token') || + url.searchParams.get('qrCodeKey') || + url.searchParams.get('qr_code_key') || + null; } - // 直接返回内容作为token - return qrContent; + // 尝试解析简单的key=value格式 + if (qrContent.includes('=')) { + const params = new URLSearchParams(qrContent); + return params.get('token') || + params.get('qrCodeKey') || + params.get('qr_code_key') || + null; + } + + // 如果是以qr-login:开头的格式 + if (qrContent.startsWith('qr-login:')) { + return qrContent.replace('qr-login:', ''); + } + + // 直接返回内容作为token(如果是32位以上的字符串) + if (qrContent.length >= 32) { + return qrContent; + } + + return null; } catch (error) { console.error('解析二维码内容失败:', error); return null; diff --git a/src/app.config.ts b/src/app.config.ts index c243822..1aa5fec 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -5,6 +5,8 @@ export default { 'pages/find/find', 'pages/user/user', 'pages/qr-login/index', + 'pages/qr-confirm/index', + 'pages/qr-test/index', 'pages/cms/category/index' ], "subpackages": [ diff --git a/src/components/QRLoginButton.tsx b/src/components/QRLoginButton.tsx index 43bab52..6a155e3 100644 --- a/src/components/QRLoginButton.tsx +++ b/src/components/QRLoginButton.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Button } from '@nutui/nutui-react-taro'; +import {View} from '@tarojs/components' import { Scan } from '@nutui/icons-react-taro'; import Taro from '@tarojs/taro'; import { useQRLogin } from '@/hooks/useQRLogin'; @@ -31,7 +32,6 @@ const QRLoginButton: React.FC = ({ size = 'normal', text = '扫码登录', showIcon = true, - className = '', onSuccess, onError, usePageMode = false @@ -40,6 +40,7 @@ const QRLoginButton: React.FC = ({ // 处理点击事件 const handleClick = async () => { + console.log('处理点击事件handleClick', usePageMode) if (usePageMode) { // 跳转到专门的扫码登录页面 if (canScan()) { @@ -74,12 +75,13 @@ const QRLoginButton: React.FC = ({ loading={isLoading} disabled={disabled} onClick={handleClick} - className={className} > - {showIcon && !isLoading && ( - - )} - {isLoading ? '扫码中...' : (disabled && !canScan() ? '请先登录' : text)} + + {showIcon && !isLoading && ( + + )} + {isLoading ? '扫码中...' : (disabled && !canScan() ? '请先登录' : text)} + ); }; diff --git a/src/components/QRLoginDemo.tsx b/src/components/QRLoginDemo.tsx new file mode 100644 index 0000000..1be26e7 --- /dev/null +++ b/src/components/QRLoginDemo.tsx @@ -0,0 +1,184 @@ +import React, { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Button, Card } from '@nutui/nutui-react-taro'; +import { Scan, User } from '@nutui/icons-react-taro'; +import Taro from '@tarojs/taro'; +import QRLoginButton from './QRLoginButton'; +import QRScanModal from './QRScanModal'; +import { useUser } from '@/hooks/useUser'; + +/** + * 扫码登录功能演示组件 + * 展示如何在页面中集成扫码登录功能 + */ +const QRLoginDemo: React.FC = () => { + const { user, getDisplayName } = useUser(); + const [showScanModal, setShowScanModal] = useState(false); + const [loginHistory, setLoginHistory] = useState([]); + + // 处理扫码成功 + const handleScanSuccess = (result: any) => { + console.log('扫码登录成功:', result); + + // 添加到历史记录 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + success: true, + userInfo: result.userInfo + }; + setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); + }; + + // 处理扫码失败 + const handleScanError = (error: string) => { + console.error('扫码登录失败:', error); + + // 添加到历史记录 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + success: false, + error + }; + setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); + }; + + // 跳转到专门的扫码页面 + const goToQRLoginPage = () => { + Taro.navigateTo({ + url: '/pages/qr-login/index' + }); + }; + + return ( + + {/* 用户信息 */} + + + + + + + + + {getDisplayName()} + + + 当前登录用户 + + + + + + + {/* 扫码登录方式 */} + + + 扫码登录方式 + + + {/* 方式1: 直接扫码按钮 */} + + + 方式1: 直接扫码登录 + + + + + {/* 方式2: 弹窗扫码 */} + + + 方式2: 弹窗扫码 + + + + + {/* 方式3: 跳转到专门页面 */} + + + 方式3: 专门页面 + + + + + {/* 方式4: 自定义按钮 */} + + + 方式4: 自定义跳转 + + + + + + + + {/* 登录历史 */} + {loginHistory.length > 0 && ( + + + 最近登录记录 + + + {loginHistory.map((record) => ( + + + + + {record.success ? '登录成功' : '登录失败'} + + {record.error && ( + + {record.error} + + )} + + + {record.time} + + + + ))} + + + + )} + + {/* 扫码弹窗 */} + setShowScanModal(false)} + onSuccess={handleScanSuccess} + onError={handleScanError} + /> + + ); +}; + +export default QRLoginDemo; diff --git a/src/components/QRScanModal.tsx b/src/components/QRScanModal.tsx new file mode 100644 index 0000000..c3f5511 --- /dev/null +++ b/src/components/QRScanModal.tsx @@ -0,0 +1,272 @@ +import React, { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Button, Popup, Loading } from '@nutui/nutui-react-taro'; +import { Scan, Close, Success, Failure } from '@nutui/icons-react-taro'; +import Taro from '@tarojs/taro'; +import { parseQRContent, confirmQRLogin } from '@/api/qr-login'; +import { useUser } from '@/hooks/useUser'; + +export interface QRScanModalProps { + /** 是否显示弹窗 */ + visible: boolean; + /** 关闭弹窗回调 */ + onClose: () => void; + /** 扫码成功回调 */ + onSuccess?: (result: any) => void; + /** 扫码失败回调 */ + onError?: (error: string) => void; + /** 弹窗标题 */ + title?: string; + /** 描述文本 */ + description?: string; + /** 是否自动确认登录 */ + autoConfirm?: boolean; +} + +/** + * 二维码扫描弹窗组件(用于扫码登录) + */ +const QRScanModal: React.FC = ({ + visible, + onClose, + onSuccess, + onError, + title = '扫描登录二维码', + description = '扫描网页端显示的登录二维码', + autoConfirm = true +}) => { + const { user } = useUser(); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState<'idle' | 'scanning' | 'confirming' | 'success' | 'error'>('idle'); + const [errorMsg, setErrorMsg] = useState(''); + + // 开始扫码 + const handleScan = async () => { + if (!user?.userId) { + onError?.('请先登录小程序'); + return; + } + + try { + setLoading(true); + setStatus('scanning'); + setErrorMsg(''); + + // 扫码 + const scanResult = await new Promise((resolve, reject) => { + Taro.scanCode({ + onlyFromCamera: true, + scanType: ['qrCode'], + success: (res) => { + if (res.result) { + resolve(res.result); + } else { + reject(new Error('扫码结果为空')); + } + }, + fail: (err) => { + reject(new Error(err.errMsg || '扫码失败')); + } + }); + }); + + // 解析二维码内容 + const token = parseQRContent(scanResult); + if (!token) { + throw new Error('无效的登录二维码'); + } + + if (autoConfirm) { + // 自动确认登录 + setStatus('confirming'); + const result = await confirmQRLogin({ + token, + userId: user.userId, + platform: 'wechat', + wechatInfo: { + nickname: user.nickname, + avatar: user.avatar + } + }); + + if (result.success) { + setStatus('success'); + onSuccess?.(result); + + // 显示成功提示 + Taro.showToast({ + title: '登录确认成功', + icon: 'success' + }); + + // 延迟关闭 + setTimeout(() => { + onClose(); + setStatus('idle'); + }, 1500); + } else { + throw new Error(result.message || '登录确认失败'); + } + } else { + // 只返回扫码结果 + onSuccess?.(scanResult); + onClose(); + setStatus('idle'); + } + } catch (error: any) { + setStatus('error'); + const errorMessage = error.message || '操作失败'; + setErrorMsg(errorMessage); + onError?.(errorMessage); + } finally { + setLoading(false); + } + }; + + // 重试 + const handleRetry = () => { + setStatus('idle'); + setErrorMsg(''); + handleScan(); + }; + + // 关闭弹窗 + const handleClose = () => { + setStatus('idle'); + setErrorMsg(''); + setLoading(false); + onClose(); + }; + + // 获取状态显示内容 + const getStatusContent = () => { + switch (status) { + case 'scanning': + return { + icon: , + title: '正在扫码...', + description: '请将二维码对准摄像头' + }; + + case 'confirming': + return { + icon: , + title: '正在确认登录...', + description: '请稍候,正在为您确认登录' + }; + + case 'success': + return { + icon: , + title: '登录确认成功', + description: '网页端将自动完成登录' + }; + + case 'error': + return { + icon: , + title: '操作失败', + description: errorMsg || '请重试' + }; + + default: + return { + icon: , + title, + description + }; + } + }; + + const statusContent = getStatusContent(); + + return ( + + + {/* 关闭按钮 */} + {status !== 'scanning' && status !== 'confirming' && ( + + + + )} + + {/* 图标 */} + + {statusContent.icon} + + + {/* 标题 */} + + {statusContent.title} + + + {/* 描述 */} + + {statusContent.description} + + + {/* 操作按钮 */} + {status === 'idle' && ( + + )} + + {status === 'error' && ( + + + + + )} + + {(status === 'scanning' || status === 'confirming') && ( + + )} + + {loading} + + ); +}; + +export default QRScanModal; diff --git a/src/hooks/useQRLogin.ts b/src/hooks/useQRLogin.ts index b72f49c..1c6e288 100644 --- a/src/hooks/useQRLogin.ts +++ b/src/hooks/useQRLogin.ts @@ -1,9 +1,9 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import Taro from '@tarojs/taro'; -import { - confirmWechatQRLogin, +import { + confirmWechatQRLogin, parseQRContent, - type ConfirmLoginResult + type ConfirmLoginResult } from '@/api/qr-login'; /** @@ -25,7 +25,7 @@ export function useQRLogin() { const [error, setError] = useState(''); const [result, setResult] = useState(null); const [isLoading, setIsLoading] = useState(false); - + // 用于取消操作的引用 const cancelRef = useRef(false); @@ -47,7 +47,7 @@ export function useQRLogin() { try { reset(); setState(ScanLoginState.SCANNING); - + // 检查用户是否已登录 const userId = Taro.getStorageSync('UserId'); if (!userId) { @@ -79,6 +79,7 @@ export function useQRLogin() { // 解析二维码内容 const token = parseQRContent(scanResult); + console.log('解析二维码内容2:',token) if (!token) { throw new Error('无效的登录二维码'); } @@ -88,7 +89,7 @@ export function useQRLogin() { setIsLoading(true); const confirmResult = await confirmWechatQRLogin(token, parseInt(userId)); - + console.log(confirmResult,'confirmResult>>>>') if (cancelRef.current) { return; } @@ -96,7 +97,7 @@ export function useQRLogin() { if (confirmResult.success) { setState(ScanLoginState.SUCCESS); setResult(confirmResult); - + // 显示成功提示 Taro.showToast({ title: '登录确认成功', @@ -112,7 +113,7 @@ export function useQRLogin() { setState(ScanLoginState.ERROR); const errorMessage = err.message || '扫码登录失败'; setError(errorMessage); - + // 显示错误提示 Taro.showToast({ title: errorMessage, @@ -156,11 +157,11 @@ export function useQRLogin() { // 确认登录 const confirmResult = await confirmWechatQRLogin(token, parseInt(userId)); - + if (confirmResult.success) { setState(ScanLoginState.SUCCESS); setResult(confirmResult); - + // 显示成功提示 Taro.showToast({ title: '登录确认成功', @@ -175,7 +176,7 @@ export function useQRLogin() { setState(ScanLoginState.ERROR); const errorMessage = err.message || '登录确认失败'; setError(errorMessage); - + // 显示错误提示 Taro.showToast({ title: errorMessage, @@ -209,14 +210,14 @@ export function useQRLogin() { error, result, isLoading, - + // 方法 startScan, cancel, reset, handleScanResult, canScan, - + // 便捷状态判断 isIdle: state === ScanLoginState.IDLE, isScanning: state === ScanLoginState.SCANNING, diff --git a/src/pages/qr-confirm/index.config.ts b/src/pages/qr-confirm/index.config.ts new file mode 100644 index 0000000..9a9855a --- /dev/null +++ b/src/pages/qr-confirm/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '确认登录', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/pages/qr-confirm/index.tsx b/src/pages/qr-confirm/index.tsx new file mode 100644 index 0000000..afe765b --- /dev/null +++ b/src/pages/qr-confirm/index.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Button, Loading, Card } from '@nutui/nutui-react-taro'; +import { Success, Failure, Tips, User } from '@nutui/icons-react-taro'; +import Taro, { useRouter } from '@tarojs/taro'; +import { confirmQRLogin } from '@/api/qr-login'; +import { useUser } from '@/hooks/useUser'; + +/** + * 扫码登录确认页面 + * 用于处理从二维码跳转过来的登录确认 + */ +const QRConfirmPage: React.FC = () => { + const router = useRouter(); + const { user, getDisplayName } = useUser(); + const [loading, setLoading] = useState(false); + const [confirmed, setConfirmed] = useState(false); + const [error, setError] = useState(''); + const [token, setToken] = useState(''); + + useEffect(() => { + // 从URL参数中获取token + const { qrCodeKey, token: urlToken } = router.params; + const loginToken = qrCodeKey || urlToken; + + if (loginToken) { + setToken(loginToken); + } else { + setError('无效的登录链接'); + } + }, [router.params]); + + // 确认登录 + const handleConfirmLogin = async () => { + if (!token) { + setError('缺少登录token'); + return; + } + + if (!user?.userId) { + setError('请先登录小程序'); + return; + } + + try { + setLoading(true); + setError(''); + + const result = await confirmQRLogin({ + token, + userId: user.userId, + platform: 'wechat', + wechatInfo: { + nickname: user.nickname, + avatar: user.avatar + } + }); + + if (result.success) { + setConfirmed(true); + Taro.showToast({ + title: '登录确认成功', + icon: 'success', + duration: 2000 + }); + + // 3秒后自动返回 + setTimeout(() => { + Taro.navigateBack(); + }, 3000); + } else { + setError(result.message || '登录确认失败'); + } + } catch (err: any) { + setError(err.message || '登录确认失败'); + } finally { + setLoading(false); + } + }; + + // 取消登录 + const handleCancel = () => { + Taro.navigateBack(); + }; + + // 重试 + const handleRetry = () => { + setError(''); + setConfirmed(false); + handleConfirmLogin(); + }; + + return ( + + + {/* 主要内容卡片 */} + + + {/* 图标 */} + + {loading ? ( + + + + ) : confirmed ? ( + + + + ) : error ? ( + + + + ) : ( + + + + )} + + + {/* 标题 */} + + {loading ? '正在确认登录...' : + confirmed ? '登录确认成功' : + error ? '登录确认失败' : '确认登录'} + + + {/* 描述 */} + + {loading ? '请稍候,正在为您确认登录' : + confirmed ? '您已成功确认登录,网页端将自动登录' : + error ? error : + `确认使用 ${getDisplayName()} 登录网页端?`} + + + {/* 用户信息 */} + {!loading && !confirmed && !error && user && ( + + + + + + + + {user.nickname || user.username || '用户'} + + + ID: {user.userId} + + + + + )} + + {/* 操作按钮 */} + + {loading ? ( + + ) : confirmed ? ( + + ) : error ? ( + + + + + ) : ( + + + + + )} + + + + + {/* 安全提示 */} + + + + + + + 安全提示 + + + 请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。 + + + + + + + + ); +}; + +export default QRConfirmPage; diff --git a/src/pages/qr-test/index.config.ts b/src/pages/qr-test/index.config.ts new file mode 100644 index 0000000..3e9b6bf --- /dev/null +++ b/src/pages/qr-test/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '扫码登录测试', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/pages/qr-test/index.tsx b/src/pages/qr-test/index.tsx new file mode 100644 index 0000000..3781f60 --- /dev/null +++ b/src/pages/qr-test/index.tsx @@ -0,0 +1,33 @@ +import { View, Text } from '@tarojs/components'; +import { Card } from '@nutui/nutui-react-taro'; +import QRLoginDemo from '@/components/QRLoginDemo'; +import QRLoginButton from "@/components/QRLoginButton"; + +/** + * 扫码登录测试页面 + */ +const QRTestPage = () => { + return ( + + + + {/* 页面标题 */} + + + + 扫码登录功能测试 + + + 测试各种扫码登录集成方式 + + + + + {/* 演示组件 */} + + + + ); +}; + +export default QRTestPage; diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx index 99fb63a..a021741 100644 --- a/src/pages/user/components/UserCard.tsx +++ b/src/pages/user/components/UserCard.tsx @@ -11,6 +11,7 @@ import {TenantId} from "@/config/app"; import {useUser} from "@/hooks/useUser"; import {useUserData} from "@/hooks/useUserData"; import {getStoredInviteParams} from "@/utils/invite"; +import QRLoginButton from "@/components/QRLoginButton"; const UserCard = forwardRef((_, ref) => { const { @@ -204,6 +205,9 @@ const UserCard = forwardRef((_, ref) => { ) : ''} + {userInfo?.userId === 33738 && + + } {isAdmin() && navTo('/user/store/verification', true)}/>} diff --git a/src/shop/search/components/GoodsItem.tsx b/src/shop/search/components/GoodsItem.tsx index e7e22e7..ae7d29b 100644 --- a/src/shop/search/components/GoodsItem.tsx +++ b/src/shop/search/components/GoodsItem.tsx @@ -18,7 +18,7 @@ const GoodsItem = ({ goods }: GoodsItemProps) => { } return ( -
+ { height="180" onClick={goToDetail} /> -
-
-
{goods.name || goods.goodsName}
-
+ + + {goods.name || goods.goodsName} + {goods.comments || ''} 已售 {goods.sales || 0} -
-
-
+ + + {goods.price || '0.00'} -
-
-
+ + + -
-
+ 购买 -
-
-
-
-
-
+ + + + + + ) } diff --git a/src/user/address/add.tsx b/src/user/address/add.tsx index 014f765..9ec01f5 100644 --- a/src/user/address/add.tsx +++ b/src/user/address/add.tsx @@ -365,7 +365,15 @@ const AddUserAddress = () => { /> {/* 底部浮动按钮 */} - submitSucceed} /> + { + // 触发表单提交 + if (formRef.current) { + formRef.current.submit(); + } + }} + /> ); };