Files
scan-code-jssdk/src/services/provider/scan.js
iqudoo b438364656 fix
2026-05-26 14:00:41 +08:00

781 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
];