@spopc/wechat-miniprogram 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -17
- package/dist/barcode.d.ts +3 -0
- package/dist/barcode.js +29 -0
- package/dist/browser.d.ts +16 -0
- package/dist/browser.js +117 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +19 -17
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +2 -1
- package/dist/scanner.d.ts +2 -0
- package/dist/scanner.js +175 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -8,19 +8,19 @@ H5 调用微信小程序原生能力的 TypeScript SDK。使用 URL 协议和一
|
|
|
8
8
|
npm install @spopc/wechat-miniprogram
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
包提供 ESM 和 TypeScript 类型声明,供浏览器 H5 项目使用;在客户端创建实例,SSR
|
|
11
|
+
包提供 ESM 和 TypeScript 类型声明,供浏览器 H5 项目使用;在客户端创建实例,SSR 项目需放在挂载后的客户端代码中。普通浏览器使用真实 Web 能力;微信能力需要小程序宿主实现本文的 URL 协议。仅安装 H5 SDK 不会自动添加原生小程序页面。
|
|
12
12
|
|
|
13
13
|
源码仓库内使用 npm workspace 开发,无需先发布到 npm。
|
|
14
14
|
|
|
15
15
|
## 快速接入
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
从 0.2.0 起,默认自动识别环境:小程序 WebView 使用原生桥接,普通浏览器使用真实浏览器 API,不再自动 mock。需要旧的模拟行为时显式设置 `mode: 'mock'`。真实小程序调用需要网页加载微信 JSSDK:`https://res.wx.qq.com/open/js/jweixin-1.6.0.js`;浏览器模式不需要它。每个页面创建一个 SDK 实例。
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import { createWechatBridge } from '@spopc/wechat-miniprogram'
|
|
21
21
|
|
|
22
22
|
const bridge = createWechatBridge({
|
|
23
|
-
mode: 'auto', // 默认值;也可强制 'native' 或 'mock'
|
|
23
|
+
mode: 'auto', // 默认值;也可强制 'native'、'browser' 或 'mock'
|
|
24
24
|
onResult({ result, delivery, source }) {
|
|
25
25
|
// 唯一业务结果入口:live 与文档重建后的 recovered 都经过这里。
|
|
26
26
|
// 若同时消费 Promise,请避免对同一调用重复执行业务副作用。
|
|
@@ -30,25 +30,63 @@ const bridge = createWechatBridge({
|
|
|
30
30
|
|
|
31
31
|
// 由 H5 上的用户操作触发;原生页自动执行,无需再点“执行”和“返回”。
|
|
32
32
|
const scan = await bridge.scanCode({ scanType: ['qrCode', 'barCode'] })
|
|
33
|
-
const position = await bridge.getLocation(
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const position = await bridge.getLocation()
|
|
34
|
+
// position.type 明确标识坐标系:浏览器默认 WGS84,小程序默认 GCJ-02。
|
|
35
|
+
if (await bridge.ready() === 'native') {
|
|
36
|
+
const { code } = await bridge.login()
|
|
37
|
+
// 将 code 提交给业务后端;不要把 AppSecret 放在 H5。
|
|
38
|
+
}
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
上述调用是顺序示例;根据实际按钮分别触发。页面卸载 / 组件卸载时调用 `bridge.dispose()`。它移除监听并拒绝当前 Promise,原生模式保留请求记录以支持文档重建。
|
|
39
42
|
|
|
40
|
-
##
|
|
43
|
+
## 自动识别
|
|
41
44
|
|
|
42
45
|
```ts
|
|
43
46
|
const bridge = createWechatBridge() // 等同于 { mode: 'auto' }
|
|
44
|
-
const environment = await bridge.ready() // 'native' | 'mock'
|
|
47
|
+
const environment = await bridge.ready() // 'native' | 'browser' | 'mock'
|
|
45
48
|
console.log(bridge.environment, bridge.isMock)
|
|
46
49
|
const location = await bridge.getLocation() // 调用本身也会等待识别,无需手动 ready
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
`auto` 结合小程序标记、User-Agent、`wx.miniProgram.getEnv` 和 `WeixinJSBridgeReady`
|
|
52
|
+
`auto` 结合小程序标记、User-Agent、`wx.miniProgram.getEnv` 和 `WeixinJSBridgeReady` 识别。普通浏览器使用 browser,即使网页加载了微信 JSSDK 也不会被误判为小程序。微信容器仍在初始化时,默认最多等待 2000 ms;可用 `environmentTimeoutMs` 调整为 100–30000 ms。无法确认时抛出 `ENVIRONMENT_TIMEOUT`,加载完成后可重试 `ready()` / 调用,不把未知环境当成模拟成功。
|
|
53
|
+
|
|
54
|
+
自动识别完成后,本实例固定使用选定模式。真实调用的权限拒绝、导航失败、超时等不会退回 mock。已识别为小程序但缺少 JSSDK 时抛出 `JSSDK_NOT_READY`。`mode: 'native'` 强制小程序调用,在普通浏览器报 `NOT_IN_MINIPROGRAM`;`mode: 'browser'` 强制 Web API;`mode: 'mock'` 强制模拟。`isInMiniProgram()` 检测实际环境,不会因强制模式返回假值。检测未完成时 `environment` 为 undefined,应等待 `ready()`。
|
|
55
|
+
|
|
56
|
+
## 浏览器真实能力
|
|
57
|
+
|
|
58
|
+
| 能力 | 默认浏览器实现 |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| getLocation | 请求定位权限,使用 Geolocation 获取 WGS84 真实坐标;成功、超时或销毁时停止监听 |
|
|
61
|
+
| scanCode | 弹出扫码窗口,摄像头或本地图片识别;`onlyFromCamera` 隐藏选图;支持二维码、常见条码、Data Matrix、PDF417 |
|
|
62
|
+
| openLocation | 在新窗口打开高德地图网页,支持显式 type: wgs84 / gcj02,默认 gcj02 |
|
|
63
|
+
| makePhoneCall | 发起 tel: 系统拨号请求 |
|
|
64
|
+
| login / checkSession / chooseAddress / requestPayment | 返回 UNSUPPORTED;可以接入自己的业务处理器 |
|
|
65
|
+
|
|
66
|
+
定位和扫码要求 HTTPS 或 localhost。浏览器地理坐标采用 [WGS84](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates),默认 `getLocation()` 返回 `type: 'wgs84'`;显式要求 gcj02 会返回 `UNSUPPORTED_COORDINATE_SYSTEM`,不会把 WGS84 假标成 GCJ-02。需要转换时通过业务适配器接入地图服务。小程序定位默认值保持 gcj02。
|
|
67
|
+
|
|
68
|
+
地图使用[高德 URI API](https://lbs.amap.com/api/uri-api/gettingstarted)。把浏览器定位用于地图时传入 `type: position.type`,地图会处理对应坐标;微信地图仅接受 GCJ-02。浏览器地图暂不使用 scale,地图和 tel: 的 `{ ok: true }` 仅代表已发起打开请求,不代表地图已加载或电话已接通。应由用户点击触发;无用户操作或窗口被拦截时返回 `USER_GESTURE_REQUIRED` / `POPUP_BLOCKED`。
|
|
69
|
+
|
|
70
|
+
扫码使用本地 canvas 与按需加载的 `@zxing/library` 解码,不上传摄像头画面或图片。图片上限 20 MB。用户可点击取消或按 Escape;成功、取消、超时、销毁后会停止摄像头并移除窗口,即使权限响应迟到也会释放媒体流。浏览器模式的 `source` / `trigger` 为 browser、`delivery` 为 live,不消费 URL 或原生 pending 记录,也不模拟 Query 重载。
|
|
71
|
+
|
|
72
|
+
业务适配器具有类型关联,并收到可响应超时/销毁的 signal。例如选择业务收货地址:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const bridge = createWechatBridge({
|
|
76
|
+
browser: {
|
|
77
|
+
handlers: {
|
|
78
|
+
chooseAddress: async (_params, { signal }) => {
|
|
79
|
+
// 应用实现此函数,返回 AddressData;取消可抛 BridgeError('CANCELLED', '用户取消')。
|
|
80
|
+
return await openYourAddressPicker({ signal })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
仅配置 mock.handlers 不会启用模拟。浏览器登录处理器不能凭空产生有效的微信小程序 code;H5 登录与支付需接入业务后端和相应平台流程。自定义处理器仍遵守现有参数与结果类型,不能用业务 token 冒充微信 code。
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
## 显式 Mock
|
|
52
90
|
|
|
53
91
|
8 项能力均内置成功数据:固定模拟坐标、门店码、无效的 MOCK 登录 code、虚构地址及成功标记。可覆盖任意方法,其余保持默认:
|
|
54
92
|
|
|
@@ -56,7 +94,7 @@ const location = await bridge.getLocation() // 调用本身也会等待识别,
|
|
|
56
94
|
import { createWechatBridge, BridgeError } from '@spopc/wechat-miniprogram'
|
|
57
95
|
|
|
58
96
|
const bridge = createWechatBridge({
|
|
59
|
-
mode: '
|
|
97
|
+
mode: 'mock',
|
|
60
98
|
mock: {
|
|
61
99
|
delayMs: 500, // 默认 300;0–600000 ms,计入调用超时
|
|
62
100
|
handlers: {
|
|
@@ -79,7 +117,7 @@ mock 与真实调用共用参数/结果校验、Promise 类型、单次并发限
|
|
|
79
117
|
|
|
80
118
|
mock 的 `onResult.source`、`trigger` 均为 `mock`,`delivery` 固定为 `live`;只在当前内存中返回数据,不跳页面、不读写原生 pending 记录、不消费或修改 URL。即使传入调用参数 `mode: 'query'`,也不模拟重载,因此不能用来验证 WebView 的 Hash / Query 传输和文档重建。模拟登录 code 不能交给微信换登录态,模拟支付不会产生订单或真实支付;业务后端联调需配套测试数据。
|
|
81
119
|
|
|
82
|
-
测试页直接打开 `http://127.0.0.1:13334/`
|
|
120
|
+
测试页直接打开 `http://127.0.0.1:13334/` 自动使用浏览器能力。`?mode=browser`、`?mode=native`、`?mode=mock` 强制模式,mock 下可选成功/取消/失败,旧 `?mock=1` 仍可用。这些 URL 开关只属于测试页面,SDK 本身不读取它们。
|
|
83
121
|
|
|
84
122
|
## 方法与参数
|
|
85
123
|
|
|
@@ -87,11 +125,11 @@ mock 的 `onResult.source`、`trigger` 均为 `mock`,`delivery` 固定为 `liv
|
|
|
87
125
|
|
|
88
126
|
| 方法 | 原生参数 | 成功结果 |
|
|
89
127
|
| --- | --- | --- |
|
|
90
|
-
| `getLocation(options?)` | type: gcj02
|
|
128
|
+
| `getLocation(options?)` | type: 小程序/mock 默认 gcj02;browser 默认且仅内置支持 wgs84 | latitude、longitude、accuracy、type |
|
|
91
129
|
| `scanCode(options?)` | onlyFromCamera?: boolean;scanType?: qrCode / barCode / datamatrix / pdf417 数组 | result、scanType;可选 charSet、path、rawData |
|
|
92
130
|
| `login(options?)` | timeout?: 1000–60000,微信接口自身超时 | code |
|
|
93
131
|
| `checkSession(options?)` | 无 | valid: true |
|
|
94
|
-
| `openLocation(options)` |
|
|
132
|
+
| `openLocation(options)` | latitude、longitude;type 默认 gcj02,浏览器也支持 wgs84;可选 name、address、scale(5–18) | ok: true |
|
|
95
133
|
| `chooseAddress(options?)` | 无 | userName、telNumber、provinceName、cityName、countyName、detailInfo,以及平台返回的可选地址字段 |
|
|
96
134
|
| `makePhoneCall(options)` | phoneNumber | ok: true |
|
|
97
135
|
| `requestPayment(options)` | timeStamp、nonceStr、package、signType、paySign | ok: true |
|
|
@@ -118,8 +156,8 @@ await bridge.openLocation({ latitude: 31.2, longitude: 121.4, name: '门店', sc
|
|
|
118
156
|
status: 'success', data // 或 status: 'cancel' | 'error', error: { code, message }
|
|
119
157
|
},
|
|
120
158
|
delivery: 'live', // 或 'recovered'
|
|
121
|
-
source: 'native', // 或 'mock'
|
|
122
|
-
trigger: 'hashchange' // 或 startup / pageshow / visible / mock
|
|
159
|
+
source: 'native', // 或 'browser' / 'mock'
|
|
160
|
+
trigger: 'hashchange' // 或 startup / pageshow / visible / browser / mock
|
|
123
161
|
}
|
|
124
162
|
```
|
|
125
163
|
|
|
@@ -127,6 +165,8 @@ await bridge.openLocation({ latitude: 31.2, longitude: 121.4, name: '门店', sc
|
|
|
127
165
|
|
|
128
166
|
常见错误:`CANCELLED`、`PERMISSION_DENIED`、`NATIVE_FAILED`、`SESSION_EXPIRED`、`UNSUPPORTED`、`TIMEOUT`、`RESULT_TOO_LARGE`、`INVALID_NATIVE_RESULT`。H5 侧还会产生 `BUSY`、`NOT_IN_MINIPROGRAM`、`ENVIRONMENT_TIMEOUT`、`JSSDK_NOT_READY`、`INVALID_PARAMS`、`STORAGE_UNAVAILABLE`、`NAVIGATE_FAILED`、`DISPOSED`、`MOCK_FAILED`、`INVALID_MOCK_RESULT`。微信错误原文保留在 message,不依赖它处理分支。H5 本地校验、环境识别、导航失败和本地等待超时通过 Promise 拒绝;它们没有原生回传,因此不产生 `onResult`。
|
|
129
167
|
|
|
168
|
+
浏览器另外可能返回 `SECURE_CONTEXT_REQUIRED`、`UNSUPPORTED_COORDINATE_SYSTEM`、`POSITION_UNAVAILABLE`、`CAMERA_UNAVAILABLE`、`CODE_NOT_FOUND`、`SCAN_FAILED`、`USER_GESTURE_REQUIRED`、`POPUP_BLOCKED`、`BROWSER_FAILED`、`INVALID_BROWSER_RESULT`。权限被拒绝会保留为 `PERMISSION_DENIED`,不会自动降级到模拟。
|
|
169
|
+
|
|
130
170
|
`checkSession` 仅检查微信 session_key,不验证业务服务自己的 token。`login` 不获取手机号、头像或昵称;手机号授权需要原生按钮交互,不属于本版静默桥接范围。
|
|
131
171
|
|
|
132
172
|
## 宿主契约与扩展
|
|
@@ -145,4 +185,4 @@ await bridge.openLocation({ latitude: 31.2, longitude: 121.4, name: '门店', sc
|
|
|
145
185
|
|
|
146
186
|
2026-10-13,Windows 微信开发者工具 Stable 2.02.2608070 / 基础库 3.17.4:自动 login、getLocation、checkSession、scanCode 成功及扫码取消均完成往返,通过 Hash 在同一页面收到 live 结果。扫码使用开发者工具选择测试二维码图片,不代表真机相机扫描已测试。初版定位页另验证过 Query 重载与 recovered 恢复。iOS / Android 真机、真实支付、拨号、地图、个人地址尚未实测。
|
|
147
187
|
|
|
148
|
-
|
|
188
|
+
在源码仓库根目录构建:`npm run build:sdk`;打包检查:`npm run pack:sdk -- --dry-run`。这些命令会从 `sdks/wechat-miniprogram/VERSION` 同步版本,包只包含 dist、README 和 package.json。发布新版本时修改 VERSION 并推送到 main,CI 会检查、发布 npm 并同步 npmmirror。
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ScanCodeParams, ScanCodeData } from './protocol.js';
|
|
2
|
+
/** Loaded only when scanning starts; decoding is local and never uploads images. */
|
|
3
|
+
export declare function createBarcodeDecoder(params: ScanCodeParams): Promise<(rgba: Uint8ClampedArray, width: number, height: number) => ScanCodeData | undefined>;
|
package/dist/barcode.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Loaded only when scanning starts; decoding is local and never uploads images. */
|
|
2
|
+
export async function createBarcodeDecoder(params) {
|
|
3
|
+
const zxing = await import('@zxing/library');
|
|
4
|
+
const { BarcodeFormat: F, DecodeHintType, MultiFormatReader, RGBLuminanceSource, BinaryBitmap, HybridBinarizer } = zxing;
|
|
5
|
+
const groups = {
|
|
6
|
+
qrCode: [F.QR_CODE], datamatrix: [F.DATA_MATRIX], pdf417: [F.PDF_417],
|
|
7
|
+
barCode: [F.CODABAR, F.CODE_39, F.CODE_93, F.CODE_128, F.EAN_8, F.EAN_13, F.ITF, F.RSS_14, F.UPC_A, F.UPC_E],
|
|
8
|
+
};
|
|
9
|
+
const formats = (params.scanType ?? ['qrCode', 'barCode']).flatMap(type => groups[type]);
|
|
10
|
+
const reader = new MultiFormatReader();
|
|
11
|
+
reader.setHints(new Map([[DecodeHintType.POSSIBLE_FORMATS, formats]]));
|
|
12
|
+
return (rgba, width, height) => {
|
|
13
|
+
const pixels = new Int32Array(width * height);
|
|
14
|
+
for (let i = 0; i < pixels.length; i++)
|
|
15
|
+
pixels[i] = (rgba[i * 4] << 16) | (rgba[i * 4 + 1] << 8) | rgba[i * 4 + 2];
|
|
16
|
+
try {
|
|
17
|
+
const result = reader.decodeWithState(new BinaryBitmap(new HybridBinarizer(new RGBLuminanceSource(pixels, width, height))));
|
|
18
|
+
return { result: result.getText(), scanType: F[result.getBarcodeFormat()] };
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof zxing.NotFoundException || error instanceof zxing.ChecksumException || error instanceof zxing.FormatException)
|
|
22
|
+
return;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
reader.reset();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BridgeMethod, type BridgeRequest, type BridgeResult, type ParamsOf, type DataOf } from './protocol.js';
|
|
2
|
+
export interface BrowserContext {
|
|
3
|
+
requestId: string;
|
|
4
|
+
/** Aborted on timeout, disposal, or completion. */
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
}
|
|
8
|
+
export type BrowserHandler<M extends BridgeMethod> = (params: ParamsOf<M>, context: BrowserContext) => DataOf<M> | Promise<DataOf<M>>;
|
|
9
|
+
export type BrowserHandlers = {
|
|
10
|
+
[M in BridgeMethod]?: BrowserHandler<M>;
|
|
11
|
+
};
|
|
12
|
+
export interface BrowserOptions {
|
|
13
|
+
/** Explicit application integrations; no successful fixtures are supplied. */
|
|
14
|
+
handlers?: BrowserHandlers;
|
|
15
|
+
}
|
|
16
|
+
export declare function createBrowserTransport(browser: Window, options: BrowserOptions): (request: BridgeRequest, complete: (result: BridgeResult) => void) => (() => void);
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { BridgeError } from './errors.js';
|
|
2
|
+
import { scanInBrowser } from './scanner.js';
|
|
3
|
+
import { BRIDGE_METHODS, parseResult } from './protocol.js';
|
|
4
|
+
function location(browser, params, context) {
|
|
5
|
+
if (!browser.isSecureContext)
|
|
6
|
+
throw new BridgeError('SECURE_CONTEXT_REQUIRED', '浏览器定位需要 HTTPS 或 localhost');
|
|
7
|
+
if (!browser.navigator.geolocation)
|
|
8
|
+
throw new BridgeError('UNSUPPORTED', '此浏览器不支持定位');
|
|
9
|
+
if (params.type !== 'wgs84')
|
|
10
|
+
throw new BridgeError('UNSUPPORTED_COORDINATE_SYSTEM', '浏览器定位返回 WGS84,请使用 type: wgs84 或提供坐标转换适配器');
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let watch;
|
|
13
|
+
let finished = false;
|
|
14
|
+
const cleanup = () => {
|
|
15
|
+
finished = true;
|
|
16
|
+
if (watch !== undefined)
|
|
17
|
+
browser.navigator.geolocation.clearWatch(watch);
|
|
18
|
+
context.signal.removeEventListener('abort', abort);
|
|
19
|
+
};
|
|
20
|
+
const abort = () => { cleanup(); reject(new BridgeError('CANCELLED', '定位已结束')); };
|
|
21
|
+
context.signal.addEventListener('abort', abort, { once: true });
|
|
22
|
+
if (context.signal.aborted) {
|
|
23
|
+
abort();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
watch = browser.navigator.geolocation.watchPosition(position => {
|
|
28
|
+
if (finished)
|
|
29
|
+
return;
|
|
30
|
+
cleanup();
|
|
31
|
+
resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, type: 'wgs84' });
|
|
32
|
+
}, error => {
|
|
33
|
+
if (finished)
|
|
34
|
+
return;
|
|
35
|
+
cleanup();
|
|
36
|
+
const code = error.code === 1 ? 'PERMISSION_DENIED' : error.code === 3 ? 'TIMEOUT' : 'POSITION_UNAVAILABLE';
|
|
37
|
+
reject(new BridgeError(code, error.message || '无法获取当前位置'));
|
|
38
|
+
}, { enableHighAccuracy: true, maximumAge: 0, timeout: Math.max(1, context.expiresAt - Date.now()) });
|
|
39
|
+
if (finished)
|
|
40
|
+
browser.navigator.geolocation.clearWatch(watch);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
cleanup();
|
|
44
|
+
reject(error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function requireGesture(browser) {
|
|
49
|
+
if (browser.navigator.userActivation?.isActive === false)
|
|
50
|
+
throw new BridgeError('USER_GESTURE_REQUIRED', '请点击页面按钮后发起此操作');
|
|
51
|
+
}
|
|
52
|
+
export function createBrowserTransport(browser, options) {
|
|
53
|
+
const handlers = { ...options.handlers };
|
|
54
|
+
for (const [method, handler] of Object.entries(handlers)) {
|
|
55
|
+
if (!BRIDGE_METHODS.includes(method) || typeof handler !== 'function')
|
|
56
|
+
throw new BridgeError('INVALID_PARAMS', `Invalid browser handler: ${method}`);
|
|
57
|
+
}
|
|
58
|
+
const defaults = {
|
|
59
|
+
getLocation: (params, context) => location(browser, params, context),
|
|
60
|
+
scanCode: (params, context) => scanInBrowser(browser, params, context.signal),
|
|
61
|
+
openLocation: params => {
|
|
62
|
+
requireGesture(browser);
|
|
63
|
+
const url = new URL('https://uri.amap.com/marker');
|
|
64
|
+
url.search = new URLSearchParams({ position: `${params.longitude},${params.latitude}`, name: params.name ?? params.address ?? '门店位置', coordinate: params.type === 'wgs84' ? 'wgs84' : 'gaode', callnative: '0', src: 'wechat-miniprogram-sdk' }).toString();
|
|
65
|
+
const tab = browser.open('about:blank', '_blank');
|
|
66
|
+
if (!tab)
|
|
67
|
+
throw new BridgeError('POPUP_BLOCKED', '地图窗口被浏览器拦截,请允许弹出窗口后重试');
|
|
68
|
+
try {
|
|
69
|
+
tab.opener = null;
|
|
70
|
+
tab.location.replace(url.href);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
tab.close();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
return { ok: true }; // The map was handed off, not verified as loaded.
|
|
77
|
+
},
|
|
78
|
+
makePhoneCall: params => {
|
|
79
|
+
requireGesture(browser);
|
|
80
|
+
browser.location.assign(`tel:${params.phoneNumber.replace(/[ -]/g, '')}`);
|
|
81
|
+
return { ok: true }; // A tel: request is not evidence of a connected call.
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return (request, complete) => {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const envelope = { v: 1, id: request.id, method: request.method, mode: request.mode };
|
|
87
|
+
const failure = (code, message) => parseResult(JSON.stringify({ ...envelope, completedAt: Date.now(), status: code === 'CANCELLED' ? 'cancel' : 'error', error: { code: code.slice(0, 128), message: message.slice(0, 1024) } }));
|
|
88
|
+
// Defer only a microtask, preserving a button's transient user activation.
|
|
89
|
+
void Promise.resolve().then(async () => {
|
|
90
|
+
if (controller.signal.aborted)
|
|
91
|
+
return;
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
const handler = (handlers[request.method] ?? defaults[request.method]);
|
|
95
|
+
if (!handler)
|
|
96
|
+
throw new BridgeError('UNSUPPORTED', `浏览器不支持微信 ${request.method},请通过 browser.handlers 接入业务实现`);
|
|
97
|
+
const data = await handler(request.params, { requestId: request.id, signal: controller.signal, expiresAt: request.expiresAt });
|
|
98
|
+
if (controller.signal.aborted)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
result = parseResult(JSON.stringify({ ...envelope, completedAt: Date.now(), status: 'success', data }));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
result = failure('INVALID_BROWSER_RESULT', `Invalid browser result for ${request.method}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (controller.signal.aborted)
|
|
109
|
+
return;
|
|
110
|
+
result = failure(error instanceof BridgeError ? error.code : 'BROWSER_FAILED', error instanceof Error ? error.message : String(error));
|
|
111
|
+
}
|
|
112
|
+
if (!controller.signal.aborted)
|
|
113
|
+
complete(result);
|
|
114
|
+
});
|
|
115
|
+
return () => controller.abort();
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { type BridgeResult, type BridgeMethod, type ParamsOf, type DataOf, type ReturnMode } from './protocol.js';
|
|
2
2
|
import { type MockOptions } from './mock.js';
|
|
3
|
+
import { type BrowserOptions } from './browser.js';
|
|
3
4
|
export * from './protocol.js';
|
|
4
5
|
export { BridgeError } from './errors.js';
|
|
5
6
|
export { defaultMockHandlers, type MockOptions, type MockHandlers, type MockHandler, type MockContext } from './mock.js';
|
|
6
|
-
export type
|
|
7
|
-
export type
|
|
7
|
+
export type { BrowserOptions, BrowserHandlers, BrowserHandler, BrowserContext } from './browser.js';
|
|
8
|
+
export type BridgeMode = 'auto' | 'native' | 'browser' | 'mock';
|
|
9
|
+
export type BridgeEnvironment = 'native' | 'browser' | 'mock';
|
|
8
10
|
export interface ResultEvent {
|
|
9
11
|
result: BridgeResult;
|
|
10
|
-
source:
|
|
12
|
+
source: BridgeEnvironment;
|
|
11
13
|
delivery: 'live' | 'recovered';
|
|
12
14
|
trigger: string;
|
|
13
15
|
}
|
|
14
16
|
export interface BridgeOptions {
|
|
15
|
-
/** Default: auto. WeChat mini-programs use native
|
|
17
|
+
/** Default: auto. WeChat mini-programs use the native bridge; other browsers use Web APIs. */
|
|
16
18
|
mode?: BridgeMode;
|
|
17
19
|
mock?: MockOptions;
|
|
20
|
+
browser?: BrowserOptions;
|
|
18
21
|
/** Maximum wait for an unfinished WeChat environment. Default: 2000 ms. */
|
|
19
22
|
environmentTimeoutMs?: number;
|
|
20
23
|
timeoutMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { buildNativeUrl, readReturnUrl, normalizeParams, parseRequest, DEFAULT_T
|
|
|
2
2
|
import { BridgeError } from './errors.js';
|
|
3
3
|
import { createMockTransport } from './mock.js';
|
|
4
4
|
import { detectMiniProgram } from './environment.js';
|
|
5
|
+
import { createBrowserTransport } from './browser.js';
|
|
5
6
|
export * from './protocol.js';
|
|
6
7
|
export { BridgeError } from './errors.js';
|
|
7
8
|
export { defaultMockHandlers } from './mock.js';
|
|
@@ -11,13 +12,14 @@ export function createWechatBridge(options = {}) {
|
|
|
11
12
|
const browser = window;
|
|
12
13
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
13
14
|
const mode = options.mode ?? 'auto';
|
|
14
|
-
if (!['auto', 'native', 'mock'].includes(mode))
|
|
15
|
+
if (!['auto', 'native', 'browser', 'mock'].includes(mode))
|
|
15
16
|
throw new BridgeError('INVALID_PARAMS', 'Invalid SDK mode');
|
|
16
17
|
const environmentTimeoutMs = options.environmentTimeoutMs ?? 2000;
|
|
17
18
|
if (!Number.isFinite(environmentTimeoutMs) || environmentTimeoutMs < 100 || environmentTimeoutMs > 30000)
|
|
18
19
|
throw new BridgeError('INVALID_PARAMS', 'environmentTimeoutMs must be between 100 and 30000');
|
|
19
20
|
const simulate = createMockTransport(options.mock ?? {});
|
|
20
|
-
|
|
21
|
+
const web = createBrowserTransport(browser, options.browser ?? {});
|
|
22
|
+
let localTransport = mode === 'mock' ? simulate : mode === 'browser' ? web : undefined;
|
|
21
23
|
let environment = mode === 'auto' ? undefined : mode;
|
|
22
24
|
let readiness;
|
|
23
25
|
let listening = false;
|
|
@@ -30,7 +32,7 @@ export function createWechatBridge(options = {}) {
|
|
|
30
32
|
}
|
|
31
33
|
catch { /* Consumer logging cannot break transport. */ } };
|
|
32
34
|
function loadPending() {
|
|
33
|
-
if (
|
|
35
|
+
if (environment !== 'native')
|
|
34
36
|
return;
|
|
35
37
|
try {
|
|
36
38
|
const value = JSON.parse(browser.sessionStorage.getItem(STORAGE_KEY) || 'null');
|
|
@@ -42,7 +44,7 @@ export function createWechatBridge(options = {}) {
|
|
|
42
44
|
return undefined;
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
|
-
function removePending() { if (
|
|
47
|
+
function removePending() { if (environment === 'native') {
|
|
46
48
|
try {
|
|
47
49
|
browser.sessionStorage.removeItem(STORAGE_KEY);
|
|
48
50
|
}
|
|
@@ -60,7 +62,7 @@ export function createWechatBridge(options = {}) {
|
|
|
60
62
|
}
|
|
61
63
|
function publish(result, delivery, trigger) {
|
|
62
64
|
try {
|
|
63
|
-
options.onResult?.({ result, delivery, trigger, source:
|
|
65
|
+
options.onResult?.({ result, delivery, trigger, source: environment });
|
|
64
66
|
}
|
|
65
67
|
catch {
|
|
66
68
|
diagnostic('onResult 回调抛出异常,SDK 已完成结果交付');
|
|
@@ -132,8 +134,8 @@ export function createWechatBridge(options = {}) {
|
|
|
132
134
|
readiness = isInMiniProgram().then(inside => {
|
|
133
135
|
if (disposed)
|
|
134
136
|
throw new BridgeError('DISPOSED', 'SDK 已销毁');
|
|
135
|
-
environment = inside ? 'native' : '
|
|
136
|
-
|
|
137
|
+
environment = inside ? 'native' : 'browser';
|
|
138
|
+
localTransport = inside ? undefined : web;
|
|
137
139
|
listen();
|
|
138
140
|
diagnostic(`运行环境:${environment}(自动识别)`);
|
|
139
141
|
return environment;
|
|
@@ -163,7 +165,7 @@ export function createWechatBridge(options = {}) {
|
|
|
163
165
|
const duration = callOptions.timeoutMs ?? timeoutMs;
|
|
164
166
|
const mode = callOptions.mode ?? 'hash';
|
|
165
167
|
try {
|
|
166
|
-
nativeParams = normalizeParams(method, params);
|
|
168
|
+
nativeParams = normalizeParams(method, environment === 'browser' && method === 'getLocation' ? { ...params, type: params.type ?? 'wgs84' } : params);
|
|
167
169
|
if (!Number.isFinite(duration) || duration < 1000 || duration > 600000)
|
|
168
170
|
throw new Error('timeoutMs must be between 1000 and 600000');
|
|
169
171
|
if (mode !== 'hash' && mode !== 'query')
|
|
@@ -175,7 +177,7 @@ export function createWechatBridge(options = {}) {
|
|
|
175
177
|
catch (error) {
|
|
176
178
|
throw new BridgeError('INVALID_PARAMS', error instanceof Error ? error.message : String(error));
|
|
177
179
|
}
|
|
178
|
-
if (!
|
|
180
|
+
if (!localTransport) {
|
|
179
181
|
launching = true;
|
|
180
182
|
let inside = false;
|
|
181
183
|
try {
|
|
@@ -194,12 +196,12 @@ export function createWechatBridge(options = {}) {
|
|
|
194
196
|
const createdAt = Date.now();
|
|
195
197
|
const request = parseRequest(JSON.stringify({
|
|
196
198
|
v: 1, id: browser.crypto.randomUUID(), method, mode,
|
|
197
|
-
returnUrl:
|
|
199
|
+
returnUrl: localTransport ? browser.location.href : readReturnUrl(browser.location.href).cleanUrl, createdAt,
|
|
198
200
|
expiresAt: createdAt + duration, params: nativeParams
|
|
199
201
|
}));
|
|
200
202
|
// Validate the route before reserving storage, so invalid configuration cannot leave a BUSY slot.
|
|
201
|
-
const url =
|
|
202
|
-
if (!
|
|
203
|
+
const url = localTransport ? undefined : buildNativeUrl(request, options.nativePath);
|
|
204
|
+
if (!localTransport) {
|
|
203
205
|
try {
|
|
204
206
|
browser.sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ request, deadline: request.expiresAt }));
|
|
205
207
|
}
|
|
@@ -208,10 +210,10 @@ export function createWechatBridge(options = {}) {
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
return new Promise((resolve, reject) => {
|
|
211
|
-
const timer = setTimeout(() => fail('TIMEOUT',
|
|
213
|
+
const timer = setTimeout(() => fail('TIMEOUT', environment === 'mock' ? '等待模拟结果超时' : environment === 'browser' ? '等待浏览器能力结果超时' : '等待原生页面回传超时'), duration);
|
|
212
214
|
active = { request, resolve, reject, timer };
|
|
213
|
-
if (
|
|
214
|
-
active.cleanup =
|
|
215
|
+
if (localTransport) {
|
|
216
|
+
active.cleanup = localTransport(request, result => {
|
|
215
217
|
const waiting = active;
|
|
216
218
|
if (!waiting || waiting.request.id !== request.id || disposed)
|
|
217
219
|
return;
|
|
@@ -222,9 +224,9 @@ export function createWechatBridge(options = {}) {
|
|
|
222
224
|
waiting.resolve(result.data);
|
|
223
225
|
else
|
|
224
226
|
waiting.reject(new BridgeError(result.error.code, result.error.message));
|
|
225
|
-
publish(result, 'live',
|
|
227
|
+
publish(result, 'live', environment);
|
|
226
228
|
});
|
|
227
|
-
diagnostic(
|
|
229
|
+
diagnostic(`${environment === 'mock' ? '模拟' : '浏览器调用'} ${method} · ${request.id}`);
|
|
228
230
|
return;
|
|
229
231
|
}
|
|
230
232
|
diagnostic(`发起 ${method} · ${request.mode} · ${request.id}`);
|
package/dist/protocol.d.ts
CHANGED
package/dist/protocol.js
CHANGED
|
@@ -48,7 +48,8 @@ export function normalizeParams(method, input) {
|
|
|
48
48
|
keys(value, []);
|
|
49
49
|
break;
|
|
50
50
|
case 'openLocation':
|
|
51
|
-
keys(value, ['latitude', 'longitude', 'name', 'address', 'scale']);
|
|
51
|
+
keys(value, ['latitude', 'longitude', 'type', 'name', 'address', 'scale']);
|
|
52
|
+
requireValid(value.type === undefined || value.type === 'gcj02' || value.type === 'wgs84', 'Invalid coordinate type');
|
|
52
53
|
requireValid(coordinate(value.latitude, 90) && coordinate(value.longitude, 180), 'Invalid coordinates');
|
|
53
54
|
requireValid(['name', 'address'].every(key => value[key] === undefined || text(value[key], 512)), 'Invalid map label');
|
|
54
55
|
requireValid(value.scale === undefined || (finite(value.scale) && value.scale >= 5 && value.scale <= 18), 'Invalid map scale');
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { BridgeError } from './errors.js';
|
|
2
|
+
import { createBarcodeDecoder } from './barcode.js';
|
|
3
|
+
export function scanInBrowser(browser, params, signal) {
|
|
4
|
+
if (!browser.isSecureContext)
|
|
5
|
+
throw new BridgeError('SECURE_CONTEXT_REQUIRED', '浏览器扫码需要 HTTPS 或 localhost');
|
|
6
|
+
const doc = browser.document;
|
|
7
|
+
const dialog = doc.createElement('dialog');
|
|
8
|
+
if (typeof dialog.showModal !== 'function')
|
|
9
|
+
throw new BridgeError('UNSUPPORTED', '浏览器不支持扫码窗口,请升级浏览器或提供扫码适配器');
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let finished = false, busy = false;
|
|
12
|
+
let stream;
|
|
13
|
+
let timer;
|
|
14
|
+
let imageUrl;
|
|
15
|
+
let cancelImage;
|
|
16
|
+
const urls = browser.URL;
|
|
17
|
+
const previousFocus = doc.activeElement;
|
|
18
|
+
const title = doc.createElement('h2');
|
|
19
|
+
title.textContent = '扫描门店码';
|
|
20
|
+
const status = doc.createElement('p');
|
|
21
|
+
status.textContent = '选择摄像头或图片,识别过程在本机完成。';
|
|
22
|
+
status.setAttribute('role', 'status');
|
|
23
|
+
const video = doc.createElement('video');
|
|
24
|
+
video.muted = true;
|
|
25
|
+
video.playsInline = true;
|
|
26
|
+
video.hidden = true;
|
|
27
|
+
video.style.cssText = 'width:100%;max-height:45vh;object-fit:contain;background:#111;border-radius:12px';
|
|
28
|
+
const canvas = doc.createElement('canvas');
|
|
29
|
+
const camera = doc.createElement('button');
|
|
30
|
+
camera.type = 'button';
|
|
31
|
+
camera.textContent = '打开摄像头';
|
|
32
|
+
const file = doc.createElement('input');
|
|
33
|
+
file.type = 'file';
|
|
34
|
+
file.accept = 'image/*';
|
|
35
|
+
file.setAttribute('aria-label', '选择二维码或条码图片');
|
|
36
|
+
file.hidden = !!params.onlyFromCamera;
|
|
37
|
+
const cancel = doc.createElement('button');
|
|
38
|
+
cancel.type = 'button';
|
|
39
|
+
cancel.textContent = '取消扫码';
|
|
40
|
+
for (const button of [camera, cancel])
|
|
41
|
+
button.style.cssText = 'padding:12px 16px;margin:8px 8px 8px 0;border:1px solid #ccc;border-radius:8px;background:white;color:#171717;font:inherit;cursor:pointer';
|
|
42
|
+
file.style.cssText = 'display:block;max-width:100%;margin:12px 0;font:inherit';
|
|
43
|
+
// Inline display must not override the onlyFromCamera restriction.
|
|
44
|
+
if (params.onlyFromCamera)
|
|
45
|
+
file.style.display = 'none';
|
|
46
|
+
dialog.setAttribute('aria-label', '扫描门店码');
|
|
47
|
+
dialog.style.cssText = 'box-sizing:border-box;width:min(92vw,460px);max-height:90vh;overflow:auto;border:0;border-radius:16px;padding:24px;background:white;color:#171717;font:16px/1.5 system-ui;box-shadow:0 12px 80px #0006';
|
|
48
|
+
dialog.append(title, status, video, camera, file, cancel);
|
|
49
|
+
function stopStream(value) { value?.getTracks().forEach(track => track.stop()); }
|
|
50
|
+
function finish(data, error) {
|
|
51
|
+
if (finished)
|
|
52
|
+
return;
|
|
53
|
+
finished = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
stopStream(stream);
|
|
56
|
+
video.pause();
|
|
57
|
+
video.srcObject = null;
|
|
58
|
+
cancelImage?.();
|
|
59
|
+
if (imageUrl)
|
|
60
|
+
urls.revokeObjectURL(imageUrl);
|
|
61
|
+
signal.removeEventListener('abort', aborted);
|
|
62
|
+
dialog.close();
|
|
63
|
+
dialog.remove();
|
|
64
|
+
if (previousFocus?.isConnected)
|
|
65
|
+
previousFocus.focus();
|
|
66
|
+
if (error)
|
|
67
|
+
reject(error);
|
|
68
|
+
else
|
|
69
|
+
resolve(data);
|
|
70
|
+
}
|
|
71
|
+
const aborted = () => finish(undefined, new BridgeError('CANCELLED', '扫码已取消'));
|
|
72
|
+
const failure = (error) => {
|
|
73
|
+
const name = error?.name;
|
|
74
|
+
finish(undefined, error instanceof BridgeError ? error : new BridgeError(name === 'NotAllowedError' || name === 'SecurityError' ? 'PERMISSION_DENIED' : name === 'NotFoundError' || name === 'NotReadableError' ? 'CAMERA_UNAVAILABLE' : 'SCAN_FAILED', error instanceof Error ? error.message : String(error)));
|
|
75
|
+
};
|
|
76
|
+
const setBusy = () => { busy = true; camera.disabled = true; file.disabled = true; };
|
|
77
|
+
function decodeFrame(source, width, height, decode) {
|
|
78
|
+
if (!width || !height)
|
|
79
|
+
return;
|
|
80
|
+
const ratio = Math.min(1, 1280 / Math.max(width, height));
|
|
81
|
+
canvas.width = Math.max(1, Math.round(width * ratio));
|
|
82
|
+
canvas.height = Math.max(1, Math.round(height * ratio));
|
|
83
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
84
|
+
if (!context)
|
|
85
|
+
throw new BridgeError('UNSUPPORTED', '此浏览器不支持图像解码');
|
|
86
|
+
context.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
87
|
+
return decode(context.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height);
|
|
88
|
+
}
|
|
89
|
+
camera.addEventListener('click', () => {
|
|
90
|
+
if (busy || finished)
|
|
91
|
+
return;
|
|
92
|
+
setBusy();
|
|
93
|
+
status.textContent = '请允许摄像头权限,并将二维码或条码置于画面中。';
|
|
94
|
+
void (async () => {
|
|
95
|
+
if (!browser.navigator.mediaDevices?.getUserMedia)
|
|
96
|
+
throw new BridgeError('UNSUPPORTED', '此浏览器不支持摄像头');
|
|
97
|
+
const decode = await createBarcodeDecoder(params);
|
|
98
|
+
if (finished)
|
|
99
|
+
return;
|
|
100
|
+
const acquired = await browser.navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: { ideal: 'environment' } } });
|
|
101
|
+
if (finished) {
|
|
102
|
+
stopStream(acquired);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
stream = acquired;
|
|
106
|
+
for (const track of stream.getTracks())
|
|
107
|
+
track.addEventListener('ended', () => failure(new BridgeError('CAMERA_UNAVAILABLE', '摄像头已断开')), { once: true });
|
|
108
|
+
video.srcObject = stream;
|
|
109
|
+
video.hidden = false;
|
|
110
|
+
await video.play();
|
|
111
|
+
if (finished)
|
|
112
|
+
return;
|
|
113
|
+
const scan = () => {
|
|
114
|
+
if (finished)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
const result = decodeFrame(video, video.videoWidth, video.videoHeight, decode);
|
|
118
|
+
if (result) {
|
|
119
|
+
finish(result);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
timer = setTimeout(scan, 150);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
failure(error);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
scan();
|
|
129
|
+
})().catch(failure);
|
|
130
|
+
});
|
|
131
|
+
file.addEventListener('change', () => {
|
|
132
|
+
const selected = file.files?.[0];
|
|
133
|
+
if (busy || finished || !selected || params.onlyFromCamera)
|
|
134
|
+
return;
|
|
135
|
+
setBusy();
|
|
136
|
+
status.textContent = '正在识别图片…';
|
|
137
|
+
void (async () => {
|
|
138
|
+
if (selected.size > 20 * 1024 * 1024)
|
|
139
|
+
throw new BridgeError('INVALID_PARAMS', '请选择不超过 20 MB 的图片');
|
|
140
|
+
const decode = await createBarcodeDecoder(params);
|
|
141
|
+
if (finished)
|
|
142
|
+
return;
|
|
143
|
+
const image = doc.createElement('img');
|
|
144
|
+
await new Promise((done, failed) => {
|
|
145
|
+
cancelImage = () => { image.onload = null; image.onerror = null; image.src = ''; failed(new BridgeError('CANCELLED', '扫码已取消')); };
|
|
146
|
+
image.onload = () => { cancelImage = undefined; done(); };
|
|
147
|
+
image.onerror = () => { cancelImage = undefined; failed(new BridgeError('SCAN_FAILED', '无法读取此图片')); };
|
|
148
|
+
imageUrl = urls.createObjectURL(selected);
|
|
149
|
+
image.src = imageUrl;
|
|
150
|
+
});
|
|
151
|
+
if (finished)
|
|
152
|
+
return;
|
|
153
|
+
const result = decodeFrame(image, image.naturalWidth, image.naturalHeight, decode);
|
|
154
|
+
if (!result)
|
|
155
|
+
throw new BridgeError('CODE_NOT_FOUND', '图片中未识别到指定类型的二维码或条码');
|
|
156
|
+
finish(result);
|
|
157
|
+
})().catch(failure);
|
|
158
|
+
});
|
|
159
|
+
cancel.addEventListener('click', aborted);
|
|
160
|
+
dialog.addEventListener('cancel', event => { event.preventDefault(); aborted(); });
|
|
161
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
162
|
+
if (signal.aborted) {
|
|
163
|
+
aborted();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
doc.body.append(dialog);
|
|
167
|
+
try {
|
|
168
|
+
dialog.showModal();
|
|
169
|
+
cancel.focus();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
failure(error);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spopc/wechat-miniprogram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Web SDK for the WeChat Mini Program URL bridge",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,5 +43,8 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public",
|
|
45
45
|
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@zxing/library": "0.21.3"
|
|
46
49
|
}
|
|
47
50
|
}
|