| @@ -1,5 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | import { QrCodeInfo } from "@/app/api/qrcode"; | ||||
| import { useRef } from "react"; | |||||
| import { | import { | ||||
| ReactNode, | ReactNode, | ||||
| createContext, | createContext, | ||||
| @@ -7,6 +8,7 @@ import { | |||||
| useContext, | useContext, | ||||
| useEffect, | useEffect, | ||||
| useState, | useState, | ||||
| startTransition, | |||||
| } from "react"; | } from "react"; | ||||
| export interface QrCodeScanner { | export interface QrCodeScanner { | ||||
| @@ -39,6 +41,10 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
| const [scanResult, setScanResult] = useState<QrCodeInfo | undefined>() | const [scanResult, setScanResult] = useState<QrCodeInfo | undefined>() | ||||
| const [scanState, setScanState] = useState<"scanning" | "pending" | "retry">("pending"); | const [scanState, setScanState] = useState<"scanning" | "pending" | "retry">("pending"); | ||||
| const [scanError, setScanError] = useState<string | undefined>() // TODO return scan error message | 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(() => { | const resetScannerInput = useCallback(() => { | ||||
| setKeys(() => []); | setKeys(() => []); | ||||
| @@ -61,10 +67,22 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
| }, []); | }, []); | ||||
| const startQrCodeScanner = useCallback(() => { | const startQrCodeScanner = useCallback(() => { | ||||
| const startTime = performance.now(); | |||||
| console.log(`⏱️ [SCANNER START] Called at: ${new Date().toISOString()}`); | |||||
| resetQrCodeScanner(); | resetQrCodeScanner(); | ||||
| const resetTime = performance.now() - startTime; | |||||
| console.log(`⏱️ [SCANNER START] Reset time: ${resetTime.toFixed(2)}ms`); | |||||
| setIsScanning(() => true); | 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(() => { | const endQrCodeScanner = useCallback(() => { | ||||
| setIsScanning(() => false); | setIsScanning(() => false); | ||||
| @@ -107,65 +125,154 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
| return result; | return result; | ||||
| }; | }; | ||||
| // Check the KeyDown | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const effectStartTime = performance.now(); | |||||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Triggered at: ${new Date().toISOString()}`); | |||||
| console.log(`⏱️ [KEYBOARD LISTENER EFFECT] isScanning: ${isScanning}`); | |||||
| if (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 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) { | 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 === "{") { | 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 === "}") { | } 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); | 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 () => { | return () => { | ||||
| console.log(`⏱️ [KEYBOARD LISTENER] Removing keyboard listener at: ${new Date().toISOString()}`); | |||||
| document.removeEventListener("keydown", handleKeyDown); | 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]); | }, [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(() => { | useEffect(() => { | ||||
| if (qrCodeScannerValues.length > 0) { | 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]; | 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 | if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING | ||||
| // 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式 | // 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式 | ||||
| @@ -174,11 +281,13 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
| const ninthChar = scannedValues.substring(8, 9); | const ninthChar = scannedValues.substring(8, 9); | ||||
| if (ninthChar === "e" || ninthChar === "u") { | if (ninthChar === "e" || ninthChar === "u") { | ||||
| // {2fiteste数字} 或 {2fitestu任何内容} 格式 | // {2fiteste数字} 或 {2fitestu任何内容} 格式 | ||||
| console.log("%c DEBUG: detected shortcut format: ", "color:pink", scannedValues); | |||||
| console.log(`%c DEBUG: detected shortcut format: `, "color:pink", scannedValues); | |||||
| const debugValue = { | const debugValue = { | ||||
| value: scannedValues // 传递完整值,让 processQrCode 处理 | value: scannedValues // 传递完整值,让 processQrCode 处理 | ||||
| } | } | ||||
| setScanResult(debugValue); | setScanResult(debugValue); | ||||
| const processTime = performance.now() - processStartTime; | |||||
| console.log(`⏱️ [QR SCANNER PROCESS] Shortcut processing time: ${processTime.toFixed(2)}ms`); | |||||
| return; | return; | ||||
| } | } | ||||
| } | } | ||||
| @@ -186,30 +295,47 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({ | |||||
| // 原有的 {2fitest数字} 格式(纯数字,向后兼容) | // 原有的 {2fitest数字} 格式(纯数字,向后兼容) | ||||
| const number = scannedValues.substring(8, scannedValues.length - 1); | const number = scannedValues.substring(8, scannedValues.length - 1); | ||||
| if (/^\d+$/.test(number)) { // Check if number contains only digits | 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 = { | const debugValue = { | ||||
| value: number | value: number | ||||
| } | } | ||||
| setScanResult(debugValue); | setScanResult(debugValue); | ||||
| const processTime = performance.now() - processStartTime; | |||||
| console.log(`⏱️ [QR SCANNER PROCESS] ID processing time: ${processTime.toFixed(2)}ms`); | |||||
| return; | |||||
| } else { | } else { | ||||
| // 如果不是纯数字,传递完整值让 processQrCode 处理 | // 如果不是纯数字,传递完整值让 processQrCode 处理 | ||||
| const debugValue = { | const debugValue = { | ||||
| value: scannedValues | value: scannedValues | ||||
| } | } | ||||
| setScanResult(debugValue); | setScanResult(debugValue); | ||||
| const processTime = performance.now() - processStartTime; | |||||
| console.log(`⏱️ [QR SCANNER PROCESS] Non-numeric processing time: ${processTime.toFixed(2)}ms`); | |||||
| return; | |||||
| } | } | ||||
| return; | |||||
| } | } | ||||
| try { | try { | ||||
| const parseStartTime = performance.now(); | |||||
| const data: QrCodeInfo = JSON.parse(scannedValues); | 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); | const content = scannedValues.substring(1, scannedValues.length - 1); | ||||
| data.value = content; | data.value = content; | ||||
| const setResultStartTime = performance.now(); | |||||
| setScanResult(data); | 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 | } 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; | const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; | ||||
| if (silId == 0) { | if (silId == 0) { | ||||