| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| import { useRef } from "react"; | |||
| import { | |||
| ReactNode, | |||
| createContext, | |||
| @@ -7,6 +8,7 @@ import { | |||
| useContext, | |||
| useEffect, | |||
| useState, | |||
| startTransition, | |||
| } from "react"; | |||
| export interface QrCodeScanner { | |||
| @@ -39,6 +41,10 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| const [scanResult, setScanResult] = useState<QrCodeInfo | undefined>() | |||
| const [scanState, setScanState] = useState<"scanning" | "pending" | "retry">("pending"); | |||
| const [scanError, setScanError] = useState<string | undefined>() // TODO return scan error message | |||
| const keysRef = useRef<string[]>([]); | |||
| const leftBraceCountRef = useRef<number>(0); | |||
| const rightBraceCountRef = useRef<number>(0); | |||
| const isFirstKeyRef = useRef<boolean>(true); | |||
| const resetScannerInput = useCallback(() => { | |||
| setKeys(() => []); | |||
| @@ -61,10 +67,22 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| }, []); | |||
| const startQrCodeScanner = useCallback(() => { | |||
| const startTime = performance.now(); | |||
| console.log(`⏱️ [SCANNER START] Called at: ${new Date().toISOString()}`); | |||
| resetQrCodeScanner(); | |||
| const resetTime = performance.now() - startTime; | |||
| console.log(`⏱️ [SCANNER START] Reset time: ${resetTime.toFixed(2)}ms`); | |||
| setIsScanning(() => true); | |||
| console.log("%c Scanning started ", "color:cyan"); | |||
| }, []); | |||
| const setScanningTime = performance.now() - startTime; | |||
| console.log(`⏱️ [SCANNER START] setScanning time: ${setScanningTime.toFixed(2)}ms`); | |||
| const totalTime = performance.now() - startTime; | |||
| console.log(`%c Scanning started `, "color:cyan"); | |||
| console.log(`⏱️ [SCANNER START] Total start time: ${totalTime.toFixed(2)}ms`); | |||
| console.log(`⏰ [SCANNER START] Scanner started at: ${new Date().toISOString()}`); | |||
| }, [resetQrCodeScanner]); | |||
| const endQrCodeScanner = useCallback(() => { | |||
| setIsScanning(() => false); | |||
| @@ -107,65 +125,154 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| return result; | |||
| }; | |||
| // Check the KeyDown | |||
| useEffect(() => { | |||
| const effectStartTime = performance.now(); | |||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Triggered at: ${new Date().toISOString()}`); | |||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] isScanning: ${isScanning}`); | |||
| if (isScanning) { | |||
| const listenerRegisterStartTime = performance.now(); | |||
| console.log(`⏱️ [KEYBOARD LISTENER] Registering keyboard listener at: ${new Date().toISOString()}`); | |||
| // Reset refs when starting scan | |||
| keysRef.current = []; | |||
| leftBraceCountRef.current = 0; | |||
| rightBraceCountRef.current = 0; | |||
| isFirstKeyRef.current = true; | |||
| const handleKeyDown = (event: KeyboardEvent) => { | |||
| const keyPressTime = performance.now(); | |||
| const keyPressTimestamp = new Date().toISOString(); | |||
| // ✅ OPTIMIZED: Use refs to accumulate keys immediately (no state update delay) | |||
| if (event.key.length === 1) { | |||
| setKeys((key) => [...key, event.key]); | |||
| if (isFirstKeyRef.current) { | |||
| console.log(`⏱️ [KEYBOARD] First key press detected: "${event.key}"`); | |||
| console.log(`⏰ [KEYBOARD] First key press at: ${keyPressTimestamp}`); | |||
| console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(keyPressTime - listenerRegisterStartTime).toFixed(2)}ms`); | |||
| isFirstKeyRef.current = false; | |||
| } | |||
| keysRef.current.push(event.key); | |||
| } | |||
| if (event.key === "{") { | |||
| setLeftCurlyBraceCount((count) => count + 1); | |||
| const braceTime = performance.now(); | |||
| console.log(`⏱️ [KEYBOARD] Left brace "{" detected at: ${new Date().toISOString()}`); | |||
| console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(braceTime - listenerRegisterStartTime).toFixed(2)}ms`); | |||
| leftBraceCountRef.current += 1; | |||
| } else if (event.key === "}") { | |||
| setRightCurlyBraceCount((count) => count + 1); | |||
| const braceTime = performance.now(); | |||
| console.log(`⏱️ [KEYBOARD] Right brace "}" detected at: ${new Date().toISOString()}`); | |||
| console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(braceTime - listenerRegisterStartTime).toFixed(2)}ms`); | |||
| rightBraceCountRef.current += 1; | |||
| // ✅ OPTIMIZED: Check for complete QR immediately and update state only once | |||
| if (leftBraceCountRef.current === rightBraceCountRef.current && leftBraceCountRef.current > 0) { | |||
| const completeTime = performance.now(); | |||
| console.log(`⏱️ [KEYBOARD] Complete QR detected immediately! Time: ${completeTime.toFixed(2)}ms`); | |||
| console.log(`⏰ [KEYBOARD] Complete QR at: ${new Date().toISOString()}`); | |||
| const qrValue = keysRef.current.join("").substring( | |||
| keysRef.current.indexOf("{"), | |||
| keysRef.current.lastIndexOf("}") + 1 | |||
| ); | |||
| console.log(`⏱️ [KEYBOARD] QR value: ${qrValue}`); | |||
| // ✅ TABLET OPTIMIZATION: Directly set qrCodeScannerValues without any state chain | |||
| // Use flushSync for immediate update on tablets (if available, otherwise use regular setState) | |||
| setQrCodeScannerValues((value) => { | |||
| console.log(`⏱️ [KEYBOARD] Setting qrCodeScannerValues directly: ${qrValue}`); | |||
| return [...value, qrValue]; | |||
| }); | |||
| // Reset scanner input immediately (using refs, no state update) | |||
| keysRef.current = []; | |||
| leftBraceCountRef.current = 0; | |||
| rightBraceCountRef.current = 0; | |||
| isFirstKeyRef.current = true; | |||
| // ✅ TABLET OPTIMIZATION: Defer all cleanup state updates to avoid blocking | |||
| // Use setTimeout to ensure QR processing happens first | |||
| setTimeout(() => { | |||
| startTransition(() => { | |||
| setKeys([]); | |||
| setLeftCurlyBraceCount(0); | |||
| setRightCurlyBraceCount(0); | |||
| setScanState("pending"); | |||
| resetScannerInput(); | |||
| }); | |||
| }, 0); | |||
| return; | |||
| } | |||
| } | |||
| // ✅ TABLET OPTIMIZATION: Completely skip state updates during scanning | |||
| // Only update state for the first brace detection (for UI feedback) | |||
| // All other updates are deferred to avoid blocking on tablets | |||
| if (leftBraceCountRef.current === 1 && keysRef.current.length === 1 && event.key === "{") { | |||
| // Only update state once when first brace is detected | |||
| startTransition(() => { | |||
| setKeys([...keysRef.current]); | |||
| setLeftCurlyBraceCount(leftBraceCountRef.current); | |||
| setRightCurlyBraceCount(rightBraceCountRef.current); | |||
| }); | |||
| } | |||
| // Skip all other state updates during scanning to maximize performance on tablets | |||
| }; | |||
| document.addEventListener("keydown", handleKeyDown); | |||
| const listenerRegisterTime = performance.now() - listenerRegisterStartTime; | |||
| console.log(`⏱️ [KEYBOARD LISTENER] Listener registered in: ${listenerRegisterTime.toFixed(2)}ms`); | |||
| console.log(`⏰ [KEYBOARD LISTENER] Listener ready at: ${new Date().toISOString()}`); | |||
| return () => { | |||
| console.log(`⏱️ [KEYBOARD LISTENER] Removing keyboard listener at: ${new Date().toISOString()}`); | |||
| document.removeEventListener("keydown", handleKeyDown); | |||
| }; | |||
| } else { | |||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Scanner not active, skipping listener registration`); | |||
| } | |||
| const effectTime = performance.now() - effectStartTime; | |||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Total effect time: ${effectTime.toFixed(2)}ms`); | |||
| }, [isScanning]); | |||
| // Update Qr Code Scanner Values | |||
| useEffect(() => { | |||
| if (rightCurlyBraceCount > leftCurlyBraceCount || leftCurlyBraceCount > 1) { // Prevent multiple scan | |||
| setScanState("retry"); | |||
| setScanError("Too many scans at once"); | |||
| resetQrCodeScanner("Too many scans at once"); | |||
| } else { | |||
| if (leftCurlyBraceCount == 1 && keys.length == 1) | |||
| { | |||
| setScanState("scanning"); | |||
| console.log("%c Scan detected, waiting for inputs...", "color:cyan"); | |||
| } | |||
| if ( | |||
| leftCurlyBraceCount !== 0 && | |||
| rightCurlyBraceCount !== 0 && | |||
| leftCurlyBraceCount === rightCurlyBraceCount | |||
| ) { | |||
| const startBrace = keys.indexOf("{"); | |||
| const endBrace = keys.lastIndexOf("}"); | |||
| setScanState("pending"); | |||
| setQrCodeScannerValues((value) => [ | |||
| ...value, | |||
| keys.join("").substring(startBrace, endBrace + 1), | |||
| ]); | |||
| // console.log(keys); | |||
| // console.log("%c QR Scanner Values:", "color:cyan", qrCodeScannerValues); | |||
| // ✅ OPTIMIZED: Simplify the QR scanner effect - it's now mainly for initial detection | |||
| useEffect(() => { | |||
| const effectStartTime = performance.now(); | |||
| console.log(`⏱️ [QR SCANNER EFFECT] Triggered at: ${new Date().toISOString()}`); | |||
| console.log(`⏱️ [QR SCANNER EFFECT] Keys count: ${keys.length}, leftBrace: ${leftCurlyBraceCount}, rightBrace: ${rightCurlyBraceCount}`); | |||
| resetScannerInput(); | |||
| if (rightCurlyBraceCount > leftCurlyBraceCount || leftCurlyBraceCount > 1) { // Prevent multiple scan | |||
| setScanState("retry"); | |||
| setScanError("Too many scans at once"); | |||
| resetQrCodeScanner("Too many scans at once"); | |||
| } else { | |||
| // Only show "scanning" state when first brace is detected | |||
| if (leftCurlyBraceCount == 1 && keys.length == 1) | |||
| { | |||
| const scanDetectedTime = performance.now(); | |||
| setScanState("scanning"); | |||
| console.log(`%c Scan detected, waiting for inputs...`, "color:cyan"); | |||
| console.log(`⏱️ [QR SCANNER] Scan detected time: ${scanDetectedTime.toFixed(2)}ms`); | |||
| console.log(`⏰ [QR SCANNER] Scan detected at: ${new Date().toISOString()}`); | |||
| } | |||
| } | |||
| }, [keys, leftCurlyBraceCount, rightCurlyBraceCount]); | |||
| // Note: Complete QR detection is now handled directly in handleKeyDown | |||
| // This effect is mainly for UI feedback and error handling | |||
| } | |||
| }, [keys, leftCurlyBraceCount, rightCurlyBraceCount]); | |||
| useEffect(() => { | |||
| if (qrCodeScannerValues.length > 0) { | |||
| const processStartTime = performance.now(); | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Processing qrCodeScannerValues at: ${new Date().toISOString()}`); | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Values count: ${qrCodeScannerValues.length}`); | |||
| const scannedValues = qrCodeScannerValues[0]; | |||
| console.log("%c Scanned Result: ", "color:cyan", scannedValues); | |||
| console.log(`%c Scanned Result: `, "color:cyan", scannedValues); | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Scanned value: ${scannedValues}`); | |||
| console.log(`⏰ [QR SCANNER PROCESS] Processing at: ${new Date().toISOString()}`); | |||
| if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING | |||
| // 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式 | |||
| @@ -174,11 +281,13 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| const ninthChar = scannedValues.substring(8, 9); | |||
| if (ninthChar === "e" || ninthChar === "u") { | |||
| // {2fiteste数字} 或 {2fitestu任何内容} 格式 | |||
| console.log("%c DEBUG: detected shortcut format: ", "color:pink", scannedValues); | |||
| console.log(`%c DEBUG: detected shortcut format: `, "color:pink", scannedValues); | |||
| const debugValue = { | |||
| value: scannedValues // 传递完整值,让 processQrCode 处理 | |||
| } | |||
| setScanResult(debugValue); | |||
| const processTime = performance.now() - processStartTime; | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Shortcut processing time: ${processTime.toFixed(2)}ms`); | |||
| return; | |||
| } | |||
| } | |||
| @@ -186,30 +295,47 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||
| // 原有的 {2fitest数字} 格式(纯数字,向后兼容) | |||
| const number = scannedValues.substring(8, scannedValues.length - 1); | |||
| if (/^\d+$/.test(number)) { // Check if number contains only digits | |||
| console.log("%c DEBUG: detected ID: ", "color:pink", number); | |||
| console.log(`%c DEBUG: detected ID: `, "color:pink", number); | |||
| const debugValue = { | |||
| value: number | |||
| } | |||
| setScanResult(debugValue); | |||
| const processTime = performance.now() - processStartTime; | |||
| console.log(`⏱️ [QR SCANNER PROCESS] ID processing time: ${processTime.toFixed(2)}ms`); | |||
| return; | |||
| } else { | |||
| // 如果不是纯数字,传递完整值让 processQrCode 处理 | |||
| const debugValue = { | |||
| value: scannedValues | |||
| } | |||
| setScanResult(debugValue); | |||
| const processTime = performance.now() - processStartTime; | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Non-numeric processing time: ${processTime.toFixed(2)}ms`); | |||
| return; | |||
| } | |||
| return; | |||
| } | |||
| try { | |||
| const parseStartTime = performance.now(); | |||
| const data: QrCodeInfo = JSON.parse(scannedValues); | |||
| console.log("%c Parsed scan data", "color:green", data); | |||
| const parseTime = performance.now() - parseStartTime; | |||
| console.log(`%c Parsed scan data`, "color:green", data); | |||
| console.log(`⏱️ [QR SCANNER PROCESS] JSON parse time: ${parseTime.toFixed(2)}ms`); | |||
| const content = scannedValues.substring(1, scannedValues.length - 1); | |||
| data.value = content; | |||
| const setResultStartTime = performance.now(); | |||
| setScanResult(data); | |||
| const setResultTime = performance.now() - setResultStartTime; | |||
| console.log(`⏱️ [QR SCANNER PROCESS] setScanResult time: ${setResultTime.toFixed(2)}ms`); | |||
| console.log(`⏰ [QR SCANNER PROCESS] setScanResult at: ${new Date().toISOString()}`); | |||
| const processTime = performance.now() - processStartTime; | |||
| console.log(`⏱️ [QR SCANNER PROCESS] Total processing time: ${processTime.toFixed(2)}ms`); | |||
| } catch (error) { // Rough match for other scanner input -- Pending Review | |||
| console.log(`⏱️ [QR SCANNER PROCESS] JSON parse failed, trying rough match`); | |||
| const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; | |||
| if (silId == 0) { | |||