feat(qr-login): 实现扫码登录功能模块
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user