FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

382 lines
16 KiB

  1. "use client";
  2. import { QrCodeInfo } from "@/app/api/qrcode";
  3. import { useRef } from "react";
  4. import {
  5. ReactNode,
  6. createContext,
  7. useCallback,
  8. useContext,
  9. useEffect,
  10. useState,
  11. startTransition,
  12. } from "react";
  13. export interface QrCodeScanner {
  14. values: string[];
  15. isScanning: boolean;
  16. startScan: () => void;
  17. stopScan: () => void;
  18. resetScan: () => void;
  19. result: QrCodeInfo | undefined;
  20. state: "scanning" | "pending" | "retry";
  21. error: string | undefined;
  22. }
  23. interface QrCodeScannerProviderProps {
  24. children: ReactNode;
  25. }
  26. export const QrCodeScannerContext = createContext<QrCodeScanner | undefined>(
  27. undefined,
  28. );
  29. const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
  30. children,
  31. }) => {
  32. const [qrCodeScannerValues, setQrCodeScannerValues] = useState<string[]>([]);
  33. const [isScanning, setIsScanning] = useState<boolean>(false);
  34. const [keys, setKeys] = useState<string[]>([]);
  35. const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState<number>(0);
  36. const [rightCurlyBraceCount, setRightCurlyBraceCount] = useState<number>(0);
  37. const [scanResult, setScanResult] = useState<QrCodeInfo | undefined>()
  38. const [scanState, setScanState] = useState<"scanning" | "pending" | "retry">("pending");
  39. const [scanError, setScanError] = useState<string | undefined>() // TODO return scan error message
  40. const keysRef = useRef<string[]>([]);
  41. const leftBraceCountRef = useRef<number>(0);
  42. const rightBraceCountRef = useRef<number>(0);
  43. const isFirstKeyRef = useRef<boolean>(true);
  44. const resetScannerInput = useCallback(() => {
  45. setKeys(() => []);
  46. setLeftCurlyBraceCount(() => 0);
  47. setRightCurlyBraceCount(() => 0);
  48. }, []);
  49. const resetQrCodeScanner = useCallback((error : string = "") => {
  50. setQrCodeScannerValues(() => []);
  51. setScanResult(undefined);
  52. resetScannerInput();
  53. console.log("%c Scanner Reset", "color:cyan");
  54. if (error.length > 0) {
  55. console.log("%c Error:", "color:red", error);
  56. console.log("%c key:", "color:red", keys);
  57. setScanState("retry");
  58. }
  59. }, []);
  60. const startQrCodeScanner = useCallback(() => {
  61. const startTime = performance.now();
  62. console.log(`⏱️ [SCANNER START] Called at: ${new Date().toISOString()}`);
  63. resetQrCodeScanner();
  64. const resetTime = performance.now() - startTime;
  65. console.log(`⏱️ [SCANNER START] Reset time: ${resetTime.toFixed(2)}ms`);
  66. setIsScanning(() => true);
  67. const setScanningTime = performance.now() - startTime;
  68. console.log(`⏱️ [SCANNER START] setScanning time: ${setScanningTime.toFixed(2)}ms`);
  69. const totalTime = performance.now() - startTime;
  70. console.log(`%c Scanning started `, "color:cyan");
  71. console.log(`⏱️ [SCANNER START] Total start time: ${totalTime.toFixed(2)}ms`);
  72. console.log(`⏰ [SCANNER START] Scanner started at: ${new Date().toISOString()}`);
  73. }, [resetQrCodeScanner]);
  74. const endQrCodeScanner = useCallback(() => {
  75. setIsScanning(() => false);
  76. console.log("%c Scanning stopped ", "color:cyan");
  77. }, []);
  78. // Find by rough match, return 0 if not found
  79. const findIdByRoughMatch = (inputString : string, keyword : string) => {
  80. console.log(`%c Performed rough match for ${keyword} within ${inputString}`, "color:brown");
  81. const keywordIndex = inputString.indexOf(keyword);
  82. let result : {keywordFound: boolean; number: number | null; message: string} = {
  83. keywordFound: false,
  84. number: null,
  85. message: `${keyword} not found in the input`,
  86. };
  87. if (keywordIndex !== -1) {
  88. const substringAfterKeyword = inputString.slice(keywordIndex + keyword.length);
  89. const numberMatch = substringAfterKeyword.match(/\d+/);
  90. if (!numberMatch) {
  91. result = {
  92. keywordFound: true,
  93. number: null,
  94. message: `No valid number found after ${keyword}`,
  95. };
  96. } else {
  97. result = {
  98. keywordFound: true,
  99. number: parseInt(numberMatch[0], 10),
  100. message: `Found ${keyword} at index ${keywordIndex}, first number found after is: ${numberMatch[0]}`,
  101. };
  102. }
  103. }
  104. console.log(`%c ${result.message}`, "color:brown");
  105. return result;
  106. };
  107. useEffect(() => {
  108. const effectStartTime = performance.now();
  109. console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Triggered at: ${new Date().toISOString()}`);
  110. console.log(`⏱️ [KEYBOARD LISTENER EFFECT] isScanning: ${isScanning}`);
  111. if (isScanning) {
  112. const listenerRegisterStartTime = performance.now();
  113. console.log(`⏱️ [KEYBOARD LISTENER] Registering keyboard listener at: ${new Date().toISOString()}`);
  114. // Reset refs when starting scan
  115. keysRef.current = [];
  116. leftBraceCountRef.current = 0;
  117. rightBraceCountRef.current = 0;
  118. isFirstKeyRef.current = true;
  119. const handleKeyDown = (event: KeyboardEvent) => {
  120. const keyPressTime = performance.now();
  121. const keyPressTimestamp = new Date().toISOString();
  122. // ✅ OPTIMIZED: Use refs to accumulate keys immediately (no state update delay)
  123. if (event.key.length === 1) {
  124. if (isFirstKeyRef.current) {
  125. console.log(`⏱️ [KEYBOARD] First key press detected: "${event.key}"`);
  126. console.log(`⏰ [KEYBOARD] First key press at: ${keyPressTimestamp}`);
  127. console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(keyPressTime - listenerRegisterStartTime).toFixed(2)}ms`);
  128. isFirstKeyRef.current = false;
  129. }
  130. keysRef.current.push(event.key);
  131. }
  132. if (event.key === "{") {
  133. const braceTime = performance.now();
  134. console.log(`⏱️ [KEYBOARD] Left brace "{" detected at: ${new Date().toISOString()}`);
  135. console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(braceTime - listenerRegisterStartTime).toFixed(2)}ms`);
  136. leftBraceCountRef.current += 1;
  137. } else if (event.key === "}") {
  138. const braceTime = performance.now();
  139. console.log(`⏱️ [KEYBOARD] Right brace "}" detected at: ${new Date().toISOString()}`);
  140. console.log(`⏱️ [KEYBOARD] Time since listener registered: ${(braceTime - listenerRegisterStartTime).toFixed(2)}ms`);
  141. rightBraceCountRef.current += 1;
  142. // ✅ OPTIMIZED: Check for complete QR immediately and update state only once
  143. if (leftBraceCountRef.current === rightBraceCountRef.current && leftBraceCountRef.current > 0) {
  144. const completeTime = performance.now();
  145. console.log(`⏱️ [KEYBOARD] Complete QR detected immediately! Time: ${completeTime.toFixed(2)}ms`);
  146. console.log(`⏰ [KEYBOARD] Complete QR at: ${new Date().toISOString()}`);
  147. const qrValue = keysRef.current.join("").substring(
  148. keysRef.current.indexOf("{"),
  149. keysRef.current.lastIndexOf("}") + 1
  150. );
  151. console.log(`⏱️ [KEYBOARD] QR value: ${qrValue}`);
  152. // ✅ TABLET OPTIMIZATION: Directly set qrCodeScannerValues without any state chain
  153. // Use flushSync for immediate update on tablets (if available, otherwise use regular setState)
  154. setQrCodeScannerValues((value) => {
  155. console.log(`⏱️ [KEYBOARD] Setting qrCodeScannerValues directly: ${qrValue}`);
  156. return [...value, qrValue];
  157. });
  158. // Reset scanner input immediately (using refs, no state update)
  159. keysRef.current = [];
  160. leftBraceCountRef.current = 0;
  161. rightBraceCountRef.current = 0;
  162. isFirstKeyRef.current = true;
  163. // ✅ TABLET OPTIMIZATION: Defer all cleanup state updates to avoid blocking
  164. // Use setTimeout to ensure QR processing happens first
  165. setTimeout(() => {
  166. startTransition(() => {
  167. setKeys([]);
  168. setLeftCurlyBraceCount(0);
  169. setRightCurlyBraceCount(0);
  170. setScanState("pending");
  171. resetScannerInput();
  172. });
  173. }, 0);
  174. return;
  175. }
  176. }
  177. // ✅ TABLET OPTIMIZATION: Completely skip state updates during scanning
  178. // Only update state for the first brace detection (for UI feedback)
  179. // All other updates are deferred to avoid blocking on tablets
  180. if (leftBraceCountRef.current === 1 && keysRef.current.length === 1 && event.key === "{") {
  181. // Only update state once when first brace is detected
  182. startTransition(() => {
  183. setKeys([...keysRef.current]);
  184. setLeftCurlyBraceCount(leftBraceCountRef.current);
  185. setRightCurlyBraceCount(rightBraceCountRef.current);
  186. });
  187. }
  188. // Skip all other state updates during scanning to maximize performance on tablets
  189. };
  190. document.addEventListener("keydown", handleKeyDown);
  191. const listenerRegisterTime = performance.now() - listenerRegisterStartTime;
  192. console.log(`⏱️ [KEYBOARD LISTENER] Listener registered in: ${listenerRegisterTime.toFixed(2)}ms`);
  193. console.log(`⏰ [KEYBOARD LISTENER] Listener ready at: ${new Date().toISOString()}`);
  194. return () => {
  195. console.log(`⏱️ [KEYBOARD LISTENER] Removing keyboard listener at: ${new Date().toISOString()}`);
  196. document.removeEventListener("keydown", handleKeyDown);
  197. };
  198. } else {
  199. console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Scanner not active, skipping listener registration`);
  200. }
  201. const effectTime = performance.now() - effectStartTime;
  202. console.log(`⏱️ [KEYBOARD LISTENER EFFECT] Total effect time: ${effectTime.toFixed(2)}ms`);
  203. }, [isScanning]);
  204. // ✅ OPTIMIZED: Simplify the QR scanner effect - it's now mainly for initial detection
  205. useEffect(() => {
  206. const effectStartTime = performance.now();
  207. console.log(`⏱️ [QR SCANNER EFFECT] Triggered at: ${new Date().toISOString()}`);
  208. console.log(`⏱️ [QR SCANNER EFFECT] Keys count: ${keys.length}, leftBrace: ${leftCurlyBraceCount}, rightBrace: ${rightCurlyBraceCount}`);
  209. if (rightCurlyBraceCount > leftCurlyBraceCount || leftCurlyBraceCount > 1) { // Prevent multiple scan
  210. setScanState("retry");
  211. setScanError("Too many scans at once");
  212. resetQrCodeScanner("Too many scans at once");
  213. } else {
  214. // Only show "scanning" state when first brace is detected
  215. if (leftCurlyBraceCount == 1 && keys.length == 1)
  216. {
  217. const scanDetectedTime = performance.now();
  218. setScanState("scanning");
  219. console.log(`%c Scan detected, waiting for inputs...`, "color:cyan");
  220. console.log(`⏱️ [QR SCANNER] Scan detected time: ${scanDetectedTime.toFixed(2)}ms`);
  221. console.log(`⏰ [QR SCANNER] Scan detected at: ${new Date().toISOString()}`);
  222. }
  223. // Note: Complete QR detection is now handled directly in handleKeyDown
  224. // This effect is mainly for UI feedback and error handling
  225. }
  226. }, [keys, leftCurlyBraceCount, rightCurlyBraceCount]);
  227. useEffect(() => {
  228. if (qrCodeScannerValues.length > 0) {
  229. const processStartTime = performance.now();
  230. console.log(`⏱️ [QR SCANNER PROCESS] Processing qrCodeScannerValues at: ${new Date().toISOString()}`);
  231. console.log(`⏱️ [QR SCANNER PROCESS] Values count: ${qrCodeScannerValues.length}`);
  232. const scannedValues = qrCodeScannerValues[0];
  233. console.log(`%c Scanned Result: `, "color:cyan", scannedValues);
  234. console.log(`⏱️ [QR SCANNER PROCESS] Scanned value: ${scannedValues}`);
  235. console.log(`⏰ [QR SCANNER PROCESS] Processing at: ${new Date().toISOString()}`);
  236. if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING
  237. // 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式
  238. // 这些格式需要传递完整值给 processQrCode 处理
  239. if (scannedValues.length > 9) {
  240. const ninthChar = scannedValues.substring(8, 9);
  241. if (ninthChar === "e" || ninthChar === "u") {
  242. // {2fiteste数字} 或 {2fitestu任何内容} 格式
  243. console.log(`%c DEBUG: detected shortcut format: `, "color:pink", scannedValues);
  244. const debugValue = {
  245. value: scannedValues // 传递完整值,让 processQrCode 处理
  246. }
  247. setScanResult(debugValue);
  248. const processTime = performance.now() - processStartTime;
  249. console.log(`⏱️ [QR SCANNER PROCESS] Shortcut processing time: ${processTime.toFixed(2)}ms`);
  250. return;
  251. }
  252. }
  253. // 原有的 {2fitest数字} 格式(纯数字,向后兼容)
  254. const number = scannedValues.substring(8, scannedValues.length - 1);
  255. if (/^\d+$/.test(number)) { // Check if number contains only digits
  256. console.log(`%c DEBUG: detected ID: `, "color:pink", number);
  257. const debugValue = {
  258. value: number
  259. }
  260. setScanResult(debugValue);
  261. const processTime = performance.now() - processStartTime;
  262. console.log(`⏱️ [QR SCANNER PROCESS] ID processing time: ${processTime.toFixed(2)}ms`);
  263. return;
  264. } else {
  265. // 如果不是纯数字,传递完整值让 processQrCode 处理
  266. const debugValue = {
  267. value: scannedValues
  268. }
  269. setScanResult(debugValue);
  270. const processTime = performance.now() - processStartTime;
  271. console.log(`⏱️ [QR SCANNER PROCESS] Non-numeric processing time: ${processTime.toFixed(2)}ms`);
  272. return;
  273. }
  274. }
  275. try {
  276. const parseStartTime = performance.now();
  277. const data: QrCodeInfo = JSON.parse(scannedValues);
  278. const parseTime = performance.now() - parseStartTime;
  279. console.log(`%c Parsed scan data`, "color:green", data);
  280. console.log(`⏱️ [QR SCANNER PROCESS] JSON parse time: ${parseTime.toFixed(2)}ms`);
  281. const content = scannedValues.substring(1, scannedValues.length - 1);
  282. data.value = content;
  283. const setResultStartTime = performance.now();
  284. setScanResult(data);
  285. const setResultTime = performance.now() - setResultStartTime;
  286. console.log(`⏱️ [QR SCANNER PROCESS] setScanResult time: ${setResultTime.toFixed(2)}ms`);
  287. console.log(`⏰ [QR SCANNER PROCESS] setScanResult at: ${new Date().toISOString()}`);
  288. const processTime = performance.now() - processStartTime;
  289. console.log(`⏱️ [QR SCANNER PROCESS] Total processing time: ${processTime.toFixed(2)}ms`);
  290. } catch (error) { // Rough match for other scanner input -- Pending Review
  291. console.log(`⏱️ [QR SCANNER PROCESS] JSON parse failed, trying rough match`);
  292. const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0;
  293. if (silId == 0) {
  294. const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0;
  295. setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()});
  296. } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); }
  297. resetQrCodeScanner(String(error));
  298. }
  299. // resetQrCodeScanner();
  300. }
  301. }, [qrCodeScannerValues]);
  302. return (
  303. <QrCodeScannerContext.Provider
  304. value={{
  305. values: qrCodeScannerValues,
  306. isScanning: isScanning,
  307. startScan: startQrCodeScanner,
  308. stopScan: endQrCodeScanner,
  309. resetScan: resetQrCodeScanner,
  310. result: scanResult,
  311. state: scanState,
  312. error: scanError,
  313. }}
  314. >
  315. {children}
  316. </QrCodeScannerContext.Provider>
  317. );
  318. };
  319. export const useQrCodeScannerContext = (): QrCodeScanner => {
  320. const context = useContext(QrCodeScannerContext);
  321. if (!context) {
  322. throw new Error(
  323. "useQrCodeScanner must be used within a QrCodeScannerProvider",
  324. );
  325. }
  326. return context;
  327. };
  328. export default QrCodeScannerProvider;