@unif/react-native-hms-scan 0.5.4 → 0.5.5
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/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +31 -15
- package/lib/module/NativeHmsScan.js.map +1 -1
- package/lib/module/Scanner/ScanErrorOverlay.js +55 -0
- package/lib/module/Scanner/ScanErrorOverlay.js.map +1 -0
- package/lib/module/Scanner/Scanner.js +176 -52
- package/lib/module/Scanner/Scanner.js.map +1 -1
- package/lib/module/decodeImage.js +3 -2
- package/lib/module/decodeImage.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/HmsScanView.d.ts +3 -6
- package/lib/typescript/src/HmsScanView.d.ts.map +1 -1
- package/lib/typescript/src/NativeHmsScan.d.ts +3 -1
- package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ScanErrorOverlay.d.ts +7 -0
- package/lib/typescript/src/Scanner/ScanErrorOverlay.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/Scanner.d.ts +9 -6
- package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -1
- package/lib/typescript/src/decodeImage.d.ts +3 -2
- package/lib/typescript/src/decodeImage.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +6 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/HmsScanView.tsx +3 -3
- package/src/NativeHmsScan.ts +3 -1
- package/src/Scanner/ScanErrorOverlay.tsx +51 -0
- package/src/Scanner/Scanner.tsx +236 -54
- package/src/decodeImage.ts +3 -2
- package/src/index.tsx +1 -0
- package/src/types.ts +7 -1
package/src/Scanner/Scanner.tsx
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Linking, StyleSheet, View } from 'react-native';
|
|
3
|
-
import { ThemeProvider
|
|
2
|
+
import { AppState, Linking, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { ThemeProvider } from '@unif/react-native-design';
|
|
4
4
|
import { HmsScanView } from '../HmsScanView';
|
|
5
5
|
import { decodeImage } from '../decodeImage';
|
|
6
6
|
import {
|
|
7
7
|
getCameraPermissionStatus,
|
|
8
8
|
requestCameraPermission,
|
|
9
9
|
} from '../permissions';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
BarcodeFormat,
|
|
12
|
+
ScanError,
|
|
13
|
+
ScanProduct,
|
|
14
|
+
ScanResult,
|
|
15
|
+
} from '../types';
|
|
11
16
|
import { scanChrome } from './scanChrome';
|
|
12
17
|
import { Viewfinder } from './Viewfinder';
|
|
13
18
|
import { ScanTopBar } from './ScanTopBar';
|
|
@@ -15,18 +20,39 @@ import { ScanToolbar } from './ScanToolbar';
|
|
|
15
20
|
import { ResultFocus } from './ResultFocus';
|
|
16
21
|
import { ResultFail } from './ResultFail';
|
|
17
22
|
import { DeniedOverlay } from './DeniedOverlay';
|
|
23
|
+
import { ScanErrorOverlay } from './ScanErrorOverlay';
|
|
18
24
|
|
|
19
25
|
// 'done':autoConfirm 自动回调后的终态——相机暂停、不出卡片、不自动重扫(宿主通常已导航离开)。
|
|
20
|
-
type Phase =
|
|
26
|
+
type Phase =
|
|
27
|
+
| 'init'
|
|
28
|
+
| 'scan'
|
|
29
|
+
| 'detecting'
|
|
30
|
+
| 'success'
|
|
31
|
+
| 'fail'
|
|
32
|
+
| 'denied'
|
|
33
|
+
| 'error'
|
|
34
|
+
| 'done';
|
|
21
35
|
|
|
22
36
|
const VF_SIZE = 256;
|
|
23
37
|
const DEFAULT_HINT = '将条码 / 二维码放入框内,自动扫描';
|
|
24
38
|
|
|
39
|
+
function toScanError(error: unknown, fallbackMessage: string): ScanError {
|
|
40
|
+
if (error && typeof error === 'object') {
|
|
41
|
+
const native = error as { code?: unknown; message?: unknown };
|
|
42
|
+
return {
|
|
43
|
+
code: typeof native.code === 'string' ? native.code : 'E_UNKNOWN',
|
|
44
|
+
message:
|
|
45
|
+
typeof native.message === 'string' ? native.message : fallbackMessage,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { code: 'E_UNKNOWN', message: fallbackMessage };
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
export interface ScannerProps {
|
|
26
52
|
/** 顶栏标题,默认 "扫一扫"。 */
|
|
27
53
|
title?: string;
|
|
28
54
|
/** 限定识别码制;不传 = 全部。 */
|
|
29
|
-
formats?: BarcodeFormat[];
|
|
55
|
+
formats?: readonly BarcodeFormat[];
|
|
30
56
|
/** 取景态提示文案。 */
|
|
31
57
|
hintText?: string;
|
|
32
58
|
/** 顶部安全区(默认 54)。用 react-native-safe-area-context 时可传 insets.top。 */
|
|
@@ -37,6 +63,8 @@ export interface ScannerProps {
|
|
|
37
63
|
showTorch?: boolean;
|
|
38
64
|
/** 返回回调(退出扫码页,回到上一级);按钮在底部工具栏,与手电筒并排。 */
|
|
39
65
|
onClose?: () => void;
|
|
66
|
+
/** 扫码相机或权限检查出错时上报。 */
|
|
67
|
+
onScanError?: (error: ScanError) => void;
|
|
40
68
|
/**
|
|
41
69
|
* 扫到条码后由宿主解析商品信息(用于浮层确认卡)。
|
|
42
70
|
* 返回 null/undefined 视为"未识别",抛错亦然。不传则默认用扫到的原文当商品名。
|
|
@@ -47,8 +75,9 @@ export interface ScannerProps {
|
|
|
47
75
|
/** 用户点"确认":把结果带回上一级(宿主通常在此导航返回)。 */
|
|
48
76
|
onConfirm?: (product: ScanProduct, result: ScanResult) => void;
|
|
49
77
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
78
|
+
* 自动确认:传 `onConfirm` 时,扫到并解析成功后**不显示结果卡**,直接触发
|
|
79
|
+
* `onConfirm(product, result)`;未传回调时仍显示结果卡。默认 `false`(显示结果卡,
|
|
80
|
+
* 由用户点"确定")。
|
|
52
81
|
* 适合"扫到即用、不需要二次确认"的场景。回调后相机暂停、不自动重扫——宿主通常在
|
|
53
82
|
* `onConfirm` 里导航离开;若需再扫由宿主控制(如重新挂载 `<Scanner>`)。
|
|
54
83
|
* 注:未识别(`resolveProduct` 返回 `null`/抛错)仍走失败态可重扫,不会误触发。
|
|
@@ -63,14 +92,13 @@ export interface ScannerProps {
|
|
|
63
92
|
|
|
64
93
|
/**
|
|
65
94
|
* 成品「扫一扫」界面(聚焦款)。底层 <HmsScanView> 出相机画面,取景框 / 工具栏 / 结果卡
|
|
66
|
-
* 全用 @unif/react-native-design 的主题令牌与组件绘制(统一风格)。自带 ThemeProvider
|
|
67
|
-
*
|
|
95
|
+
* 全用 @unif/react-native-design 的主题令牌与组件绘制(统一风格)。自带 ThemeProvider,
|
|
96
|
+
* 可直接整屏接入;放进宿主已有的 ThemeProvider 里也兼容。
|
|
68
97
|
*/
|
|
69
98
|
export function Scanner(props: ScannerProps) {
|
|
70
99
|
return (
|
|
71
100
|
<ThemeProvider>
|
|
72
101
|
<ScannerInner {...props} />
|
|
73
|
-
<ToastHost />
|
|
74
102
|
</ThemeProvider>
|
|
75
103
|
);
|
|
76
104
|
}
|
|
@@ -87,59 +115,148 @@ function ScannerInner({
|
|
|
87
115
|
onConfirm,
|
|
88
116
|
autoConfirm = false,
|
|
89
117
|
pickImage,
|
|
118
|
+
onScanError,
|
|
90
119
|
}: ScannerProps) {
|
|
91
120
|
const [phase, setPhase] = useState<Phase>('init');
|
|
92
121
|
const [torch, setTorch] = useState(false);
|
|
93
122
|
const [product, setProduct] = useState<ScanProduct | null>(null);
|
|
94
123
|
const [detectMs, setDetectMs] = useState(0);
|
|
124
|
+
const [scanSession, setScanSession] = useState(0);
|
|
95
125
|
|
|
96
126
|
const handlingRef = useRef(false);
|
|
127
|
+
const acceptingResultsRef = useRef(false);
|
|
128
|
+
const scanSessionRef = useRef(0);
|
|
97
129
|
const detectStartRef = useRef(0);
|
|
98
130
|
const lastResultRef = useRef<ScanResult | null>(null);
|
|
99
131
|
const mountedRef = useRef(true);
|
|
132
|
+
const permissionRunRef = useRef(0);
|
|
133
|
+
const processingRunRef = useRef(0);
|
|
134
|
+
const waitingForSettingsRef = useRef(false);
|
|
135
|
+
const onScanErrorRef = useRef(onScanError);
|
|
136
|
+
onScanErrorRef.current = onScanError;
|
|
100
137
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
const invalidateProcessing = useCallback(() => {
|
|
139
|
+
++processingRunRef.current;
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const invalidateScanSession = useCallback(() => {
|
|
143
|
+
++scanSessionRef.current;
|
|
144
|
+
acceptingResultsRef.current = false;
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const enterScan = useCallback(() => {
|
|
148
|
+
invalidateProcessing();
|
|
149
|
+
handlingRef.current = false;
|
|
150
|
+
const sessionId = ++scanSessionRef.current;
|
|
151
|
+
acceptingResultsRef.current = true;
|
|
152
|
+
setScanSession(sessionId);
|
|
153
|
+
setProduct(null);
|
|
154
|
+
lastResultRef.current = null;
|
|
155
|
+
setPhase('scan');
|
|
156
|
+
}, [invalidateProcessing]);
|
|
157
|
+
|
|
158
|
+
const enterDenied = useCallback(() => {
|
|
159
|
+
++permissionRunRef.current;
|
|
160
|
+
invalidateProcessing();
|
|
161
|
+
invalidateScanSession();
|
|
162
|
+
setProduct(null);
|
|
163
|
+
setPhase('denied');
|
|
164
|
+
}, [invalidateProcessing, invalidateScanSession]);
|
|
165
|
+
|
|
166
|
+
const reportFatalError = useCallback(
|
|
167
|
+
(error: ScanError) => {
|
|
168
|
+
// fatal 后不允许已在途的权限检查或扫码 / 相册处理链覆盖 error phase。
|
|
169
|
+
++permissionRunRef.current;
|
|
170
|
+
invalidateProcessing();
|
|
171
|
+
invalidateScanSession();
|
|
172
|
+
setProduct(null);
|
|
173
|
+
setPhase('error');
|
|
174
|
+
onScanErrorRef.current?.(error);
|
|
175
|
+
},
|
|
176
|
+
[invalidateProcessing, invalidateScanSession]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const runPermissionFlow = useCallback(
|
|
180
|
+
async (requestIfNeeded: boolean) => {
|
|
181
|
+
const runId = ++permissionRunRef.current;
|
|
182
|
+
const isCurrent = () =>
|
|
183
|
+
mountedRef.current && permissionRunRef.current === runId;
|
|
105
184
|
try {
|
|
106
185
|
let status = await getCameraPermissionStatus();
|
|
107
|
-
if (
|
|
186
|
+
if (!isCurrent()) return;
|
|
187
|
+
if (
|
|
188
|
+
requestIfNeeded &&
|
|
189
|
+
(status === 'undetermined' || status === 'denied')
|
|
190
|
+
) {
|
|
108
191
|
status = await requestCameraPermission();
|
|
192
|
+
if (!isCurrent()) return;
|
|
109
193
|
}
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
194
|
+
if (status === 'granted') {
|
|
195
|
+
enterScan();
|
|
196
|
+
} else {
|
|
197
|
+
enterDenied();
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (!isCurrent()) return;
|
|
201
|
+
reportFatalError(toScanError(error, '检查相机权限失败'));
|
|
114
202
|
}
|
|
115
|
-
}
|
|
203
|
+
},
|
|
204
|
+
[enterDenied, enterScan, reportFatalError]
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// 权限流:已授权 → scan;否则请求;仍未授权 → denied。
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
mountedRef.current = true;
|
|
210
|
+
void runPermissionFlow(true);
|
|
116
211
|
return () => {
|
|
117
212
|
mountedRef.current = false;
|
|
213
|
+
++permissionRunRef.current;
|
|
214
|
+
invalidateProcessing();
|
|
215
|
+
invalidateScanSession();
|
|
118
216
|
};
|
|
119
|
-
}, []);
|
|
217
|
+
}, [invalidateProcessing, invalidateScanSession, runPermissionFlow]);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
221
|
+
if (nextState === 'active' && waitingForSettingsRef.current) {
|
|
222
|
+
waitingForSettingsRef.current = false;
|
|
223
|
+
void runPermissionFlow(false);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return () => subscription.remove();
|
|
227
|
+
}, [runPermissionFlow]);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!showTorch) setTorch(false);
|
|
231
|
+
}, [showTorch]);
|
|
120
232
|
|
|
121
233
|
const reset = useCallback(() => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
setPhase('scan');
|
|
125
|
-
}, []);
|
|
234
|
+
enterScan();
|
|
235
|
+
}, [enterScan]);
|
|
126
236
|
|
|
127
237
|
const finalize = useCallback(
|
|
128
|
-
async (result: ScanResult) => {
|
|
238
|
+
async (result: ScanResult, runId: number) => {
|
|
239
|
+
const isCurrent = () =>
|
|
240
|
+
mountedRef.current && processingRunRef.current === runId;
|
|
241
|
+
if (!isCurrent()) return;
|
|
129
242
|
lastResultRef.current = result;
|
|
130
243
|
try {
|
|
131
244
|
const resolved = resolveProduct
|
|
132
245
|
? await resolveProduct(result)
|
|
133
246
|
: { name: result.value };
|
|
134
|
-
if (!
|
|
247
|
+
if (!isCurrent()) return;
|
|
135
248
|
if (!resolved) {
|
|
136
249
|
setPhase('fail');
|
|
137
250
|
return;
|
|
138
251
|
}
|
|
139
|
-
const p: ScanProduct = {
|
|
140
|
-
|
|
252
|
+
const p: ScanProduct = {
|
|
253
|
+
...resolved,
|
|
254
|
+
barcode: resolved.barcode ?? result.value,
|
|
255
|
+
};
|
|
256
|
+
if (autoConfirm && onConfirm) {
|
|
141
257
|
// 跳过结果卡:直接回调,进 'done' 终态(相机暂停、不自动重扫)。
|
|
142
|
-
onConfirm
|
|
258
|
+
onConfirm(p, result);
|
|
259
|
+
if (!isCurrent()) return;
|
|
143
260
|
setPhase('done');
|
|
144
261
|
return;
|
|
145
262
|
}
|
|
@@ -147,7 +264,7 @@ function ScannerInner({
|
|
|
147
264
|
setDetectMs(Date.now() - detectStartRef.current);
|
|
148
265
|
setPhase('success');
|
|
149
266
|
} catch {
|
|
150
|
-
if (
|
|
267
|
+
if (isCurrent()) setPhase('fail');
|
|
151
268
|
}
|
|
152
269
|
},
|
|
153
270
|
[resolveProduct, autoConfirm, onConfirm]
|
|
@@ -155,44 +272,98 @@ function ScannerInner({
|
|
|
155
272
|
|
|
156
273
|
const onCameraResult = useCallback(
|
|
157
274
|
(results: ScanResult[]) => {
|
|
158
|
-
if (
|
|
275
|
+
if (
|
|
276
|
+
scanSession !== scanSessionRef.current ||
|
|
277
|
+
phase !== 'scan' ||
|
|
278
|
+
!acceptingResultsRef.current ||
|
|
279
|
+
handlingRef.current
|
|
280
|
+
) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
159
283
|
const first = results[0];
|
|
160
284
|
if (!first) return;
|
|
285
|
+
acceptingResultsRef.current = false;
|
|
161
286
|
handlingRef.current = true;
|
|
287
|
+
const runId = ++processingRunRef.current;
|
|
162
288
|
detectStartRef.current = Date.now();
|
|
163
289
|
setPhase('detecting');
|
|
164
|
-
void finalize(first);
|
|
290
|
+
void finalize(first, runId);
|
|
165
291
|
},
|
|
166
|
-
[phase, finalize]
|
|
292
|
+
[phase, scanSession, finalize]
|
|
167
293
|
);
|
|
168
294
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
295
|
+
const handleViewScanError = useCallback(
|
|
296
|
+
(error: ScanError) => {
|
|
297
|
+
if (error.code === 'E_NO_CAMERA_PERMISSION') {
|
|
298
|
+
enterDenied();
|
|
299
|
+
} else if (error.code !== 'E_NO_RESULT') {
|
|
300
|
+
reportFatalError(error);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
onScanErrorRef.current?.(error);
|
|
304
|
+
},
|
|
305
|
+
[enterDenied, reportFatalError]
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const onTorchStatus = useCallback(
|
|
309
|
+
(status: { available: boolean; on: boolean }) => {
|
|
310
|
+
setTorch(status.on);
|
|
311
|
+
},
|
|
312
|
+
[]
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const openSettings = useCallback(async () => {
|
|
316
|
+
waitingForSettingsRef.current = true;
|
|
317
|
+
try {
|
|
318
|
+
await Linking.openSettings();
|
|
319
|
+
} catch (error) {
|
|
320
|
+
waitingForSettingsRef.current = false;
|
|
321
|
+
if (mountedRef.current) {
|
|
322
|
+
reportFatalError(toScanError(error, '打开系统设置失败'));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}, [reportFatalError]);
|
|
326
|
+
|
|
327
|
+
const retryCamera = useCallback(() => {
|
|
328
|
+
invalidateProcessing();
|
|
329
|
+
invalidateScanSession();
|
|
330
|
+
handlingRef.current = false;
|
|
331
|
+
setProduct(null);
|
|
332
|
+
lastResultRef.current = null;
|
|
333
|
+
setPhase('init');
|
|
334
|
+
void runPermissionFlow(true);
|
|
335
|
+
}, [invalidateProcessing, invalidateScanSession, runPermissionFlow]);
|
|
172
336
|
|
|
173
337
|
const onAlbum = useCallback(async () => {
|
|
174
|
-
if (!pickImage || handlingRef.current)
|
|
338
|
+
if (!pickImage || !acceptingResultsRef.current || handlingRef.current) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
acceptingResultsRef.current = false;
|
|
342
|
+
handlingRef.current = true;
|
|
343
|
+
const runId = ++processingRunRef.current;
|
|
344
|
+
const isCurrent = () =>
|
|
345
|
+
mountedRef.current && processingRunRef.current === runId;
|
|
346
|
+
setPhase('detecting');
|
|
175
347
|
try {
|
|
176
348
|
const uri = await pickImage();
|
|
177
|
-
if (!
|
|
178
|
-
|
|
349
|
+
if (!isCurrent()) return;
|
|
350
|
+
if (!uri) {
|
|
351
|
+
reset();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
179
354
|
detectStartRef.current = Date.now();
|
|
180
|
-
setPhase('detecting');
|
|
181
355
|
const results = await decodeImage(uri, formats ? { formats } : undefined);
|
|
182
|
-
if (!
|
|
356
|
+
if (!isCurrent()) return;
|
|
183
357
|
const first = results[0];
|
|
184
358
|
if (!first) {
|
|
185
359
|
setPhase('fail');
|
|
186
360
|
return;
|
|
187
361
|
}
|
|
188
|
-
await finalize(first);
|
|
362
|
+
await finalize(first, runId);
|
|
189
363
|
} catch {
|
|
190
|
-
if (
|
|
191
|
-
handlingRef.current = true;
|
|
192
|
-
setPhase('fail');
|
|
193
|
-
}
|
|
364
|
+
if (isCurrent()) setPhase('fail');
|
|
194
365
|
}
|
|
195
|
-
}, [pickImage, formats, finalize]);
|
|
366
|
+
}, [pickImage, formats, finalize, reset]);
|
|
196
367
|
|
|
197
368
|
const onConfirmPress = useCallback(() => {
|
|
198
369
|
const r = lastResultRef.current;
|
|
@@ -207,22 +378,27 @@ function ScannerInner({
|
|
|
207
378
|
|
|
208
379
|
return (
|
|
209
380
|
<View style={styles.root}>
|
|
210
|
-
{phase !== 'denied' && phase !== 'init' && (
|
|
381
|
+
{phase !== 'denied' && phase !== 'init' && phase !== 'error' && (
|
|
211
382
|
<HmsScanView
|
|
212
383
|
style={StyleSheet.absoluteFill}
|
|
213
384
|
formats={formats}
|
|
214
385
|
paused={phase !== 'scan'}
|
|
215
386
|
torch={torch}
|
|
216
387
|
onScanResult={onCameraResult}
|
|
217
|
-
onScanError={
|
|
388
|
+
onScanError={handleViewScanError}
|
|
389
|
+
onTorchStatus={onTorchStatus}
|
|
218
390
|
/>
|
|
219
391
|
)}
|
|
220
392
|
|
|
221
393
|
{showChrome && (
|
|
222
|
-
<Viewfinder
|
|
394
|
+
<Viewfinder
|
|
395
|
+
size={VF_SIZE}
|
|
396
|
+
detecting={phase === 'detecting'}
|
|
397
|
+
hintText={hintText}
|
|
398
|
+
/>
|
|
223
399
|
)}
|
|
224
400
|
|
|
225
|
-
{phase !== 'denied' && phase !== 'init' && (
|
|
401
|
+
{phase !== 'denied' && phase !== 'init' && phase !== 'error' && (
|
|
226
402
|
<ScanTopBar title={title} topInset={topInset} />
|
|
227
403
|
)}
|
|
228
404
|
|
|
@@ -246,10 +422,16 @@ function ScannerInner({
|
|
|
246
422
|
/>
|
|
247
423
|
)}
|
|
248
424
|
|
|
249
|
-
{phase === 'fail' &&
|
|
425
|
+
{phase === 'fail' && (
|
|
426
|
+
<ResultFail bottomInset={bottomInset} onRescan={reset} />
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{phase === 'error' && (
|
|
430
|
+
<ScanErrorOverlay onClose={onClose} onRetry={retryCamera} />
|
|
431
|
+
)}
|
|
250
432
|
|
|
251
433
|
{phase === 'denied' && (
|
|
252
|
-
<DeniedOverlay onClose={onClose} onSettings={
|
|
434
|
+
<DeniedOverlay onClose={onClose} onSettings={openSettings} />
|
|
253
435
|
)}
|
|
254
436
|
</View>
|
|
255
437
|
);
|
package/src/decodeImage.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { HmsScanError, type DecodeImageOptions, type ScanResult } from './types'
|
|
|
5
5
|
/**
|
|
6
6
|
* 从本地图片解码条码 / 二维码(华为 Bitmap 模式)。
|
|
7
7
|
*
|
|
8
|
-
* @param uri
|
|
9
|
-
*
|
|
8
|
+
* @param uri 本地图片。iOS 支持 file://、绝对路径、data:;Android 支持
|
|
9
|
+
* file://、绝对路径、content://、android.resource://。
|
|
10
|
+
* 两端都不下载远程 URL,也不支持 ph:// / assets-library://。
|
|
10
11
|
* @param options 可选;formats 限定码制,不传 = 全部。
|
|
11
12
|
* @returns 命中的结果数组(可能为空)。
|
|
12
13
|
* @throws HmsScanError 图片加载失败 / 读权限缺失等。
|
package/src/index.tsx
CHANGED
package/src/types.ts
CHANGED
|
@@ -82,7 +82,13 @@ export interface ScanResult {
|
|
|
82
82
|
/** decodeImage 选项。 */
|
|
83
83
|
export interface DecodeImageOptions {
|
|
84
84
|
/** 限定识别码制;不传 = 全部。 */
|
|
85
|
-
formats?: BarcodeFormat[];
|
|
85
|
+
formats?: readonly BarcodeFormat[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `<HmsScanView>` / `<Scanner>` 通过回调上报的普通错误对象。 */
|
|
89
|
+
export interface ScanError {
|
|
90
|
+
code: string;
|
|
91
|
+
message: string;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/** 相机权限状态。 */
|