import { inRuntime, bridgeAsync } from "../bridge"; import { isSupportWebScan, startScanForWeb, stopScanForWeb, isSupportImageScan, startScanForImage, unlockScanBeep, playScanBeep, isWebScanImageFallbackEnabled, canUseWebCameraScan, shouldSkipWebCameraProbe, cleanupWebScanResiduals, detectImageFileForScan } from "../web"; import { isSupportWxScan, startScanForWx, isWxEnv } from "../wx"; import { startScanner, stopScanner } from "../scanner"; import { getConfig } from "../config"; import { toAny } from "../../utils/toany"; import { printDebug, printWarn } from "../../utils/logger"; import { forwardEmbedScanResultIfNeeded, forwardEmbedScanErrorIfNeeded } from "../embedScanBridge"; let _scan_status = "ready"; let _scan_status_listener = null; let _scan_listener_list = []; let _scan_error_listener_list = []; let _scan_resolve = null; let _scan_closing = false; let _scan_next_start_time = 0; let _embed_scan_host_enabled = false; const SCAN_RESTART_DELAY = 500; const BRIDGE_SCAN_TIMEOUT = 5000; const SCAN_SESSION_TIMEOUT = 90000; function getScanRestartDelay() { return toAny(getConfig("scanRestartDelay"), SCAN_RESTART_DELAY); } function getBridgeScanTimeout() { const timeout = getConfig("bridgeScanTimeout"); return typeof timeout === "number" && timeout > 0 ? timeout : BRIDGE_SCAN_TIMEOUT; } function getScanSessionTimeout() { const timeout = getConfig("scanSessionTimeout"); return typeof timeout === "number" && timeout > 0 ? timeout : SCAN_SESSION_TIMEOUT; } function withScanSessionTimeout(scanPromise) { return Promise.race([ scanPromise, new Promise(resolve => { setTimeout(() => { printWarn("scan session timeout"); resolve({ cancel: 1, scanTimeout: true }); }, getScanSessionTimeout()); }) ]); } function __fallbackScanAfterBridgeFailure(err) { if (!isScanning()) { throw err; } printWarn("bridge scan unavailable, fallback:", err); return __startNonBridgeScan(err); } function __fallbackScanAfterWxFailure(err) { if (!isScanning()) { throw err; } printWarn("wx scan unavailable, fallback to web/image scan:", err); return __startNonBridgeScan(err); } function __startNonBridgeScan(err) { if (isSupportWebScan()) { return __startWebScan(); } if (isSupportImageScan()) { return __startImageScan(); } throw err; } function parseBarcodeString(input) { // 标准化的类型映射表:将各种变体映射到统一标识 // 这样即使传入 EAN_13、EAN-13、EAN13 都能匹配成功 const normalizedTypeMap = { // EAN 系列 "ean13": true, "ean-13": true, "ean_13": true, "ean8": true, "ean-8": true, "ean_8": true, // UPC 系列 "upca": true, "upc-a": true, "upc_a": true, "upce": true, "upc-e": true, "upc_e": true, // CODE 系列 "code128": true, "code-128": true, "code_128": true, "code39": true, "code-39": true, "code_39": true, "code93": true, "code-93": true, "code_93": true, // 其他常见类型 "itf": true, "itf14": true, "itf-14": true, "itf_14": true, "codabar": true, "pdf417": true, "pdf-417": true, "pdf_417": true, "qrcode": true, "qr-code": true, "qr_code": true, "datamatrix": true, "data-matrix": true, "data_matrix": true, "aztec": true }; // 健壮性检查 if (typeof input !== 'string') { return input; } // 按第一个逗号分割 const commaIndex = input.indexOf(','); if (commaIndex === -1) { return input; } const possibleType = input.substring(0, commaIndex).trim(); const value = input.substring(commaIndex + 1).trim(); // 类型或值为空,返回原值 if (possibleType === "" || value === "") { return input; } // 标准化类型(转小写,便于匹配) const normalizedType = possibleType.toLowerCase(); // 检查是否为已知的条形码类型(支持多种分隔符变体) const isKnownType = normalizedTypeMap.hasOwnProperty(normalizedType); if (isKnownType) { return value; } else { return input; } } function __checkScanner() { if (_scan_listener_list.length > 0 || _embed_scan_host_enabled) { startScanner((result) => { result = parseBarcodeString(result); __scannerResult(result, { source: "scanner", skipBeep: true }); }); } else { stopScanner(); } } function __match(result, match) { let reg = null; if (!!match && typeof match === "string") { reg = new RegExp(match); } if (!!reg) { return reg.test(result); } return true; } function __shouldSkipBeep(meta) { if (meta && meta.skipBeep) { return true; } const source = meta && meta.source; return source === "scanner" || source === "bridge" || source === "wx"; } function __result(result, meta) { result = parseBarcodeString(result); forwardEmbedScanResultIfNeeded(result, meta); let matched = false; for (let i = 0; i < _scan_listener_list.length; i++) { const item = _scan_listener_list[i]; if (item.listener && __match(result, item.match)) { matched = true; item.listener({ result, key: item.key }); break; } } if (matched) { if (!__shouldSkipBeep(meta)) { playScanBeep(); } _scan_next_start_time = Date.now() + getScanRestartDelay(); } return matched; } function normalizeScanError(raw) { if (typeof raw === "string") { return raw; } if (raw && raw.error != null && raw.error !== "") { return String(raw.error); } if (raw && raw.message) { return String(raw.message); } if (raw == null || raw === "") { return ""; } return String(raw); } function __scanError(raw, meta) { const error = normalizeScanError(raw); if (!error) { return false; } forwardEmbedScanErrorIfNeeded(error); let matched = false; for (let i = 0; i < _scan_error_listener_list.length; i++) { const item = _scan_error_listener_list[i]; if (item.listener && __match(error, item.match)) { matched = true; item.listener(Object.assign({ error, key: item.key }, meta || {})); break; } } return matched; } function __notifyImageScanFailure(raw, meta) { if (raw && raw.cancel) { return false; } const error = normalizeScanError(raw); const notified = __scanError(raw, Object.assign({ source: "image" }, meta || {})); if (!notified && error) { printWarn("image scan failed:", error); } return notified; } function __notifyWebScanFailure(raw, meta) { if (raw && raw.cancel) { return false; } const payload = raw && raw.error != null ? raw.error : raw; const error = normalizeScanError(payload); const notified = __scanError(payload, Object.assign({ source: "web" }, meta || {})); if (!notified && error) { printWarn("web scan failed:", error); } return notified; } function __hasMatchedListener(result) { for (let i = 0; i < _scan_listener_list.length; i++) { const item = _scan_listener_list[i]; if (item.listener && __match(result, item.match)) { return true; } } return false; } function __scanning() { if (_scan_status !== "scanning") { _scan_status = "scanning"; if (_scan_status_listener) { _scan_status_listener({ status: "scanning" }); } } } function __closed() { if (_scan_status !== "ready") { _scan_status = "ready"; if (_scan_status_listener) { _scan_status_listener({ status: "ready" }); } } } function __finishScan() { const resolve = _scan_resolve; _scan_resolve = null; __closed(); return resolve; } function __stopCurrentScan() { if (inRuntime()) { return bridgeAsync("stopScan").catch(() => { // no thing }); } else if (isSupportWebScan()) { return stopScanForWeb().catch(() => { // no thing }); } return Promise.resolve(); } /** * 父页通过 postMessage 将识别结果投递到嵌入 iframe 时调用(与本地扫码枪/监听同一链路)。 * @returns {boolean} 是否有监听消费了该结果 */ export function dispatchEmbedScanResult(raw, meta) { const result = typeof raw === "string" ? parseBarcodeString(raw) : raw; return __scannerResult(result, meta); } /** * 父页通过 postMessage 将识别错误投递到嵌入 iframe 时调用。 * @returns {boolean} 是否有监听消费了该错误 */ export function dispatchEmbedScanError(raw) { const error = normalizeScanError(raw); return __scanError(error, { source: "image" }); } /** * 嵌入 iframe 已消费扫码结果时通知父页结束当前识别(关闭摄像头/UI 等)。 */ export function acknowledgeEmbedScanConsumed(raw) { const result = typeof raw === "string" ? parseBarcodeString(raw) : raw; if (!isScanning()) { return; } const resolve = __finishScan(); _scan_closing = true; _scan_next_start_time = Date.now() + getScanRestartDelay(); __stopCurrentScan().then(() => { setTimeout(() => { _scan_closing = false; }, 0); }); resolve && resolve({ result }); } function __scannerResult(result, meta) { if (!__hasMatchedListener(result)) { return __result(result, meta); } if (isScanning()) { const resolve = __finishScan(); _scan_closing = true; __stopCurrentScan().then(() => { setTimeout(() => { _scan_closing = false; }, 0); }); const matched = __result(result, meta); resolve && resolve({ result }); return matched; } return __result(result, meta); } function __startBridgeScan() { return bridgeAsync("startScan", { closeable: true }, getBridgeScanTimeout()).then(resp => { if (!isScanning()) { return resp; } if (!resp || !resp.result) { return resp; } if (__result(resp.result, { source: "bridge" })) { return resp; } if (isScanning()) { return __startBridgeScan(); } return resp; }).catch(err => { if (!isScanning()) { throw err; } if (!err || !err.result) { throw err; } if (__result(err.result, { source: "bridge" })) { return err; } if (isScanning()) { return __startBridgeScan(); } return err; }); } function __startWxScan() { return startScanForWx({ needResult: 1, scanType: ["qrCode", "barCode"] }).then(resp => { if (!isScanning()) { return resp; } if (resp && resp.error && !resp.result) { throw resp.error; } if (!resp || !resp.result) { return resp; } if (__result(resp.result, { source: "wx" })) { return resp; } return resp; }).catch(err => { if (!isScanning()) { throw err; } if (err && err.result) { if (__result(err.result, { source: "wx" })) { return err; } return err; } return __fallbackScanAfterWxFailure(err); }); } function __startWebScan(useImageScan = false) { return startScanForWeb(__result, __scanError).then(resp => { if (!isScanning()) { return resp; } if (!resp || !resp.result) { if (resp && resp.success === false) { __notifyWebScanFailure(resp); } return resp; } __result(resp.result); return resp; }).catch(err => { if (!isScanning()) { return err; } if (err && err.cancel) { return err; } if (err && err.scanTimeout) { return err; } if (err && err.imageFallbackUsed) { __notifyWebScanFailure(err); return err; } if (isWebScanImageFallbackEnabled() && useImageScan) { return __startImageScan(); } __notifyWebScanFailure(err); printWarn("web scan failed:", err); return err; }); } function __startImageScan() { return startScanForImage().then(resp => { if (!isScanning()) { return resp; } if (resp && resp.cancel) { return resp; } if (!resp || !resp.result) { __notifyImageScanFailure(resp); return resp; } __result(resp.result); return resp; }).catch(err => { if (!isScanning()) { return err; } if (err && err.cancel) { return err; } if (err && err.result) { __result(err.result); return err; } __notifyImageScanFailure(err); return err; }); } export function isScanning() { return _scan_status === "scanning"; } export function setEmbedScanHostEnabled(enabled) { const nextEnabled = enabled !== false; if (_embed_scan_host_enabled === nextEnabled) { return; } _embed_scan_host_enabled = nextEnabled; __checkScanner(); } export function clear() { for (let i = 0; i < _scan_listener_list.length; i++) { const item = _scan_listener_list[i]; item.cancel(); } _scan_listener_list.length = 0; for (let i = 0; i < _scan_error_listener_list.length; i++) { const item = _scan_error_listener_list[i]; item.cancel(); } _scan_error_listener_list.length = 0; __checkScanner(); } export function onScanListener(listener, key, match, level) { if (!key || typeof key !== 'string') { return; } if (typeof listener !== 'function') { return; } let item = null; for (let i = 0; i < _scan_listener_list.length; i++) { const _i = _scan_listener_list[i]; if (_i.key === key) { item = _i; break; } } if (item) { item.level = level; item.match = match; item.listener = listener; } else { item = { key, match: match || "", level: level || 0, listener: listener, cancel: () => { const index = _scan_listener_list.indexOf(item); if (index !== -1) { const items = _scan_listener_list.splice(index, 1); for (let i = 0; i < items.length; i++) { const item = items[i]; item.listener && item.listener({ cancel: 1 }); } } } }; _scan_listener_list.push(item); } // 根据level排序 _scan_listener_list.sort((a, b) => b.level - a.level); __checkScanner(); return item; } export function offScanListener(listener) { for (let i = 0; i < _scan_listener_list.length; i++) { const _i = _scan_listener_list[i]; if (typeof listener === 'string') { if (_i.key === listener) { _i.cancel(); break; } } else if (_i.listener === listener) { _i.cancel(); break; } } __checkScanner(); } function createScanErrorListenerItem(listener, key, match, level) { const item = { key, match: match || "", level: level || 0, listener: listener, cancel: () => { const index = _scan_error_listener_list.indexOf(item); if (index !== -1) { const items = _scan_error_listener_list.splice(index, 1); for (let i = 0; i < items.length; i++) { const removed = items[i]; removed.listener && removed.listener({ cancel: 1, key: removed.key }); } } } }; return item; } export function onScanErrorListener(listener, key, match, level) { if (!key || typeof key !== 'string') { return; } if (typeof listener !== 'function') { return; } let item = null; for (let i = 0; i < _scan_error_listener_list.length; i++) { const _i = _scan_error_listener_list[i]; if (_i.key === key) { item = _i; break; } } if (item) { item.level = level; item.match = match; item.listener = listener; } else { item = createScanErrorListenerItem(listener, key, match, level); _scan_error_listener_list.push(item); } _scan_error_listener_list.sort((a, b) => b.level - a.level); return item; } export function offScanErrorListener(listener) { for (let i = 0; i < _scan_error_listener_list.length; i++) { const _i = _scan_error_listener_list[i]; if (typeof listener === 'string') { if (_i.key === listener) { _i.cancel(); break; } } else if (_i.listener === listener) { _i.cancel(); break; } } } export function setStatusListener(listener) { if (typeof listener !== 'function') { return; } _scan_status_listener = listener; } export function getStatus() { return _scan_status; } export function stopScan() { cleanupWebScanResiduals(); if (!isScanning()) { return; } const resolve = __finishScan(); _scan_closing = true; __stopCurrentScan().then(() => { resolve && resolve({ cancel: 1 }); _scan_closing = false; }); } export function startScan() { if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) { return; } unlockScanBeep(); Promise.resolve().then(() => { __scanning(); let scannerPromise = new Promise(resolve => { _scan_resolve = resolve; }); let scanPromise = Promise.resolve(); if (inRuntime()) { scanPromise = __startBridgeScan().catch(__fallbackScanAfterBridgeFailure); } else if (isSupportWxScan()) { scanPromise = __startWxScan(); } else if (isSupportWebScan()) { scanPromise = __startWebScan(true); } else if (isSupportImageScan()) { scanPromise = __startImageScan(); } else { printWarn("Not support scanner"); } return withScanSessionTimeout(Promise.race([scanPromise, scannerPromise])); }).finally(() => { _scan_resolve = null; __closed(); }); } export function scanImage() { if (!isSupportImageScan()) { printDebug("Not support image scanner"); return; } if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) { return; } unlockScanBeep(); __scanning(); withScanSessionTimeout(__startImageScan()).catch(err => { if (err && err.cancel) { return; } __notifyImageScanFailure(err); }).finally(() => { __closed(); }); } /** 由原生/业务传入已选图片 File,避免 WebView 无法从 input.files 取文件 */ export function scanImageFromFile(file) { if (!isSupportImageScan()) { printDebug("Not support image scanner"); return; } if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) { return; } unlockScanBeep(); cleanupWebScanResiduals(); __scanning(); withScanSessionTimeout( detectImageFileForScan(file).then(resp => { if (!isScanning()) { return resp; } if (resp && resp.cancel) { return resp; } if (!resp || !resp.result) { __notifyImageScanFailure(resp); return resp; } __result(resp.result); return resp; }) ).catch(err => { if (err && err.cancel) { return; } __notifyImageScanFailure(err); }).finally(() => { __closed(); }); } export const supportList = [ { name: "bridge", get support() { return !!inRuntime(); } }, { name: "wx", get support() { return !!isSupportWxScan(); } }, { name: "web", get support() { if (shouldSkipWebCameraProbe()) { return false; } return !!canUseWebCameraScan(); } }, { name: "image", get support() { return !!isSupportImageScan(); } }, { name: "scanner", support: true } ];