forked from gxwebsoft/mp-10550
feat(qr-login): 实现扫码登录功能模块
This commit is contained in:
@@ -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<ApiResult<GenerateQRTokenResult>>(
|
||||
'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<ApiResult<QRLoginStatusResult>>(
|
||||
`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<ApiResult<ConfirmLoginResult>>(
|
||||
'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<ApiResult<ConfirmLoginResult>>(
|
||||
'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;
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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<QRLoginButtonProps> = ({
|
||||
size = 'normal',
|
||||
text = '扫码登录',
|
||||
showIcon = true,
|
||||
className = '',
|
||||
onSuccess,
|
||||
onError,
|
||||
usePageMode = false
|
||||
@@ -40,6 +40,7 @@ const QRLoginButton: React.FC<QRLoginButtonProps> = ({
|
||||
|
||||
// 处理点击事件
|
||||
const handleClick = async () => {
|
||||
console.log('处理点击事件handleClick', usePageMode)
|
||||
if (usePageMode) {
|
||||
// 跳转到专门的扫码登录页面
|
||||
if (canScan()) {
|
||||
@@ -74,12 +75,13 @@ const QRLoginButton: React.FC<QRLoginButtonProps> = ({
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
>
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{isLoading ? '扫码中...' : (disabled && !canScan() ? '请先登录' : text)}
|
||||
<View className="flex items-center justify-center">
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{isLoading ? '扫码中...' : (disabled && !canScan() ? '请先登录' : text)}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
184
src/components/QRLoginDemo.tsx
Normal file
184
src/components/QRLoginDemo.tsx
Normal file
@@ -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<any[]>([]);
|
||||
|
||||
// 处理扫码成功
|
||||
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 (
|
||||
<View className="qr-login-demo p-4">
|
||||
{/* 用户信息 */}
|
||||
<Card className="mb-4">
|
||||
<View className="p-4">
|
||||
<View className="flex items-center mb-4">
|
||||
<View className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
||||
<User className="text-blue-500" size="24" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-lg font-bold text-gray-800 block">
|
||||
{getDisplayName()}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500 block">
|
||||
当前登录用户
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 扫码登录方式 */}
|
||||
<Card className="mb-4">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4 block">扫码登录方式</Text>
|
||||
|
||||
<View className="space-y-3">
|
||||
{/* 方式1: 直接扫码按钮 */}
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600 mb-2 block">
|
||||
方式1: 直接扫码登录
|
||||
</Text>
|
||||
<QRLoginButton
|
||||
text="立即扫码登录"
|
||||
onSuccess={handleScanSuccess}
|
||||
onError={handleScanError}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 方式2: 弹窗扫码 */}
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600 mb-2 block">
|
||||
方式2: 弹窗扫码
|
||||
</Text>
|
||||
<Button
|
||||
type="success"
|
||||
size="normal"
|
||||
onClick={() => setShowScanModal(true)}
|
||||
>
|
||||
<Scan className="mr-2" />
|
||||
弹窗扫码
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 方式3: 跳转到专门页面 */}
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600 mb-2 block">
|
||||
方式3: 专门页面
|
||||
</Text>
|
||||
<QRLoginButton
|
||||
text="进入扫码页面"
|
||||
type="warning"
|
||||
usePageMode={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 方式4: 自定义按钮 */}
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600 mb-2 block">
|
||||
方式4: 自定义跳转
|
||||
</Text>
|
||||
<Button
|
||||
type="default"
|
||||
size="normal"
|
||||
onClick={goToQRLoginPage}
|
||||
>
|
||||
自定义跳转
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 登录历史 */}
|
||||
{loginHistory.length > 0 && (
|
||||
<Card>
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4 block">最近登录记录</Text>
|
||||
|
||||
<View className="space-y-2">
|
||||
{loginHistory.map((record) => (
|
||||
<View
|
||||
key={record.id}
|
||||
className="p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className={`text-sm font-medium ${
|
||||
record.success ? 'text-green-600' : 'text-red-600'
|
||||
} block`}>
|
||||
{record.success ? '登录成功' : '登录失败'}
|
||||
</Text>
|
||||
{record.error && (
|
||||
<Text className="text-xs text-red-500 block">
|
||||
{record.error}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{record.time}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 扫码弹窗 */}
|
||||
<QRScanModal
|
||||
visible={showScanModal}
|
||||
onClose={() => setShowScanModal(false)}
|
||||
onSuccess={handleScanSuccess}
|
||||
onError={handleScanError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRLoginDemo;
|
||||
272
src/components/QRScanModal.tsx
Normal file
272
src/components/QRScanModal.tsx
Normal file
@@ -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<QRScanModalProps> = ({
|
||||
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<string>((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: <Loading className="text-blue-500" />,
|
||||
title: '正在扫码...',
|
||||
description: '请将二维码对准摄像头'
|
||||
};
|
||||
|
||||
case 'confirming':
|
||||
return {
|
||||
icon: <Loading className="text-orange-500" />,
|
||||
title: '正在确认登录...',
|
||||
description: '请稍候,正在为您确认登录'
|
||||
};
|
||||
|
||||
case 'success':
|
||||
return {
|
||||
icon: <Success size="32" className="text-green-500" />,
|
||||
title: '登录确认成功',
|
||||
description: '网页端将自动完成登录'
|
||||
};
|
||||
|
||||
case 'error':
|
||||
return {
|
||||
icon: <Failure size="32" className="text-red-500" />,
|
||||
title: '操作失败',
|
||||
description: errorMsg || '请重试'
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
icon: <Scan size="32" className="text-blue-500" />,
|
||||
title,
|
||||
description
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const statusContent = getStatusContent();
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable={false}
|
||||
onClose={handleClose}
|
||||
style={{ width: '85%', borderRadius: '12px' }}
|
||||
>
|
||||
<View className="p-6 text-center relative">
|
||||
{/* 关闭按钮 */}
|
||||
{status !== 'scanning' && status !== 'confirming' && (
|
||||
<View className="absolute top-4 right-4">
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
onClick={handleClose}
|
||||
className="w-8 h-8 p-0"
|
||||
>
|
||||
<Close size="16" />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 图标 */}
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
{statusContent.icon}
|
||||
</View>
|
||||
|
||||
{/* 标题 */}
|
||||
<Text className="text-lg font-bold text-gray-800 mb-2 block">
|
||||
{statusContent.title}
|
||||
</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{statusContent.description}
|
||||
</Text>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{status === 'idle' && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleScan}
|
||||
className="w-full"
|
||||
disabled={!user?.userId}
|
||||
>
|
||||
<Scan className="mr-2" />
|
||||
{user?.userId ? '开始扫码' : '请先登录'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<View className="space-y-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleRetry}
|
||||
className="w-full"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="normal"
|
||||
onClick={handleClose}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(status === 'scanning' || status === 'confirming') && (
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={handleClose}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
{loading}
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRScanModal;
|
||||
@@ -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<string>('');
|
||||
const [result, setResult] = useState<ConfirmLoginResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(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,
|
||||
|
||||
5
src/pages/qr-confirm/index.config.ts
Normal file
5
src/pages/qr-confirm/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
navigationBarTitleText: '确认登录',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
}
|
||||
239
src/pages/qr-confirm/index.tsx
Normal file
239
src/pages/qr-confirm/index.tsx
Normal file
@@ -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<string>('');
|
||||
const [token, setToken] = useState<string>('');
|
||||
|
||||
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 (
|
||||
<View className="qr-confirm-page min-h-screen bg-gray-50">
|
||||
<View className="p-4">
|
||||
{/* 主要内容卡片 */}
|
||||
<Card className="bg-white rounded-lg shadow-sm">
|
||||
<View className="p-6 text-center">
|
||||
{/* 图标 */}
|
||||
<View className="mb-6">
|
||||
{loading ? (
|
||||
<View className="w-16 h-16 mx-auto flex items-center justify-center">
|
||||
<Loading className="text-blue-500" />
|
||||
</View>
|
||||
) : confirmed ? (
|
||||
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Success className="text-green-500" />
|
||||
</View>
|
||||
) : error ? (
|
||||
<View className="w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
||||
<Failure className="text-red-500" />
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-16 h-16 mx-auto bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<User size="32" className="text-blue-500" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 标题 */}
|
||||
<Text className="text-xl font-bold text-gray-800 mb-2 block">
|
||||
{loading ? '正在确认登录...' :
|
||||
confirmed ? '登录确认成功' :
|
||||
error ? '登录确认失败' : '确认登录'}
|
||||
</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{loading ? '请稍候,正在为您确认登录' :
|
||||
confirmed ? '您已成功确认登录,网页端将自动登录' :
|
||||
error ? error :
|
||||
`确认使用 ${getDisplayName()} 登录网页端?`}
|
||||
</Text>
|
||||
|
||||
{/* 用户信息 */}
|
||||
{!loading && !confirmed && !error && user && (
|
||||
<View className="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<View className="flex items-center justify-center">
|
||||
<View className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
||||
<User className="text-blue-500" size="20" />
|
||||
</View>
|
||||
<View className="text-left">
|
||||
<Text className="text-sm font-medium text-gray-800 block">
|
||||
{user.nickname || user.username || '用户'}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500 block">
|
||||
ID: {user.userId}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="space-y-3">
|
||||
{loading ? (
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
disabled
|
||||
className="w-full"
|
||||
>
|
||||
确认中...
|
||||
</Button>
|
||||
) : confirmed ? (
|
||||
<Button
|
||||
type="success"
|
||||
size="large"
|
||||
onClick={handleCancel}
|
||||
className="w-full"
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
) : error ? (
|
||||
<View className="space-y-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleRetry}
|
||||
className="w-full"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={handleCancel}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View className="space-y-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleConfirmLogin}
|
||||
className="w-full"
|
||||
disabled={!token || !user?.userId}
|
||||
>
|
||||
确认登录
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={handleCancel}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 安全提示 */}
|
||||
<Card className="bg-yellow-50 border border-yellow-200 rounded-lg mt-4">
|
||||
<View className="p-4">
|
||||
<View className="flex items-start">
|
||||
<Tips className="text-yellow-600 mr-2 mt-1" size="16" />
|
||||
<View>
|
||||
<Text className="text-sm font-medium text-yellow-800 mb-1 block">
|
||||
安全提示
|
||||
</Text>
|
||||
<Text className="text-xs text-yellow-700 block">
|
||||
请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRConfirmPage;
|
||||
5
src/pages/qr-test/index.config.ts
Normal file
5
src/pages/qr-test/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
navigationBarTitleText: '扫码登录测试',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
}
|
||||
33
src/pages/qr-test/index.tsx
Normal file
33
src/pages/qr-test/index.tsx
Normal file
@@ -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 (
|
||||
<View className="qr-test-page min-h-screen bg-gray-50">
|
||||
<QRLoginButton />
|
||||
<View className="p-4">
|
||||
{/* 页面标题 */}
|
||||
<Card className="mb-4">
|
||||
<View className="p-4 text-center">
|
||||
<Text className="text-xl font-bold text-gray-800 mb-2 block">
|
||||
扫码登录功能测试
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600 block">
|
||||
测试各种扫码登录集成方式
|
||||
</Text>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 演示组件 */}
|
||||
<QRLoginDemo />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRTestPage;
|
||||
@@ -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<any, any>((_, ref) => {
|
||||
const {
|
||||
@@ -204,6 +205,9 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
) : ''}
|
||||
</View>
|
||||
</View>
|
||||
{userInfo?.userId === 33738 &&
|
||||
<QRLoginButton />
|
||||
}
|
||||
{isAdmin() &&
|
||||
<Scan className={'text-gray-900'} size={24} onClick={() => navTo('/user/store/verification', true)}/>}
|
||||
</View>
|
||||
|
||||
@@ -18,7 +18,7 @@ const GoodsItem = ({ goods }: GoodsItemProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
||||
<View className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
||||
<Image
|
||||
src={goods.image || ''}
|
||||
mode={'aspectFit'}
|
||||
@@ -27,31 +27,31 @@ const GoodsItem = ({ goods }: GoodsItemProps) => {
|
||||
height="180"
|
||||
onClick={goToDetail}
|
||||
/>
|
||||
<div className={'flex flex-col p-2 rounded-lg'}>
|
||||
<div>
|
||||
<div className={'car-no text-sm'}>{goods.name || goods.goodsName}</div>
|
||||
<div className={'flex justify-between text-xs py-1'}>
|
||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
||||
<View>
|
||||
<View className={'car-no text-sm'}>{goods.name || goods.goodsName}</View>
|
||||
<View className={'flex justify-between text-xs py-1'}>
|
||||
<span className={'text-orange-500'}>{goods.comments || ''}</span>
|
||||
<span className={'text-gray-400'}>已售 {goods.sales || 0}</span>
|
||||
</div>
|
||||
<div className={'flex justify-between items-center py-2'}>
|
||||
<div className={'flex text-red-500 text-xl items-baseline'}>
|
||||
</View>
|
||||
<View className={'flex justify-between items-center py-2'}>
|
||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
||||
<span className={'text-xs'}>¥</span>
|
||||
<span className={'font-bold text-2xl'}>{goods.price || '0.00'}</span>
|
||||
</div>
|
||||
<div className={'buy-btn'}>
|
||||
<div className={'cart-icon'}>
|
||||
</View>
|
||||
<View className={'buy-btn'}>
|
||||
<View className={'cart-icon'}>
|
||||
<Share size={20} className={'mx-4 mt-2'}
|
||||
onClick={goToDetail}/>
|
||||
</div>
|
||||
<div className={'text-white pl-4 pr-5'}
|
||||
</View>
|
||||
<View className={'text-white pl-4 pr-5'}
|
||||
onClick={goToDetail}>购买
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -365,7 +365,15 @@ const AddUserAddress = () => {
|
||||
/>
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={isEditMode ? '更新地址' : '保存并使用'} onClick={() => submitSucceed} />
|
||||
<FixedButton
|
||||
text={isEditMode ? '更新地址' : '保存并使用'}
|
||||
onClick={() => {
|
||||
// 触发表单提交
|
||||
if (formRef.current) {
|
||||
formRef.current.submit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user