feat(passport): 实现统一扫码功能并迁移二维码登录页面

将原有的扫码登录和扫码核销功能合并为统一扫码功能,支持智能识别二维码类型,
自动执行相应操作。同时将二维码登录相关页面迁移到 /passport 路径下,优化用户体验与代码结构。

主要变更:
- 新增统一扫码 Hook (useUnifiedQRScan) 和按钮组件 (UnifiedQRButton)- 新增统一扫码页面 /passport/unified-qr/index
- 迁移二维码登录页面路径:/pages/qr-login → /passport/qr-login
- 更新管理员面板及用户卡片中的扫码入口为统一扫码- 移除旧的 QRLoginDemo 和 qr-test 测试页面- 补充统一扫码使用指南文档```
This commit is contained in:
2025-09-22 15:44:44 +08:00
parent 09af5c158b
commit e47e34825a
20 changed files with 1036 additions and 302 deletions

View File

@@ -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?.();
}
},

View File

@@ -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({

View File

@@ -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;

View 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;