|
|
@@ -1,10 +1,11 @@
|
|
|
/**
|
|
|
- * 二维码扫描模态框
|
|
|
+ * 二维码扫描模态框 - 稳定版
|
|
|
* 使用 html5-qrcode 库实现摄像头扫描功能
|
|
|
+ * 核心原则:每次 Modal 打开 → 新建实例,关闭 → stop + clear
|
|
|
*/
|
|
|
|
|
|
-import React, { useEffect, useRef, useState } from 'react';
|
|
|
-import { Modal, Button, Alert, Spin, Typography, Space } from 'antd';
|
|
|
+import React, { useEffect, useRef, useState, useMemo } from 'react';
|
|
|
+import { Modal, Button, Alert, Spin, Typography } from 'antd';
|
|
|
import { Html5Qrcode } from 'html5-qrcode';
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
|
import { FormInstance } from 'antd';
|
|
|
@@ -30,149 +31,95 @@ interface QRCodeScanModalProps {
|
|
|
const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, form }) => {
|
|
|
const dispatch = useDispatch();
|
|
|
const { isScanning, isProcessing, error } = useSelector((state: RootState) => state.qrCodeScan);
|
|
|
- const [scanner, setScanner] = useState<Html5Qrcode | null>(null);
|
|
|
- const [scannedText, setScannedText] = useState<string>(''); // 新增:存储扫描到的文本
|
|
|
+ const [scannedText, setScannedText] = useState<string>('');
|
|
|
const scannerRef = useRef<Html5Qrcode | null>(null);
|
|
|
const readerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
- // 初始化扫描器 - 只在模态框首次打开时初始化一次
|
|
|
- useEffect(() => {
|
|
|
- let isMounted = true;
|
|
|
-
|
|
|
- if (visible && !scanner) {
|
|
|
- const initScanner = () => {
|
|
|
- // 检查组件是否还挂载着
|
|
|
- if (!isMounted) return;
|
|
|
-
|
|
|
- const element = document.getElementById('qr-reader');
|
|
|
- if (element) {
|
|
|
- // DOM 元素存在,初始化扫描器
|
|
|
- try {
|
|
|
- const qrScanner = new Html5Qrcode('qr-reader');
|
|
|
- if (isMounted) {
|
|
|
- setScanner(qrScanner);
|
|
|
- scannerRef.current = qrScanner;
|
|
|
- console.log('[QRCodeScan] 扫描器初始化成功');
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('初始化扫描器失败:', error);
|
|
|
- if (isMounted) {
|
|
|
- dispatch(setError('初始化扫描器失败,请重试'));
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- // DOM 还没准备好,延迟重试
|
|
|
- console.log('[QRCodeScan] DOM 元素未准备好,延迟重试');
|
|
|
- setTimeout(initScanner, 100);
|
|
|
- }
|
|
|
- };
|
|
|
+ // 生成唯一的扫描器ID,避免多个实例冲突
|
|
|
+ const qrReaderId = useMemo(() => `qr-reader-${Math.random().toString(36).substr(2, 9)}`, []);
|
|
|
|
|
|
- // 初始延迟 100ms 确保模态框完全渲染
|
|
|
- setTimeout(initScanner, 100);
|
|
|
- }
|
|
|
-
|
|
|
- // 清理函数
|
|
|
- return () => {
|
|
|
- isMounted = false;
|
|
|
- if (scannerRef.current && scannerRef.current.isScanning) {
|
|
|
- scannerRef.current.stop().catch(err => console.error('停止扫描器失败:', err));
|
|
|
- }
|
|
|
- setScanner(null);
|
|
|
- scannerRef.current = null;
|
|
|
- };
|
|
|
- }, [visible]); // 只依赖 visible,移除 scanner 和 dispatch 依赖
|
|
|
-
|
|
|
- // 启动扫描 - 确保扫描器已初始化
|
|
|
+ // 🎯 核心逻辑:每次 visible=true → new Html5Qrcode,关闭 → stop + clear
|
|
|
useEffect(() => {
|
|
|
- if (visible && scanner && !isScanning) {
|
|
|
- startQRScanner();
|
|
|
- }
|
|
|
- }, [visible, scanner, isScanning]);
|
|
|
+ if (!visible) return;
|
|
|
+ if (!readerRef.current) return;
|
|
|
|
|
|
- const startQRScanner = async () => {
|
|
|
- if (!scanner) return;
|
|
|
-
|
|
|
- try {
|
|
|
- dispatch(startScanning());
|
|
|
- dispatch(clearError());
|
|
|
+ // 每次打开都新建实例 - html5-qrcode 需要字符串 ID
|
|
|
+ const qr = new Html5Qrcode(qrReaderId);
|
|
|
+ scannerRef.current = qr;
|
|
|
|
|
|
- await scanner.start(
|
|
|
- { facingMode: 'environment' }, // 使用后置摄像头
|
|
|
- {
|
|
|
- fps: 10, // 每秒扫描帧数
|
|
|
- qrbox: { width: 250, height: 250 }, // 扫描框大小
|
|
|
- },
|
|
|
- onScanSuccess,
|
|
|
- onScanError
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- console.error('启动扫描失败:', err);
|
|
|
- const errorMsg = err instanceof Error ? err.message : '摄像头启动失败';
|
|
|
- dispatch(setError(errorMsg));
|
|
|
- dispatch(stopScanning());
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const stopQRScanner = async () => {
|
|
|
- if (scanner && scanner.isScanning) {
|
|
|
+ // 直接启动扫描
|
|
|
+ const startScan = async () => {
|
|
|
try {
|
|
|
- await scanner.stop();
|
|
|
- dispatch(stopScanning());
|
|
|
+ await qr.start(
|
|
|
+ { facingMode: 'environment' },
|
|
|
+ { fps: 10, qrbox: 250 },
|
|
|
+ onScanSuccess,
|
|
|
+ onScanError
|
|
|
+ );
|
|
|
} catch (err) {
|
|
|
- console.error('停止扫描失败:', err);
|
|
|
+ console.error('启动扫描失败:', err);
|
|
|
+ const errorMsg = err instanceof Error ? err.message : '摄像头启动失败';
|
|
|
+ dispatch(setError(errorMsg));
|
|
|
}
|
|
|
- }
|
|
|
- };
|
|
|
+ };
|
|
|
+
|
|
|
+ startScan();
|
|
|
+
|
|
|
+ // 清理函数:关闭时 stop + clear
|
|
|
+ return () => {
|
|
|
+ const cleanup = async () => {
|
|
|
+ try {
|
|
|
+ await qr.stop();
|
|
|
+ await qr.clear();
|
|
|
+ } catch (e) {
|
|
|
+ // 忽略清理错误
|
|
|
+ }
|
|
|
+ scannerRef.current = null;
|
|
|
+ setScannedText('');
|
|
|
+ dispatch(clearError());
|
|
|
+ };
|
|
|
+ cleanup();
|
|
|
+ };
|
|
|
+ }, [visible, qrReaderId]);
|
|
|
|
|
|
const onScanSuccess = async (decodedText: string) => {
|
|
|
console.log('[QRCodeScan] 扫描成功:', decodedText);
|
|
|
- console.log('[QRCodeScan] 文本长度:', decodedText.length);
|
|
|
|
|
|
// 停止扫描
|
|
|
- await stopQRScanner();
|
|
|
+ if (scannerRef.current) {
|
|
|
+ await scannerRef.current.stop().catch(() => { });
|
|
|
+ }
|
|
|
|
|
|
- // 显示扫描到的文本,让用户确认
|
|
|
- console.log('[QRCodeScan] 设置 scannedText:', decodedText);
|
|
|
setScannedText(decodedText);
|
|
|
};
|
|
|
|
|
|
const onScanError = (errorMessage: string) => {
|
|
|
- // 扫描错误不需要显示,只在控制台记录
|
|
|
- // console.log('[QRCodeScan] 扫描错误:', errorMessage);
|
|
|
+ // 扫描错误不显示,只记录
|
|
|
};
|
|
|
|
|
|
const handleConfirmScan = async () => {
|
|
|
if (!scannedText) return;
|
|
|
|
|
|
- // 处理数据
|
|
|
dispatch(startProcessing());
|
|
|
|
|
|
try {
|
|
|
- // 1. 处理二维码数据
|
|
|
const result = processQRCodeData(scannedText);
|
|
|
-
|
|
|
if (!result.success) {
|
|
|
dispatch(setError(result.error || '数据处理失败'));
|
|
|
dispatch(stopProcessing());
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 2. 转换为表单数据
|
|
|
const formData = transformToFormData(result.data!);
|
|
|
-
|
|
|
- // 3. 填充表单
|
|
|
form.setFieldsValue(formData);
|
|
|
dispatch(setFormData(formData));
|
|
|
|
|
|
- // 4. 关闭模态框
|
|
|
onCancel();
|
|
|
dispatch(stopProcessing());
|
|
|
|
|
|
- // 5. 如果包含体位信息,执行自动注册
|
|
|
if (hasViews(result.data!)) {
|
|
|
console.log('[QRCodeScan] 检测到体位信息,开始自动注册');
|
|
|
const registerResult = await executeAutoRegister(result.data!, formData);
|
|
|
-
|
|
|
if (!registerResult.success) {
|
|
|
console.log('[QRCodeScan] 自动注册失败,用户可手动注册');
|
|
|
}
|
|
|
@@ -185,45 +132,43 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ // 🔄 重新扫描:stop + clear + start(不重新 new)
|
|
|
const handleRescan = async () => {
|
|
|
- // 1. 清除扫描结果,显示扫描框
|
|
|
- setScannedText('');
|
|
|
- dispatch(clearError());
|
|
|
-
|
|
|
- // 2. 停止当前扫描器
|
|
|
- if (scanner && scanner.isScanning) {
|
|
|
- await stopQRScanner();
|
|
|
- }
|
|
|
+ if (!scannerRef.current) return;
|
|
|
|
|
|
- // 3. 重新创建扫描器实例(关键修复)
|
|
|
try {
|
|
|
- const element = document.getElementById('qr-reader');
|
|
|
- if (element) {
|
|
|
- const newScanner = new Html5Qrcode('qr-reader');
|
|
|
- setScanner(newScanner);
|
|
|
- scannerRef.current = newScanner;
|
|
|
-
|
|
|
- // 4. 启动新扫描器
|
|
|
- await startQRScanner();
|
|
|
- } else {
|
|
|
- dispatch(setError('扫描器初始化失败,请重试'));
|
|
|
+ setScannedText('');
|
|
|
+ dispatch(clearError());
|
|
|
+
|
|
|
+ // stop + clear + start
|
|
|
+ if (scannerRef.current.isScanning) {
|
|
|
+ await scannerRef.current.stop();
|
|
|
+ await scannerRef.current.clear();
|
|
|
}
|
|
|
+ // 2. 创建新实例
|
|
|
+ const qr = new Html5Qrcode(qrReaderId);
|
|
|
+ scannerRef.current = qr;
|
|
|
+ await scannerRef.current.start(
|
|
|
+ { facingMode: 'environment' },
|
|
|
+ { fps: 10, qrbox: 250 },
|
|
|
+ onScanSuccess,
|
|
|
+ onScanError
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log('[QRCodeScan] 重新扫描启动成功');
|
|
|
} catch (error) {
|
|
|
- console.error('重新创建扫描器失败:', error);
|
|
|
- dispatch(setError('重新启动扫描失败,请刷新页面重试'));
|
|
|
+ console.error('重新扫描失败:', error);
|
|
|
+ const errorMsg = error instanceof Error ? error.message : '重新启动扫描失败';
|
|
|
+ dispatch(setError(errorMsg));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleCancel = async () => {
|
|
|
- await stopQRScanner();
|
|
|
- setScannedText(''); // 清除扫描结果
|
|
|
+ const handleCancel = () => {
|
|
|
onCancel();
|
|
|
};
|
|
|
|
|
|
- // 根据状态决定footer按钮
|
|
|
const getFooterButtons = () => {
|
|
|
if (scannedText) {
|
|
|
- // 已扫描到内容,显示确认和重新扫描按钮
|
|
|
return [
|
|
|
<Button key="rescan" onClick={handleRescan}>
|
|
|
重新扫描
|
|
|
@@ -233,7 +178,6 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
</Button>,
|
|
|
];
|
|
|
} else {
|
|
|
- // 还在扫描中,只显示取消按钮
|
|
|
return [
|
|
|
<Button key="cancel" onClick={handleCancel}>
|
|
|
取消
|
|
|
@@ -249,7 +193,6 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
onCancel={handleCancel}
|
|
|
footer={getFooterButtons()}
|
|
|
width={600}
|
|
|
- destroyOnClose
|
|
|
maskClosable={false} // 防止误点击关闭
|
|
|
>
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
@@ -272,27 +215,30 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
)}
|
|
|
|
|
|
{/* 扫描区域 */}
|
|
|
- {!scannedText && (
|
|
|
- <>
|
|
|
- <div
|
|
|
- id="qr-reader"
|
|
|
- style={{
|
|
|
- width: '100%',
|
|
|
- maxWidth: 500,
|
|
|
- margin: '0 auto',
|
|
|
- border: isScanning ? '2px solid #1890ff' : '2px solid #d9d9d9',
|
|
|
- borderRadius: 8,
|
|
|
- padding: 8,
|
|
|
- }}
|
|
|
- />
|
|
|
- <div style={{ marginTop: 16, color: '#666' }}>
|
|
|
- <p>请将二维码对准扫描框</p>
|
|
|
- <p style={{ fontSize: 12 }}>
|
|
|
- 提示:确保光线充足,二维码清晰可见
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </>
|
|
|
- )}
|
|
|
+
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ id={qrReaderId}
|
|
|
+ ref={readerRef}
|
|
|
+ style={{
|
|
|
+ width: '100%',
|
|
|
+ maxWidth: 500,
|
|
|
+ margin: '0 auto',
|
|
|
+ border: '2px solid #1890ff',
|
|
|
+ borderRadius: 8,
|
|
|
+ padding: 8,
|
|
|
+ minHeight: 300, // 给扫描区域固定高度
|
|
|
+ display: scannedText ? 'none' : 'block',
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <div style={{ marginTop: 16, color: '#666' }}>
|
|
|
+ <p>请将二维码对准扫描框</p>
|
|
|
+ <p style={{ fontSize: 12 }}>
|
|
|
+ 提示:确保光线充足,二维码清晰可见
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+
|
|
|
|
|
|
{/* 扫描结果显示 */}
|
|
|
{scannedText && (
|
|
|
@@ -309,7 +255,6 @@ const QRCodeScanModal: React.FC<QRCodeScanModalProps> = ({ visible, onCancel, fo
|
|
|
<Typography.Title level={5}>扫描结果:</Typography.Title>
|
|
|
<div
|
|
|
style={{
|
|
|
- backgroundColor: '#f5f5f5',
|
|
|
padding: 12,
|
|
|
borderRadius: 4,
|
|
|
border: '1px solid #d9d9d9',
|