|
@@ -5,7 +5,8 @@
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
|
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
|
|
-import { Modal, Button, Alert, Spin, Typography } from 'antd';
|
|
|
|
|
|
|
+import { Modal, Button, Alert, Spin, Typography, Upload, message } from 'antd';
|
|
|
|
|
+import { UploadOutlined } from '@ant-design/icons';
|
|
|
import { Html5Qrcode } from 'html5-qrcode';
|
|
import { Html5Qrcode } from 'html5-qrcode';
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
|
import { FormInstance } from 'antd';
|
|
import { FormInstance } from 'antd';
|
|
@@ -32,6 +33,7 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
const dispatch = useDispatch();
|
|
const dispatch = useDispatch();
|
|
|
const { isScanning, isProcessing, error } = useSelector((state: RootState) => state.qrCodeScan);
|
|
const { isScanning, isProcessing, error } = useSelector((state: RootState) => state.qrCodeScan);
|
|
|
const [scannedText, setScannedText] = useState<string>('');
|
|
const [scannedText, setScannedText] = useState<string>('');
|
|
|
|
|
+ const [uploadMode, setUploadMode] = useState<boolean>(false);
|
|
|
const scannerRef = useRef<Html5Qrcode | null>(null);
|
|
const scannerRef = useRef<Html5Qrcode | null>(null);
|
|
|
const readerRef = useRef<HTMLDivElement>(null);
|
|
const readerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
@@ -52,7 +54,12 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
try {
|
|
try {
|
|
|
await qr.start(
|
|
await qr.start(
|
|
|
{ facingMode: 'environment' },
|
|
{ facingMode: 'environment' },
|
|
|
- { fps: 10, qrbox: 250 },
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ fps: 20, // 提高帧率,增加扫描频率
|
|
|
|
|
+ qrbox: { width: 300, height: 300 }, // 更大的扫描框
|
|
|
|
|
+ aspectRatio: 1.0, // 正方形扫描框
|
|
|
|
|
+ disableFlip: false, // 允许镜像扫描,提高识别率
|
|
|
|
|
+ },
|
|
|
onScanSuccess,
|
|
onScanSuccess,
|
|
|
onScanError
|
|
onScanError
|
|
|
);
|
|
);
|
|
@@ -150,7 +157,12 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
scannerRef.current = qr;
|
|
scannerRef.current = qr;
|
|
|
await scannerRef.current.start(
|
|
await scannerRef.current.start(
|
|
|
{ facingMode: 'environment' },
|
|
{ facingMode: 'environment' },
|
|
|
- { fps: 10, qrbox: 250 },
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ fps: 20,
|
|
|
|
|
+ qrbox: { width: 300, height: 300 },
|
|
|
|
|
+ aspectRatio: 1.0,
|
|
|
|
|
+ disableFlip: false,
|
|
|
|
|
+ },
|
|
|
onScanSuccess,
|
|
onScanSuccess,
|
|
|
onScanError
|
|
onScanError
|
|
|
);
|
|
);
|
|
@@ -164,15 +176,93 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
const handleCancel = () => {
|
|
|
|
|
+ setUploadMode(false);
|
|
|
onCancel();
|
|
onCancel();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 📤 处理图片上传扫描
|
|
|
|
|
+ const handleFileUpload = async (file: File) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ dispatch(startProcessing());
|
|
|
|
|
+
|
|
|
|
|
+ // 创建临时的 Html5Qrcode 实例用于文件扫描
|
|
|
|
|
+ const tempScanner = new Html5Qrcode(qrReaderId);
|
|
|
|
|
+
|
|
|
|
|
+ const decodedText = await tempScanner.scanFile(file, false);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[QRCodeScan] 图片扫描成功:', decodedText);
|
|
|
|
|
+ setScannedText(decodedText);
|
|
|
|
|
+ setUploadMode(true);
|
|
|
|
|
+
|
|
|
|
|
+ // 停止摄像头扫描
|
|
|
|
|
+ if (scannerRef.current && scannerRef.current.isScanning) {
|
|
|
|
|
+ await scannerRef.current.stop().catch(() => {});
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dispatch(stopProcessing());
|
|
|
|
|
+ message.success('图片扫描成功');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('[QRCodeScan] 图片扫描失败:', error);
|
|
|
|
|
+ const errorMsg = error instanceof Error ? error.message : '图片扫描失败,请确保图片包含有效的二维码';
|
|
|
|
|
+ dispatch(setError(errorMsg));
|
|
|
|
|
+ dispatch(stopProcessing());
|
|
|
|
|
+ message.error(errorMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false; // 阻止自动上传
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 切换到上传模式
|
|
|
|
|
+ const handleSwitchToUpload = () => {
|
|
|
|
|
+ setUploadMode(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 切换回摄像头模式
|
|
|
|
|
+ const handleSwitchToCamera = async () => {
|
|
|
|
|
+ setUploadMode(false);
|
|
|
|
|
+ setScannedText('');
|
|
|
|
|
+ dispatch(clearError());
|
|
|
|
|
+
|
|
|
|
|
+ // 重新启动摄像头扫描
|
|
|
|
|
+ if (scannerRef.current) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (scannerRef.current.isScanning) {
|
|
|
|
|
+ await scannerRef.current.stop();
|
|
|
|
|
+ await scannerRef.current.clear();
|
|
|
|
|
+ }
|
|
|
|
|
+ const qr = new Html5Qrcode(qrReaderId);
|
|
|
|
|
+ scannerRef.current = qr;
|
|
|
|
|
+ await qr.start(
|
|
|
|
|
+ { facingMode: 'environment' },
|
|
|
|
|
+ {
|
|
|
|
|
+ fps: 20,
|
|
|
|
|
+ qrbox: { width: 300, height: 300 },
|
|
|
|
|
+ aspectRatio: 1.0,
|
|
|
|
|
+ disableFlip: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ onScanSuccess,
|
|
|
|
|
+ onScanError
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('重新启动摄像头失败:', error);
|
|
|
|
|
+ const errorMsg = error instanceof Error ? error.message : '重新启动摄像头失败';
|
|
|
|
|
+ dispatch(setError(errorMsg));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const getFooterButtons = () => {
|
|
const getFooterButtons = () => {
|
|
|
if (scannedText) {
|
|
if (scannedText) {
|
|
|
return [
|
|
return [
|
|
|
- <Button key="rescan" onClick={handleRescan}>
|
|
|
|
|
- 重新扫描
|
|
|
|
|
- </Button>,
|
|
|
|
|
|
|
+ uploadMode ? (
|
|
|
|
|
+ <Button key="camera" onClick={handleSwitchToCamera}>
|
|
|
|
|
+ 切换摄像头
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Button key="rescan" onClick={handleRescan}>
|
|
|
|
|
+ 重新扫描
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ ),
|
|
|
<Button key="confirm" type="primary" onClick={handleConfirmScan} loading={isProcessing}>
|
|
<Button key="confirm" type="primary" onClick={handleConfirmScan} loading={isProcessing}>
|
|
|
确认使用
|
|
确认使用
|
|
|
</Button>,
|
|
</Button>,
|
|
@@ -182,7 +272,12 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
<Button key="cancel" onClick={handleCancel}>
|
|
<Button key="cancel" onClick={handleCancel}>
|
|
|
取消
|
|
取消
|
|
|
</Button>,
|
|
</Button>,
|
|
|
- ];
|
|
|
|
|
|
|
+ !uploadMode && (
|
|
|
|
|
+ <Button key="upload" icon={<UploadOutlined />} onClick={handleSwitchToUpload}>
|
|
|
|
|
+ 上传图片
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ ),
|
|
|
|
|
+ ].filter(Boolean);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -215,29 +310,63 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{/* 扫描区域 */}
|
|
{/* 扫描区域 */}
|
|
|
-
|
|
|
|
|
- <>
|
|
|
|
|
- <div
|
|
|
|
|
- id={qrReaderId}
|
|
|
|
|
- ref={readerRef}
|
|
|
|
|
- style={{
|
|
|
|
|
- width: '100%',
|
|
|
|
|
- maxWidth: 500,
|
|
|
|
|
- margin: '0 auto',
|
|
|
|
|
- border: '2px solid #1890ff',
|
|
|
|
|
- borderRadius: 8,
|
|
|
|
|
- padding: 0,
|
|
|
|
|
- minHeight: 300, // 给扫描区域固定高度
|
|
|
|
|
- display: scannedText ? 'none' : 'block',
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- <div style={{ marginTop: 16, color: '#666' }}>
|
|
|
|
|
- <p>请将二维码对准扫描框</p>
|
|
|
|
|
- <p style={{ fontSize: 12 }}>
|
|
|
|
|
- 提示:确保光线充足,二维码清晰可见
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </>
|
|
|
|
|
|
|
+ {!uploadMode ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div
|
|
|
|
|
+ id={qrReaderId}
|
|
|
|
|
+ ref={readerRef}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ maxWidth: 500,
|
|
|
|
|
+ margin: '0 auto',
|
|
|
|
|
+ border: '2px solid #1890ff',
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ padding: 0,
|
|
|
|
|
+ minHeight: 300,
|
|
|
|
|
+ display: scannedText ? 'none' : 'block',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ {!scannedText && (
|
|
|
|
|
+ <div style={{ marginTop: 16, color: '#666' }}>
|
|
|
|
|
+ <p>请将二维码对准扫描框</p>
|
|
|
|
|
+ <p style={{ fontSize: 12 }}>
|
|
|
|
|
+ 提示:确保光线充足,二维码清晰可见
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p style={{ fontSize: 12, marginTop: 8 }}>
|
|
|
|
|
+ 如果摄像头扫描困难,可以点击下方"上传图片"按钮
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ !scannedText && (
|
|
|
|
|
+ <div style={{ padding: '40px 0', textAlign: 'center' }}>
|
|
|
|
|
+ <Upload
|
|
|
|
|
+ accept="image/*"
|
|
|
|
|
+ beforeUpload={handleFileUpload}
|
|
|
|
|
+ showUploadList={false}
|
|
|
|
|
+ maxCount={1}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button icon={<UploadOutlined />} size="large" type="primary">
|
|
|
|
|
+ 选择二维码图片
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Upload>
|
|
|
|
|
+ <div style={{ marginTop: 16, color: '#666' }}>
|
|
|
|
|
+ <p>支持 JPG、PNG 等常见图片格式</p>
|
|
|
|
|
+ <p style={{ fontSize: 12 }}>
|
|
|
|
|
+ 请确保图片清晰,二维码完整可见
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="link"
|
|
|
|
|
+ onClick={handleSwitchToCamera}
|
|
|
|
|
+ style={{ marginTop: 16 }}
|
|
|
|
|
+ >
|
|
|
|
|
+ 返回摄像头扫描
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 扫描结果显示 */}
|
|
{/* 扫描结果显示 */}
|