|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- "use client";
- import { QrCodeInfo } from "@/app/api/qrcode";
- import { useRef } from "react";
- import {
- ReactNode,
- createContext,
- useCallback,
- useContext,
- useEffect,
- useState,
- startTransition,
- } from "react";
-
- export interface QrCodeScanner {
- values: string[];
- isScanning: boolean;
- startScan: () => void;
- stopScan: () => void;
- resetScan: () => void;
- result: QrCodeInfo | undefined;
- state: "scanning" | "pending" | "retry";
- error: string | undefined;
- }
-
- interface QrCodeScannerProviderProps {
- children: ReactNode;
- }
-
- export const QrCodeScannerContext = createContext<QrCodeScanner | undefined>(
- undefined,
- );
-
- const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
- children,
- }) => {
- const [qrCodeScannerValues, setQrCodeScannerValues] = useState<string[]>([]);
- const [isScanning, setIsScanning] = useState<boolean>(false);
- const [keys, setKeys] = useState<string[]>([]);
- const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState<number>(0);
- const [rightCurlyBraceCount, setRightCurlyBraceCount] = useState<number>(0);
- 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(() => []);
- setLeftCurlyBraceCount(() => 0);
- setRightCurlyBraceCount(() => 0);
- }, []);
-
- const resetQrCodeScanner = useCallback((error : string = "") => {
- setQrCodeScannerValues(() => []);
- setScanResult(undefined);
- resetScannerInput();
-
- console.log("%c Scanner Reset", "color:cyan");
-
- if (error.length > 0) {
- console.log("%c Error:", "color:red", error);
- console.log("%c key:", "color:red", keys);
- setScanState("retry");
- }
- }, []);
-
- 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);
- 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);
- console.log("%c Scanning stopped ", "color:cyan");
- }, []);
-
- // Find by rough match, return 0 if not found
- const findIdByRoughMatch = (inputString : string, keyword : string) => {
- console.log(`%c Performed rough match for ${keyword} within ${inputString}`, "color:brown");
-
- const keywordIndex = inputString.indexOf(keyword);
-
- let result : {keywordFound: boolean; number: number | null; message: string} = {
- keywordFound: false,
- number: null,
- message: `${keyword} not found in the input`,
- };
-
- if (keywordIndex !== -1) {
- const substringAfterKeyword = inputString.slice(keywordIndex + keyword.length);
-
- const numberMatch = substringAfterKeyword.match(/\d+/);
-
- if (!numberMatch) {
- result = {
- keywordFound: true,
- number: null,
- message: `No valid number found after ${keyword}`,
- };
- } else {
- result = {
- keywordFound: true,
- number: parseInt(numberMatch[0], 10),
- message: `Found ${keyword} at index ${keywordIndex}, first number found after is: ${numberMatch[0]}`,
- };
- }
- }
-
- console.log(`%c ${result.message}`, "color:brown");
- return result;
- };
-
- 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) {
- 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 === "{") {
- 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 === "}") {
- 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]);
-
- // ✅ 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}`);
-
- 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()}`);
- }
- // 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(`⏱️ [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...} 格式
- // 这些格式需要传递完整值给 processQrCode 处理
- if (scannedValues.length > 9) {
- const ninthChar = scannedValues.substring(8, 9);
- if (ninthChar === "e" || ninthChar === "u") {
- // {2fiteste数字} 或 {2fitestu任何内容} 格式
- 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;
- }
- }
-
- // 原有的 {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);
- 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;
- }
- }
-
- try {
- const parseStartTime = performance.now();
- const data: QrCodeInfo = JSON.parse(scannedValues);
- 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) {
- const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0;
- setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()});
- } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); }
-
- resetQrCodeScanner(String(error));
- }
-
- // resetQrCodeScanner();
- }
- }, [qrCodeScannerValues]);
-
- return (
- <QrCodeScannerContext.Provider
- value={{
- values: qrCodeScannerValues,
- isScanning: isScanning,
- startScan: startQrCodeScanner,
- stopScan: endQrCodeScanner,
- resetScan: resetQrCodeScanner,
- result: scanResult,
- state: scanState,
- error: scanError,
- }}
- >
- {children}
- </QrCodeScannerContext.Provider>
- );
- };
-
- export const useQrCodeScannerContext = (): QrCodeScanner => {
- const context = useContext(QrCodeScannerContext);
- if (!context) {
- throw new Error(
- "useQrCodeScanner must be used within a QrCodeScannerProvider",
- );
- }
- return context;
- };
-
- export default QrCodeScannerProvider;
|