SDK开发完成

This commit is contained in:
iqudoo
2026-04-30 16:36:31 +08:00
parent 21ae621c6e
commit 0acd16e0a5
9 changed files with 189 additions and 53 deletions

File diff suppressed because one or more lines are too long

View File

@@ -180,7 +180,7 @@
<body> <body>
<main class="page"> <main class="page">
<section class="hero"> <section class="hero">
<h1>IScan 扫码 SDK Demo</h1> <h1>IScan 通用扫码 SDK</h1>
<p>统一接入桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p> <p>统一接入桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p>
</section> </section>
@@ -188,11 +188,9 @@
<div class="card"> <div class="card">
<h2>功能说明</h2> <h2>功能说明</h2>
<ul class="feature-list"> <ul class="feature-list">
<li>桥接环境优先调用 App 原生扫码</li> <li>支持桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入</li>
<li>微信环境支持初始化 JSSDK 并调用 scanQRCode</li> <li>浏览器环境使用 ZXing的wasm库识别二维码和条形码</li>
<li>浏览器环境使用 BarcodeDetector 识别二维码和条形码</li> <li>扫码结果又监听规则统一回调</li>
<li>摄像头不可用时可选择图片识别。</li>
<li>支持扫码枪快速输入,并与扫码监听规则统一回调。</li>
</ul> </ul>
</div> </div>
@@ -209,20 +207,21 @@
<p>点击开始后会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p> <p>点击开始后会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p>
<div class="actions"> <div class="actions">
<button onclick="startScan()" class="btn">开始扫码</button> <button onclick="startScan()" class="btn">开始扫码</button>
<button onclick="scanVideo()" class="btn secondary">开启视频扫码</button>
<button onclick="scanImage()" class="btn secondary">选择图片识别</button> <button onclick="scanImage()" class="btn secondary">选择图片识别</button>
<button onclick="stopScan()" class="btn secondary">停止扫码</button> <button onclick="stopScan()" class="btn secondary">停止扫码</button>
</div> </div>
</section> </section>
<section class="grid"> <section class="card">
<div class="card">
<h2>扫码结果</h2> <h2>扫码结果</h2>
<pre id="result" class="panel result"></pre> <pre id="result" class="panel result"></pre>
</div> </section>
<div class="card">
<section class="card">
<h2>错误信息</h2> <h2>错误信息</h2>
<pre id="error" class="panel error"></pre> <pre id="error" class="panel error"></pre>
</div> <p style="color: #919191">错误信息可能来源于扫码结果、扫码过程、扫码初始化等。</p>
</section> </section>
<section class="card code"> <section class="card code">
@@ -234,10 +233,11 @@
apiUrl: "https://your-domain.com/wechat/jssdk-config" apiUrl: "https://your-domain.com/wechat/jssdk-config"
} }
}).then(function () { }).then(function () {
// 监听扫码状态
IScan.setStatusListener(function () { IScan.setStatusListener(function () {
console.log("status:", IScan.getStatus()); console.log("status:", IScan.getStatus());
}); });
// 监听扫码结果
IScan.onScanListener(function (res) { IScan.onScanListener(function (res) {
console.log("scan result:", res); console.log("scan result:", res);
}, "scan", null, 100); }, "scan", null, 100);
@@ -348,6 +348,11 @@ IScan.stopScan();</pre>
IScan.scanImage(); IScan.scanImage();
} }
function scanVideo() {
hide();
IScan.scanVideo();
}
ready(); ready();
</script> </script>
</body> </body>

20
dist/index.d.ts vendored
View File

