forked from gxwebsoft/mp-10550
```
feat(passport): 实现统一扫码功能并迁移二维码登录页面 将原有的扫码登录和扫码核销功能合并为统一扫码功能,支持智能识别二维码类型, 自动执行相应操作。同时将二维码登录相关页面迁移到 /passport 路径下,优化用户体验与代码结构。 主要变更: - 新增统一扫码 Hook (useUnifiedQRScan) 和按钮组件 (UnifiedQRButton)- 新增统一扫码页面 /passport/unified-qr/index - 迁移二维码登录页面路径:/pages/qr-login → /passport/qr-login - 更新管理员面板及用户卡片中的扫码入口为统一扫码- 移除旧的 QRLoginDemo 和 qr-test 测试页面- 补充统一扫码使用指南文档```
This commit is contained in:
@@ -145,7 +145,6 @@ export async function confirmWechatQRLogin(token: string, userId: number) {
|
||||
try {
|
||||
// 获取微信用户信息
|
||||
const userInfo = await getUserInfo();
|
||||
console.log('获取微信用户信息3:', userInfo)
|
||||
|
||||
const data: ConfirmLoginParam = {
|
||||
token,
|
||||
@@ -169,37 +168,10 @@ export async function confirmWechatQRLogin(token: string, userId: number) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
} catch (error: any) {
|
||||
return Promise.reject(new Error(error.message || '确认登录失败'));
|
||||
return Promise.reject(new Error(error.message || '22确认登录失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信用户信息
|
||||
*/
|
||||
async function getUserProfile() {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
Taro.getUserProfile({
|
||||
desc: '用于扫码登录身份确认',
|
||||
success: (res) => {
|
||||
resolve(res.userInfo);
|
||||
},
|
||||
fail: (err) => {
|
||||
// 如果获取失败,尝试使用已存储的用户信息
|
||||
const storedUser = Taro.getStorageSync('User');
|
||||
if (storedUser) {
|
||||
resolve({
|
||||
nickName: storedUser.nickname,
|
||||
avatarUrl: storedUser.avatar,
|
||||
gender: storedUser.gender
|
||||
});
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
*/
|
||||
|
||||
@@ -4,9 +4,6 @@ export default {
|
||||
'pages/cart/cart',
|
||||
'pages/find/find',
|
||||
'pages/user/user',
|
||||
'pages/qr-login/index',
|
||||
'pages/qr-confirm/index',
|
||||
'pages/qr-test/index',
|
||||
'pages/cms/category/index'
|
||||
],
|
||||
"subpackages": [
|
||||
@@ -18,7 +15,10 @@ export default {
|
||||
"forget",
|
||||
"setting",
|
||||
"agreement",
|
||||
"sms-login"
|
||||
"sms-login",
|
||||
'qr-login/index',
|
||||
'qr-confirm/index',
|
||||
'unified-qr/index'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,24 +26,13 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
// 管理员功能列表
|
||||
const adminFeatures = [
|
||||
{
|
||||
id: 'store-verification',
|
||||
title: '门店核销',
|
||||
description: '扫码核销用户优惠券',
|
||||
id: 'unified-qr',
|
||||
title: '统一扫码',
|
||||
description: '扫码登录和核销一体化功能',
|
||||
icon: <Scan className="text-blue-500" size="24" />,
|
||||
color: 'bg-blue-50 border-blue-200',
|
||||
onClick: () => {
|
||||
navTo('/user/store/verification', true);
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'qr-login',
|
||||
title: '扫码登录',
|
||||
description: '扫码快速登录网页端',
|
||||
icon: <Scan className="text-green-500" size="24" />,
|
||||
color: 'bg-green-50 border-green-200',
|
||||
onClick: () => {
|
||||
navTo('/pages/qr-login/index', true);
|
||||
navTo('/passport/unified-qr/index', true);
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ const QRLoginButton: React.FC<QRLoginButtonProps> = ({
|
||||
// 跳转到专门的扫码登录页面
|
||||
if (canScan()) {
|
||||
Taro.navigateTo({
|
||||
url: '/pages/qr-login/index'
|
||||
url: '/passport/qr-login/index'
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
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;
|
||||
126
src/components/UnifiedQRButton.tsx
Normal file
126
src/components/UnifiedQRButton.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
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 { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
|
||||
export interface UnifiedQRButtonProps {
|
||||
/** 按钮类型 */
|
||||
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
|
||||
/** 按钮大小 */
|
||||
size?: 'large' | 'normal' | 'small';
|
||||
/** 按钮文本 */
|
||||
text?: string;
|
||||
/** 是否显示图标 */
|
||||
showIcon?: boolean;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 扫码成功回调 */
|
||||
onSuccess?: (result: UnifiedScanResult) => void;
|
||||
/** 扫码失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 是否使用页面模式(跳转到专门页面) */
|
||||
usePageMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码按钮组件
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
*/
|
||||
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
type = 'default',
|
||||
size = 'small',
|
||||
text = '扫码',
|
||||
showIcon = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
usePageMode = false
|
||||
}) => {
|
||||
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
|
||||
console.log(result,'useUnifiedQRScan>>result')
|
||||
// 处理点击事件
|
||||
const handleClick = async () => {
|
||||
if (usePageMode) {
|
||||
// 跳转到专门的统一扫码页面
|
||||
if (canScan()) {
|
||||
Taro.navigateTo({
|
||||
url: '/passport/unified-qr/index'
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接执行扫码
|
||||
try {
|
||||
const scanResult = await startScan();
|
||||
if (scanResult) {
|
||||
onSuccess?.(scanResult);
|
||||
|
||||
// 根据扫码类型给出不同的后续提示
|
||||
if (scanResult.type === ScanType.VERIFICATION) {
|
||||
// 核销成功后可以继续扫码
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || '扫码失败');
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = !canScan() || isLoading;
|
||||
|
||||
// 根据当前状态动态显示文本
|
||||
const getButtonText = () => {
|
||||
if (isLoading) {
|
||||
switch (state) {
|
||||
case 'scanning':
|
||||
return '扫码中...';
|
||||
case 'processing':
|
||||
return '处理中...';
|
||||
default:
|
||||
return '扫码中...';
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled && !canScan()) {
|
||||
return '请先登录';
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
size={size}
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<View className="flex items-center justify-center">
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{getButtonText()}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedQRButton;
|
||||
332
src/hooks/useUnifiedQRScan.ts
Normal file
332
src/hooks/useUnifiedQRScan.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent
|
||||
} from '@/api/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 统一扫码状态
|
||||
*/
|
||||
export enum UnifiedScanState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
PROCESSING = 'processing', // 正在处理
|
||||
SUCCESS = 'success', // 处理成功
|
||||
ERROR = 'error' // 处理失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码类型
|
||||
*/
|
||||
export enum ScanType {
|
||||
LOGIN = 'login', // 登录二维码
|
||||
VERIFICATION = 'verification', // 核销二维码
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
export interface UnifiedScanResult {
|
||||
type: ScanType;
|
||||
data: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码Hook
|
||||
* 可以处理登录和核销两种类型的二维码
|
||||
*/
|
||||
export function useUnifiedQRScan() {
|
||||
const { isAdmin } = useUser();
|
||||
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<UnifiedScanResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scanType, setScanType] = useState<ScanType>(ScanType.UNKNOWN);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(UnifiedScanState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
setScanType(ScanType.UNKNOWN);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 检测二维码类型
|
||||
*/
|
||||
const detectScanType = useCallback((scanResult: string): ScanType => {
|
||||
try {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否为登录二维码
|
||||
const loginToken = parseQRContent(scanResult);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (error) {
|
||||
console.error('检测二维码类型失败:', error);
|
||||
return ScanType.UNKNOWN;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理登录二维码
|
||||
*/
|
||||
const handleLoginQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
|
||||
if (confirmResult.success) {
|
||||
return {
|
||||
type: ScanType.LOGIN,
|
||||
data: confirmResult,
|
||||
message: '登录确认成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理核销二维码
|
||||
*/
|
||||
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
if (!isAdmin()) {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
}
|
||||
|
||||
if (gift.status === 1) {
|
||||
throw new Error('此礼品码已使用');
|
||||
}
|
||||
|
||||
if (gift.status === 2) {
|
||||
throw new Error('此礼品码已失效');
|
||||
}
|
||||
|
||||
if (gift.userId === 0) {
|
||||
throw new Error('此礼品码未认领');
|
||||
}
|
||||
|
||||
// 执行核销
|
||||
await updateShopGift({
|
||||
...gift,
|
||||
status: 1,
|
||||
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
|
||||
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
/**
|
||||
* 开始扫码
|
||||
*/
|
||||
const startScan = useCallback(async (): Promise<UnifiedScanResult | null> => {
|
||||
try {
|
||||
reset();
|
||||
setState(UnifiedScanState.SCANNING);
|
||||
|
||||
// 调用扫码API
|
||||
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 || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检测二维码类型
|
||||
const type = detectScanType(scanResult);
|
||||
setScanType(type);
|
||||
|
||||
if (type === ScanType.UNKNOWN) {
|
||||
throw new Error('不支持的二维码类型');
|
||||
}
|
||||
|
||||
// 开始处理
|
||||
setState(UnifiedScanState.PROCESSING);
|
||||
setIsLoading(true);
|
||||
|
||||
let result: UnifiedScanResult;
|
||||
|
||||
switch (type) {
|
||||
case ScanType.LOGIN:
|
||||
result = await handleLoginQR(scanResult);
|
||||
break;
|
||||
case ScanType.VERIFICATION:
|
||||
result = await handleVerificationQR(scanResult);
|
||||
break;
|
||||
default:
|
||||
throw new Error('未知的扫码类型');
|
||||
}
|
||||
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(UnifiedScanState.SUCCESS);
|
||||
setResult(result);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: result.message,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(UnifiedScanState.ERROR);
|
||||
const errorMessage = err.message || '处理失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
|
||||
|
||||
/**
|
||||
* 取消扫码
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
scanType,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === UnifiedScanState.IDLE,
|
||||
isScanning: state === UnifiedScanState.SCANNING,
|
||||
isProcessing: state === UnifiedScanState.PROCESSING,
|
||||
isSuccess: state === UnifiedScanState.SUCCESS,
|
||||
isError: state === UnifiedScanState.ERROR
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
navigationBarTitleText: '扫码登录测试',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
@@ -1,8 +1,6 @@
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Avatar, Tag, Space} from '@nutui/nutui-react-taro'
|
||||
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||
import {Scan} from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
|
||||
import {User} from "@/api/system/user/model";
|
||||
@@ -11,12 +9,9 @@ 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";
|
||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||
|
||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
const {
|
||||
isAdmin
|
||||
} = useUser();
|
||||
const {data, refresh} = useUserData()
|
||||
const {getDisplayName, getRoleName} = useUser();
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
@@ -213,19 +208,25 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
marginTop: '30px',
|
||||
marginRight: '10px'
|
||||
}}>
|
||||
{/*扫码登录*/}
|
||||
<QRLoginButton/>
|
||||
{!isAdmin() &&
|
||||
<Button
|
||||
size={'small'}
|
||||
onClick={() => navTo('/user/store/verification', true)}
|
||||
>
|
||||
<View className="flex items-center justify-center">
|
||||
<Scan className="mr-1"/>
|
||||
扫码核销
|
||||
</View>
|
||||
</Button>
|
||||
}
|
||||
{/*统一扫码入口 - 支持登录和核销*/}
|
||||
<UnifiedQRButton
|
||||
text="扫码"
|
||||
size="small"
|
||||
onSuccess={(result) => {
|
||||
console.log('统一扫码成功:', result);
|
||||
// 根据扫码类型给出不同的提示
|
||||
if (result.type === 'verification') {
|
||||
// 核销成功,可以显示更多信息或跳转到详情页
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error('统一扫码失败:', error);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</View>
|
||||
<View className={'flex justify-around mt-1'}>
|
||||
|
||||
@@ -191,12 +191,12 @@ const QRConfirmPage: React.FC = () => {
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View className="space-y-2">
|
||||
<View className="mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleConfirmLogin}
|
||||
className="w-full"
|
||||
className="w-full mb-2"
|
||||
disabled={!token || !user?.userId}
|
||||
>
|
||||
确认登录
|
||||
4
src/passport/unified-qr/index.config.ts
Normal file
4
src/passport/unified-qr/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '统一扫码',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
320
src/passport/unified-qr/index.tsx
Normal file
320
src/passport/unified-qr/index.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { Card, Button, Tag } from '@nutui/nutui-react-taro';
|
||||
import { Scan, Success, Failure, Tips, ArrowLeft } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
|
||||
/**
|
||||
* 统一扫码页面
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
*/
|
||||
const UnifiedQRPage: React.FC = () => {
|
||||
const [scanHistory, setScanHistory] = useState<any[]>([]);
|
||||
const {
|
||||
startScan,
|
||||
isLoading,
|
||||
canScan,
|
||||
state,
|
||||
result,
|
||||
error,
|
||||
scanType,
|
||||
reset
|
||||
} = useUnifiedQRScan();
|
||||
|
||||
// 处理扫码成功
|
||||
const handleScanSuccess = (result: UnifiedScanResult) => {
|
||||
console.log('扫码成功:', result);
|
||||
|
||||
// 添加到扫码历史
|
||||
const newRecord = {
|
||||
id: Date.now(),
|
||||
time: new Date().toLocaleString(),
|
||||
type: result.type,
|
||||
data: result.data,
|
||||
message: result.message,
|
||||
success: true
|
||||
};
|
||||
setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录
|
||||
|
||||
// 根据类型给出不同提示
|
||||
if (result.type === ScanType.VERIFICATION) {
|
||||
// 核销成功后询问是否继续扫码
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleStartScan();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理扫码失败
|
||||
const handleScanError = (error: string) => {
|
||||
console.error('扫码失败:', error);
|
||||
|
||||
// 添加到扫码历史
|
||||
const newRecord = {
|
||||
id: Date.now(),
|
||||
time: new Date().toLocaleString(),
|
||||
error,
|
||||
success: false
|
||||
};
|
||||
setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录
|
||||
};
|
||||
|
||||
// 开始扫码
|
||||
const handleStartScan = async () => {
|
||||
try {
|
||||
const scanResult = await startScan();
|
||||
if (scanResult) {
|
||||
handleScanSuccess(scanResult);
|
||||
}
|
||||
} catch (error: any) {
|
||||
handleScanError(error.message || '扫码失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const handleGoBack = () => {
|
||||
Taro.navigateBack();
|
||||
};
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (success: boolean, type?: ScanType) => {
|
||||
console.log(type,'获取状态图标')
|
||||
if (success) {
|
||||
return <Success className="text-green-500" size="16" />;
|
||||
} else {
|
||||
return <Failure className="text-red-500" size="16" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeTag = (type: ScanType) => {
|
||||
switch (type) {
|
||||
case ScanType.LOGIN:
|
||||
return <Tag type="success">登录</Tag>;
|
||||
case ScanType.VERIFICATION:
|
||||
return <Tag type="warning">核销</Tag>;
|
||||
default:
|
||||
return <Tag type="default">未知</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="unified-qr-page min-h-screen bg-gray-50">
|
||||
{/* 页面头部 */}
|
||||
<View className="bg-white px-4 py-3 border-b border-gray-100 flex items-center">
|
||||
<ArrowLeft
|
||||
className="text-gray-600 mr-3"
|
||||
size="20"
|
||||
onClick={handleGoBack}
|
||||
/>
|
||||
<View className="flex-1">
|
||||
<Text className="text-lg font-bold">统一扫码</Text>
|
||||
<Text className="text-sm text-gray-600 block">
|
||||
支持登录和核销功能
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 主要扫码区域 */}
|
||||
<Card className="m-4">
|
||||
<View className="text-center py-6">
|
||||
{/* 状态显示 */}
|
||||
{state === 'idle' && (
|
||||
<>
|
||||
<Scan className="text-blue-500 mx-auto mb-4" size="48" />
|
||||
<Text className="text-lg font-medium text-gray-800 mb-2 block">
|
||||
智能扫码
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
自动识别登录和核销二维码
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleStartScan}
|
||||
disabled={!canScan() || isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{canScan() ? '开始扫码' : '请先登录'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state === 'scanning' && (
|
||||
<>
|
||||
<Scan className="text-blue-500 mx-auto mb-4" size="48" />
|
||||
<Text className="text-lg font-medium text-blue-600 mb-2 block">
|
||||
扫码中...
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
请对准二维码
|
||||
</Text>
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={reset}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state === 'processing' && (
|
||||
<>
|
||||
<View className="text-orange-500 mx-auto mb-4">
|
||||
<Tips size="48" />
|
||||
</View>
|
||||
<Text className="text-lg font-medium text-orange-600 mb-2 block">
|
||||
处理中...
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{scanType === ScanType.LOGIN ? '正在确认登录' :
|
||||
scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state === 'success' && result && (
|
||||
<>
|
||||
<Success className="text-green-500 mx-auto mb-4" size="48" />
|
||||
<Text className="text-lg font-medium text-green-600 mb-2 block">
|
||||
{result.message}
|
||||
</Text>
|
||||
{result.type === ScanType.VERIFICATION && result.data && (
|
||||
<View className="bg-green-50 rounded-lg p-3 mb-4">
|
||||
<Text className="text-sm text-green-800 block">
|
||||
礼品卡:{result.data.goodsName || '未知商品'}
|
||||
</Text>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
面值:¥{result.data.faceValue}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleStartScan}
|
||||
className="w-full mb-2"
|
||||
>
|
||||
继续扫码
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="normal"
|
||||
onClick={reset}
|
||||
className="w-full"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state === 'error' && (
|
||||
<>
|
||||
<Failure className="text-red-500 mx-auto mb-4" size="48" />
|
||||
<Text className="text-lg font-medium text-red-600 mb-2 block">
|
||||
操作失败
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{error || '未知错误'}
|
||||
</Text>
|
||||
<View className="mt-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleStartScan}
|
||||
className="w-full mb-2"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="normal"
|
||||
onClick={reset}
|
||||
className="w-full"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 扫码历史 */}
|
||||
{scanHistory.length > 0 && (
|
||||
<Card className="m-4">
|
||||
<View className="pb-4">
|
||||
<Text className="text-lg font-medium text-gray-800 mb-3 block">
|
||||
最近扫码记录
|
||||
</Text>
|
||||
|
||||
{scanHistory.map((record, index) => (
|
||||
<View
|
||||
key={record.id}
|
||||
className={`flex items-center justify-between p-3 bg-gray-50 rounded-lg ${index < scanHistory.length - 1 ? 'mb-2' : ''}`}
|
||||
>
|
||||
<View className="flex items-center flex-1">
|
||||
{getStatusIcon(record.success, record.type)}
|
||||
<View className="ml-3 flex-1">
|
||||
<View className="flex items-center mb-1">
|
||||
{record.type && getTypeTag(record.type)}
|
||||
<Text className="text-sm text-gray-600 ml-2">
|
||||
{record.time}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-sm text-gray-800">
|
||||
{record.success ? record.message : record.error}
|
||||
</Text>
|
||||
{record.success && record.type === ScanType.VERIFICATION && record.data && (
|
||||
<Text className="text-xs text-gray-500">
|
||||
{record.data.goodsName} - ¥{record.data.faceValue}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 功能说明 */}
|
||||
<Card className="m-4 bg-blue-50 border border-blue-200">
|
||||
<View className="p-4">
|
||||
<View className="flex items-start">
|
||||
<Tips className="text-blue-600 mr-2 mt-1" size="16" />
|
||||
<View>
|
||||
<Text className="text-sm font-medium text-blue-800 mb-1 block">
|
||||
功能说明
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block mb-1">
|
||||
• 登录二维码:自动确认网页端登录
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block mb-1">
|
||||
• 核销二维码:门店核销用户礼品卡
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block">
|
||||
• 系统会自动识别二维码类型并执行相应操作
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedQRPage;
|
||||
Reference in New Issue
Block a user