FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

QrCodeScannerProvider.tsx 16 KiB

3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3週間前
3ヶ月前
3ヶ月前
6日前
3週間前
6日前
3週間前
3週間前
3ヶ月前
3週間前
3ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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 normalizedScannedValues = scannedValues.replace(/\\"/g, '"');
  278. const data: QrCodeInfo = JSON.parse(normalizedScannedValues);
  279. const parseTime = performance.now() - parseStartTime;
  280. // console.log(`%c Parsed scan data`, "color:green", data);
  281. //console.log(`⏱️ [QR SCANNER PROCESS] JSON parse time: ${parseTime.toFixed(2)}ms`);
  282. const content = normalizedScannedValues.substring(1, normalizedScannedValues.length - 1);
  283. data.value = content;
  284. const setResultStartTime = performance.now();
  285. setScanResult(data);
  286. const setResultTime = performance.now() - setResultStartTime;
  287. // console.log(`⏱️ [QR SCANNER PROCESS] setScanResult time: ${setResultTime.toFixed(2)}ms`);
  288. //console.log(`⏰ [QR SCANNER PROCESS] setScanResult at: ${new Date().toISOString()}`);
  289. const processTime = performance.now() - processStartTime;
  290. // console.log(`⏱️ [QR SCANNER PROCESS] Total processing time: ${processTime.toFixed(2)}ms`);
  291. } catch (error) { // Rough match for other scanner input -- Pending Review
  292. //console.log(`⏱️ [QR SCANNER PROCESS] JSON parse failed, trying rough match`);
  293. const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0;
  294. if (silId == 0) {
  295. const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0;
  296. setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()});
  297. } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); }
  298. resetQrCodeScanner(String(error));
  299. }
  300. // resetQrCodeScanner();
  301. }
  302. }, [qrCodeScannerValues]);
  303. return (
  304. <QrCodeScannerContext.Provider
  305. value={{
  306. values: qrCodeScannerValues,
  307. isScanning: isScanning,
  308. startScan: startQrCodeScanner,
  309. stopScan: endQrCodeScanner,
  310. resetScan: resetQrCodeScanner,
  311. result: scanResult,
  312. state: scanState,
  313. error: scanError,
  314. }}
  315. >
  316. {children}
  317. </QrCodeScannerContext.Provider>
  318. );
  319. };
  320. export const useQrCodeScannerContext = (): QrCodeScanner => {
  321. const context = useContext(QrCodeScannerContext);
  322. if (!context) {
  323. throw new Error(
  324. "useQrCodeScanner must be used within a QrCodeScannerProvider",
  325. );
  326. }
  327. return context;
  328. };
  329. export default QrCodeScannerProvider;