@@ -3,17 +3,25 @@
*/ */
interface ScanConfigOptions { interface ScanConfigOptions {
/** /**
* 网页扫码canvas样式 * 网页扫码canvas是否启用,默认启用
*/ */
webCanvasStyle?: string, webCanvasEnabled?: boolean,
/**
* 网页扫码canvas样式默认position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;
*/
webScanCanvasStyle?: string,
/** /**
* 网页扫码类型,默认支持二维码和条码 * 网页扫码类型,默认支持二维码和条码
*/ */
webScanType?: ('qrCode' | 'barCode')[], webScanType?: ('qrCode' | 'barCode')[],
/** /**
* 网页扫码canvas是否启用默认启用 * 网页扫码视频是否镜像,默认自动判断:前置/PC镜像后置不镜像
*/ */
webCanvasEnabled?: boolean, webScanVideoMirror?: boolean,
/**
* 网页扫码视频是否垂直镜像,默认不镜像
*/
webScanVideoMirrorVertical?: boolean,
/** /**
* 网页扫码成功提示音地址,默认使用内置提示音 * 网页扫码成功提示音地址,默认使用内置提示音
*/ */
@@ -153,6 +161,10 @@ interface IScan {
* 开启扫码 * 开启扫码
*/ */
startScan(): void; startScan(): void;
/**
* 开启视频扫码
*/
scanVideo(): void;
/** /**
* 选择图片进行识别 * 选择图片进行识别
*/ */

10
dist/index.html vendored
View File

@@ -161,17 +161,18 @@
section { section {
margin-bottom: 16px; margin-bottom: 16px;
}</style></head><body><main class="page"><section class="hero"><h1>IScan 扫码 SDK Demo</h1><p>统一接入桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p></section><section class="grid"><div class="card"><h2>功能说明</h2><ul class="feature-list"><li>桥接环境优先调用 App 原生扫码。</li><li>微信环境支持初始化 JSSDK 并调用 scanQRCode</li><li>浏览器环境使用 BarcodeDetector 识别二维码和条形码。</li><li>摄像头不可用时可选择图片识别。</li><li>支持扫码枪快速输入,并与扫码监听规则统一回调。</li></ul></div><div class="card"><h2>当前状态</h2><p>SDK 状态:<span id="status" class="status">loading</span></p><p>运行环境:</p><pre id="output" class="panel"></pre></div></section><section class="card"><h2>操作</h2><p>点击开始后会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p><div class="actions"><button onclick="startScan()" class="btn">开始扫码</button> <button onclick="scanImage()" class="btn secondary">选择图片识别</button> <button onclick="stopScan()" class="btn secondary">停止扫码</button></div></section><section class="grid"><div class="card"><h2>扫码结果</h2><pre id="result" class="panel result"></pre></div><div class="card"><h2>错误信息</h2><pre id="error" class="panel error"></pre></div></section><section class="card code"><h2>接入方式</h2><pre>IScan.config({ }</style></head><body><main class="page"><section class="hero"><h1>IScan 通用扫码 SDK</h1><p>统一接入桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入。</p></section><section class="grid"><div class="card"><h2>功能说明</h2><ul class="feature-list"><li>支持桥接扫码、微信 JSSDK 扫码、Web 摄像头扫码、图片识别和扫码枪输入</li><li>浏览器环境使用 ZXing的wasm库识别二维码和条形码。</li><li>扫码结果又监听规则统一回调。</li></ul></div><div class="card"><h2>当前状态</h2><p>SDK 状态:<span id="status" class="status">loading</span></p><p>运行环境:</p><pre id="output" class="panel"></pre></div></section><section class="card"><h2>操作</h2><p>点击开始后会按桥接、微信、Web 摄像头、图片识别的顺序选择可用扫码方式。</p><div class="actions"><button onclick="startScan()" class="btn">开始扫码</button> <button onclick="scanVideo()" class="btn secondary">开启视频扫码</button> <button onclick="scanImage()" class="btn secondary">选择图片识别</button> <button onclick="stopScan()" class="btn secondary">停止扫码</button></div></section><section class="card"><h2>扫码结果</h2><pre id="result" class="panel result"></pre></section><section class="card"><h2>错误信息</h2><pre id="error" class="panel error"></pre><p style="color: #919191">错误信息可能来源于扫码结果、扫码过程、扫码初始化等。</p></section><section class="card code"><h2>接入方式</h2><pre>IScan.config({
webCanvasEnabled: true, webCanvasEnabled: true,
webScanBeepEnabled: true, webScanBeepEnabled: true,
initWechatJssdk: { initWechatJssdk: {
apiUrl: "https://your-domain.com/wechat/jssdk-config" apiUrl: "https://your-domain.com/wechat/jssdk-config"
} }
}).then(function () { }).then(function () {
// 监听扫码状态
IScan.setStatusListener(function () { IScan.setStatusListener(function () {
console.log("status:", IScan.getStatus()); console.log("status:", IScan.getStatus());
}); });
// 监听扫码结果
IScan.onScanListener(function (res) { IScan.onScanListener(function (res) {
console.log("scan result:", res); console.log("scan result:", res);
}, "scan", null, 100); }, "scan", null, 100);
@@ -277,4 +278,9 @@ IScan.stopScan();</pre></section></main><script>(function () {
IScan.scanImage(); IScan.scanImage();
} }
function scanVideo() {
hide();
IScan.scanVideo();
}
ready();</script><script src="index.js"></script></body></html> ready();</script><script src="index.js"></script></body></html>

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
import './polyfill'; import './polyfill';
import { setConfig, getVersion } from './services/config'; import { setConfig, getVersion } from './services/config';
import { onScanListener, offScanListener, setStatusListener, getStatus, startScan, stopScan, scanImage, clear } from './services/provider/scan'; import { onScanListener, offScanListener, setStatusListener, getStatus,
startScan, stopScan, scanVideo, scanImage, clear } from './services/provider/scan';
import { initWxJssdk } from './services/wx'; import { initWxJssdk } from './services/wx';
import { printDebug } from './utils/logger'; import { printDebug } from './utils/logger';
@@ -41,6 +42,7 @@ export default Object.assign({}, {
getStatus, getStatus,
startScan, startScan,
stopScan, stopScan,
scanVideo,
scanImage, scanImage,
clear, clear,
}); });

View File

@@ -258,7 +258,7 @@ export function onScanListener(listener, key, match, level) {
key, key,
match: match || "", match: match || "",
level: level || 0, level: level || 0,
listener: listener , listener: listener,
cancel: () => { cancel: () => {
const index = _scan_listener_list.indexOf(item); const index = _scan_listener_list.indexOf(item);
if (index !== -1) { if (index !== -1) {
@@ -332,12 +332,12 @@ export function startScan() {
scanPromise = __startWxScan(); scanPromise = __startWxScan();
} else if (isSupportWebScan()) { } else if (isSupportWebScan()) {
console.log("startScanForWeb"); console.log("startScanForWeb");
scanPromise = startScanForWeb(getConfig("webCanvasStyle"), __result); scanPromise = startScanForWeb(getConfig("webScanCanvasStyle"), __result);
} else if (isSupportImageScan()) { } else if (isSupportImageScan()) {
console.log("startScanForImage"); console.log("startScanForImage");
scanPromise = __startImageScan(); scanPromise = __startImageScan();
} else { } else {
console.log("not support scanner"); console.log("Not support scanner");
} }
return Promise.race([scanPromise, scannerPromise]); return Promise.race([scanPromise, scannerPromise]);
}).finally(() => { }).finally(() => {
@@ -346,16 +346,38 @@ export function startScan() {
}); });
} }
export function scanImage() { export function scanVideo() {
if (!isSupportImageScan()) { if (!isSupportWebScan()) {
console.log("not support image scanner"); console.log("Not support video scanner");
return; return;
} }
startScanForImage().then(resp => { Promise.resolve().then(() => {
__scanning();
return startScanForWeb(getConfig("webScanCanvasStyle"), __result).then(resp => {
if (resp && resp.result) { if (resp && resp.result) {
__result(resp.result); __result(resp.result);
} }
}).catch(err => { throw resp.error;
console.log("scan image error", err); }).catch(err => { });
}).finally(() => {
__closed();
});
}
export function scanImage() {
if (!isSupportImageScan()) {
console.log("Not support image scanner");
return;
}
Promise.resolve().then(() => {
__scanning();
return startScanForImage().then(resp => {
if (resp && resp.result) {
__result(resp.result);
}
throw resp.error;
}).catch(err => { });
}).finally(() => {
__closed();
}); });
} }

View File

@@ -32,15 +32,35 @@ function createEl(tagName, id, style, appendChild) {
return el; return el;
} }
function canvasDrawLine(context, width, begin, end, color) { function transformPoint(point, width, height, mirrorHorizontal, mirrorVertical, cover) {
let x = point.x;
let y = point.y;
if (cover) {
x = cover.x + point.x * cover.scale;
y = cover.y + point.y * cover.scale;
}
return {
x: mirrorHorizontal ? width - x : x,
y: mirrorVertical ? height - y : y
};
}
function canvasDrawLine(context, width, height, begin, end, color, mirrorHorizontal, mirrorVertical, cover) {
const beginPoint = transformPoint(begin, width, height, mirrorHorizontal, mirrorVertical, cover);
const endPoint = transformPoint(end, width, height, mirrorHorizontal, mirrorVertical, cover);
context.beginPath(); context.beginPath();
context.moveTo(width - begin.x, begin.y); context.moveTo(beginPoint.x, beginPoint.y);
context.lineTo(width - end.x, end.y); context.lineTo(endPoint.x, endPoint.y);
context.lineWidth = 4; context.lineWidth = 4;
context.strokeStyle = color; context.strokeStyle = color;
context.stroke(); context.stroke();
} }
function isMobile() {
return typeof navigator !== 'undefined'
&& /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
}
function getBarcodeFormats(scanType) { function getBarcodeFormats(scanType) {
let formats = []; let formats = [];
if (!scanType) { if (!scanType) {
@@ -114,11 +134,57 @@ function createBarcodeDetector(scanType) {
}); });
} }
function drawBarcode(context, width, barcode) { function shouldMirrorWebVideo(stream) {
const webScanVideoMirror = getConfig("webScanVideoMirror");
if (typeof webScanVideoMirror === "boolean") {
return webScanVideoMirror;
}
try {
const track = stream && stream.getVideoTracks && stream.getVideoTracks()[0];
const settings = track && track.getSettings && track.getSettings();
if (settings && settings.facingMode === "environment") {
return false;
}
if (settings && settings.facingMode === "user") {
return true;
}
} catch (e) {
}
return !isMobile();
}
function shouldMirrorWebVideoVertical() {
return getConfig("webScanVideoMirrorVertical") === true;
}
function getCanvasDisplaySize(canvasEl, fallbackWidth, fallbackHeight) {
const rect = canvasEl.getBoundingClientRect();
const width = rect.width || parseFloat(canvasEl.style.width) || fallbackWidth;
const height = rect.height || parseFloat(canvasEl.style.height) || fallbackHeight;
return {
width: Math.max(1, Math.round(width)),
height: Math.max(1, Math.round(height))
};
}
function getCoverDrawOptions(sourceWidth, sourceHeight, targetWidth, targetHeight) {
const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
const width = sourceWidth * scale;
const height = sourceHeight * scale;
return {
scale,
width,
height,
x: (targetWidth - width) / 2,
y: (targetHeight - height) / 2
};
}
function drawBarcode(context, width, height, barcode, mirrorHorizontal, mirrorVertical, cover) {
const cornerPoints = barcode.cornerPoints; const cornerPoints = barcode.cornerPoints;
if (cornerPoints && cornerPoints.length) { if (cornerPoints && cornerPoints.length) {
for (let i = 0; i < cornerPoints.length; i++) { for (let i = 0; i < cornerPoints.length; i++) {
canvasDrawLine(context, width, cornerPoints[i], cornerPoints[(i + 1) % cornerPoints.length], "#FF3B58"); canvasDrawLine(context, width, height, cornerPoints[i], cornerPoints[(i + 1) % cornerPoints.length], "#FF3B58", mirrorHorizontal, mirrorVertical, cover);
} }
return; return;
} }
@@ -131,7 +197,7 @@ function drawBarcode(context, width, barcode) {
{ x: rect.x, y: rect.y + rect.height } { x: rect.x, y: rect.y + rect.height }
]; ];
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
canvasDrawLine(context, width, points[i], points[(i + 1) % points.length], "#FF3B58"); canvasDrawLine(context, width, height, points[i], points[(i + 1) % points.length], "#FF3B58", mirrorHorizontal, mirrorVertical, cover);
} }
} }
} }
@@ -261,13 +327,14 @@ export function startScanForWeb(canvasStyle, onResult) {
"display: none", false); "display: none", false);
let canvasEnabled = getConfig("webCanvasEnabled") !== false; let canvasEnabled = getConfig("webCanvasEnabled") !== false;
let canvasDisplay = ""; let canvasDisplay = "";
let canvasBaseStyle = canvasStyle || "position: fixed; width: 300px; height: 240px; top: 0; left: 0; z-index: 9999;"; let canvasBaseStyle = canvasStyle || "position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;";
let canvasEl = createEl("canvas", let canvasEl = createEl("canvas",
"__webscan_canvas__", "__webscan_canvas__",
canvasBaseStyle + " display: none;", true); canvasBaseStyle + " display: none;", true);
canvasDisplay = canvasEl.style.display; canvasDisplay = canvasEl.style.display;
canvasEl.style.cssText = canvasBaseStyle; canvasEl.style.cssText = canvasBaseStyle;
canvasDisplay = canvasEl.style.display; canvasDisplay = canvasEl.style.display;
let canvasDisplaySize = getCanvasDisplaySize(canvasEl, 300, 240);
canvasEl.style.display = "none"; canvasEl.style.display = "none";
let context = canvasEl.getContext("2d"); let context = canvasEl.getContext("2d");
let currentUuid = scanWeb.uuid; let currentUuid = scanWeb.uuid;
@@ -288,6 +355,8 @@ export function startScanForWeb(canvasStyle, onResult) {
}).then(function (options) { }).then(function (options) {
const detector = options.detector; const detector = options.detector;
const stream = options.stream; const stream = options.stream;
const mirrorVideo = shouldMirrorWebVideo(stream);
const mirrorVideoVertical = shouldMirrorWebVideoVertical();
videoEl.srcObject = stream; videoEl.srcObject = stream;
videoEl.setAttribute("playsinline", true); // iOS使用 videoEl.setAttribute("playsinline", true); // iOS使用
videoEl.play(); videoEl.play();
@@ -307,10 +376,18 @@ export function startScanForWeb(canvasStyle, onResult) {
let tick = () => { let tick = () => {
try { try {
if (videoEl.readyState === videoEl.HAVE_ENOUGH_DATA && !detecting) { if (videoEl.readyState === videoEl.HAVE_ENOUGH_DATA && !detecting) {
canvasEl.height = videoEl.videoHeight; canvasEl.width = canvasDisplaySize.width;
canvasEl.width = videoEl.videoWidth; canvasEl.height = canvasDisplaySize.height;
context.setTransform(-1, 0, 0, 1, canvasEl.width, 0); const cover = getCoverDrawOptions(videoEl.videoWidth, videoEl.videoHeight, canvasEl.width, canvasEl.height);
context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height); context.setTransform(
mirrorVideo ? -1 : 1,
0,
0,
mirrorVideoVertical ? -1 : 1,
mirrorVideo ? canvasEl.width : 0,
mirrorVideoVertical ? canvasEl.height : 0
);
context.drawImage(videoEl, cover.x, cover.y, cover.width, cover.height);
context.setTransform(1, 0, 0, 1, 0, 0); context.setTransform(1, 0, 0, 1, 0, 0);
if (canvasEnabled && !displayed) { if (canvasEnabled && !displayed) {
displayed = true; displayed = true;
@@ -323,7 +400,7 @@ export function startScanForWeb(canvasStyle, onResult) {
if (!onResult || !onResult(code.rawValue)) { if (!onResult || !onResult(code.rawValue)) {
return; return;
} }
drawBarcode(context, canvasEl.width, code); drawBarcode(context, canvasEl.width, canvasEl.height, code, mirrorVideo, mirrorVideoVertical, cover);
playScanBeep(); playScanBeep();
scanWeb.uuid = null; scanWeb.uuid = null;
scanWeb.finish = true; scanWeb.finish = true;

20
types/index.d.ts vendored
View File

@@ -3,17 +3,25 @@
*/ */
interface ScanConfigOptions { interface ScanConfigOptions {
/** /**
* 网页扫码canvas样式 * 网页扫码canvas是否启用,默认启用
*/ */
webCanvasStyle?: string, webCanvasEnabled?: boolean,
/**
* 网页扫码canvas样式默认position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;
*/
webScanCanvasStyle?: string,
/** /**
* 网页扫码类型,默认支持二维码和条码 * 网页扫码类型,默认支持二维码和条码
*/ */
webScanType?: ('qrCode' | 'barCode')[], webScanType?: ('qrCode' | 'barCode')[],
/** /**
* 网页扫码canvas是否启用默认启用 * 网页扫码视频是否镜像,默认自动判断:前置/PC镜像后置不镜像
*/ */
webCanvasEnabled?: boolean, webScanVideoMirror?: boolean,
/**
* 网页扫码视频是否垂直镜像,默认不镜像
*/
webScanVideoMirrorVertical?: boolean,
/** /**
* 网页扫码成功提示音地址,默认使用内置提示音 * 网页扫码成功提示音地址,默认使用内置提示音
*/ */
@@ -153,6 +161,10 @@ interface IScan {
* 开启扫码 * 开启扫码
*/ */
startScan(): void; startScan(): void;
/**
* 开启视频扫码
*/
scanVideo(): void;
/** /**
* 选择图片进行识别 * 选择图片进行识别
*/ */