@mpxjs/webpack-plugin 2.10.18 → 2.10.19
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/lib/platform/template/wx/component-config/camera.js +12 -0
- package/lib/platform/template/wx/component-config/unsupported.js +1 -1
- package/lib/runtime/components/react/dist/mpx-camera.d.ts +31 -0
- package/lib/runtime/components/react/dist/mpx-camera.jsx +270 -0
- package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +92 -15
- package/lib/runtime/components/react/dist/mpx-web-view.jsx +1 -1
- package/lib/runtime/components/react/mpx-camera.tsx +358 -0
- package/lib/runtime/components/react/mpx-scroll-view.tsx +106 -16
- package/lib/runtime/components/react/mpx-web-view.tsx +1 -1
- package/lib/utils/dom-tag-config.js +1 -1
- package/package.json +4 -3
|
@@ -18,6 +18,18 @@ module.exports = function ({ print }) {
|
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
20
|
test: TAG_NAME,
|
|
21
|
+
ios (tag, { el }) {
|
|
22
|
+
el.isBuiltIn = true
|
|
23
|
+
return 'mpx-camera'
|
|
24
|
+
},
|
|
25
|
+
android (tag, { el }) {
|
|
26
|
+
el.isBuiltIn = true
|
|
27
|
+
return 'mpx-camera'
|
|
28
|
+
},
|
|
29
|
+
harmony (tag, { el }) {
|
|
30
|
+
el.isBuiltIn = true
|
|
31
|
+
return 'mpx-camera'
|
|
32
|
+
},
|
|
21
33
|
props: [
|
|
22
34
|
{
|
|
23
35
|
test: 'mode',
|
|
@@ -13,7 +13,7 @@ const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher',
|
|
|
13
13
|
// 快应用不支持的标签集合
|
|
14
14
|
const QA_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'cover-image']
|
|
15
15
|
// RN不支持的标签集合
|
|
16
|
-
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', '
|
|
16
|
+
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @param {function(object): function} print
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
interface CameraProps {
|
|
3
|
+
mode?: 'normal' | 'scanCode';
|
|
4
|
+
resolution?: 'low' | 'medium' | 'high';
|
|
5
|
+
'device-position'?: 'front' | 'back';
|
|
6
|
+
flash?: 'auto' | 'on' | 'off';
|
|
7
|
+
'frame-size'?: 'small' | 'medium' | 'large';
|
|
8
|
+
style?: Record<string, any>;
|
|
9
|
+
bindstop?: () => void;
|
|
10
|
+
binderror?: (error: {
|
|
11
|
+
message: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
bindinitdone?: (result: {
|
|
14
|
+
type: string;
|
|
15
|
+
data: string;
|
|
16
|
+
}) => void;
|
|
17
|
+
bindscancode?: (result: {
|
|
18
|
+
type: string;
|
|
19
|
+
data: string;
|
|
20
|
+
}) => void;
|
|
21
|
+
'parent-font-size'?: number;
|
|
22
|
+
'parent-width'?: number;
|
|
23
|
+
'parent-height'?: number;
|
|
24
|
+
'enable-var'?: boolean;
|
|
25
|
+
'external-var-context'?: any;
|
|
26
|
+
}
|
|
27
|
+
type HandlerRef<T, P> = {
|
|
28
|
+
current: T | null;
|
|
29
|
+
};
|
|
30
|
+
declare const _camera: import("react").ForwardRefExoticComponent<CameraProps & import("react").RefAttributes<HandlerRef<any, CameraProps>>>;
|
|
31
|
+
export default _camera;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { createElement, forwardRef, useRef, useCallback, useContext, useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { getCurrentPage, useTransformStyle, useLayout, extendObject } from './utils';
|
|
3
|
+
import useInnerProps, { getCustomEvent } from './getInnerListeners';
|
|
4
|
+
import { Camera, useCameraDevice, useCodeScanner, useCameraFormat } from 'react-native-vision-camera';
|
|
5
|
+
import { noop, warn, hasOwn } from '@mpxjs/utils';
|
|
6
|
+
import { RouteContext } from './context';
|
|
7
|
+
import { watch } from '@mpxjs/core';
|
|
8
|
+
const qualityValue = {
|
|
9
|
+
high: 90,
|
|
10
|
+
normal: 75,
|
|
11
|
+
low: 50,
|
|
12
|
+
original: 100
|
|
13
|
+
};
|
|
14
|
+
let RecordRes = null;
|
|
15
|
+
const _camera = forwardRef((props, ref) => {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
17
|
+
const cameraRef = useRef(null);
|
|
18
|
+
const { mode = 'normal', resolution = 'medium', 'device-position': devicePosition = 'back', flash = 'auto', 'frame-size': frameSize = 'medium', bindinitdone, bindstop, bindscancode, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, 'enable-var': enableVar, 'external-var-context': externalVarContext, style = {} } = props;
|
|
19
|
+
const styleObj = extendObject({}, style);
|
|
20
|
+
const { normalStyle, hasSelfPercent, setWidth, setHeight } = useTransformStyle(styleObj, {
|
|
21
|
+
enableVar,
|
|
22
|
+
externalVarContext,
|
|
23
|
+
parentFontSize,
|
|
24
|
+
parentWidth,
|
|
25
|
+
parentHeight
|
|
26
|
+
});
|
|
27
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: cameraRef });
|
|
28
|
+
const isPhoto = useRef(false);
|
|
29
|
+
isPhoto.current = mode === 'normal';
|
|
30
|
+
const device = useCameraDevice(devicePosition || 'back');
|
|
31
|
+
const { navigation, pageId } = useContext(RouteContext) || {};
|
|
32
|
+
const [zoomValue, setZoomValue] = useState(1);
|
|
33
|
+
const [isActive, setIsActive] = useState(true);
|
|
34
|
+
const [hasPermission, setHasPermission] = useState(null);
|
|
35
|
+
const page = getCurrentPage(pageId);
|
|
36
|
+
// 先定义常量,避免在条件判断后使用
|
|
37
|
+
const maxZoom = device?.maxZoom || 1;
|
|
38
|
+
const RESOLUTION_MAPPING = {
|
|
39
|
+
low: { width: 1280, height: 720 },
|
|
40
|
+
medium: { width: 1920, height: 1080 },
|
|
41
|
+
high: 'max'
|
|
42
|
+
};
|
|
43
|
+
const FRAME_SIZE_MAPPING = {
|
|
44
|
+
small: { width: 1280, height: 720 },
|
|
45
|
+
medium: { width: 1920, height: 1080 },
|
|
46
|
+
large: 'max'
|
|
47
|
+
};
|
|
48
|
+
const format = useCameraFormat(device, [
|
|
49
|
+
{
|
|
50
|
+
photoResolution: RESOLUTION_MAPPING[resolution],
|
|
51
|
+
videoResolution: FRAME_SIZE_MAPPING[frameSize] || RESOLUTION_MAPPING[resolution]
|
|
52
|
+
}
|
|
53
|
+
]);
|
|
54
|
+
const isScancode = useCallback((fail, complete) => {
|
|
55
|
+
if (!isPhoto.current) {
|
|
56
|
+
const result = {
|
|
57
|
+
errMsg: 'Not allow to invoke takePhoto in \'scanCode\' mode.'
|
|
58
|
+
};
|
|
59
|
+
fail(result);
|
|
60
|
+
complete(result);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}, []);
|
|
65
|
+
const codeScanner = useCodeScanner({
|
|
66
|
+
codeTypes: ['qr'],
|
|
67
|
+
onCodeScanned: (codes) => {
|
|
68
|
+
codes.forEach(code => {
|
|
69
|
+
const type = code.type === 'qr' ? 'QR_CODE' : code.type?.toUpperCase();
|
|
70
|
+
const frame = code.frame || {};
|
|
71
|
+
bindscancode && bindscancode(getCustomEvent('scancode', {}, {
|
|
72
|
+
detail: {
|
|
73
|
+
result: code.value,
|
|
74
|
+
type,
|
|
75
|
+
scanArea: [parseInt(frame.x) || 0, parseInt(frame.y) || 0, parseInt(frame.width) || 0, parseInt(frame.height) || 0]
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const onInitialized = useCallback(() => {
|
|
82
|
+
bindinitdone && bindinitdone(getCustomEvent('initdone', {}, {
|
|
83
|
+
detail: {
|
|
84
|
+
maxZoom
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
}, [bindinitdone, maxZoom]);
|
|
88
|
+
const onStopped = useCallback(() => {
|
|
89
|
+
bindstop && bindstop();
|
|
90
|
+
}, [bindstop]);
|
|
91
|
+
const camera = useMemo(() => ({
|
|
92
|
+
setZoom: (zoom) => {
|
|
93
|
+
setZoomValue(zoom);
|
|
94
|
+
},
|
|
95
|
+
takePhoto: (options = {}) => {
|
|
96
|
+
const { success = noop, fail = noop, complete = noop } = options;
|
|
97
|
+
if (isScancode(fail, complete))
|
|
98
|
+
return;
|
|
99
|
+
cameraRef.current?.takePhoto?.({
|
|
100
|
+
quality: qualityValue[options.quality || 'normal']
|
|
101
|
+
}).then((res) => {
|
|
102
|
+
const result = {
|
|
103
|
+
errMsg: 'takePhoto:ok',
|
|
104
|
+
tempImagePath: res.path
|
|
105
|
+
};
|
|
106
|
+
success(result);
|
|
107
|
+
complete(result);
|
|
108
|
+
}).catch(() => {
|
|
109
|
+
const result = {
|
|
110
|
+
errMsg: 'takePhoto:fail'
|
|
111
|
+
};
|
|
112
|
+
fail(result);
|
|
113
|
+
complete(result);
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
startRecord: (options = {}) => {
|
|
117
|
+
let { timeout = 30, success = noop, fail = noop, complete = noop, timeoutCallback = noop } = options;
|
|
118
|
+
timeout = timeout > 300 ? 300 : timeout;
|
|
119
|
+
let recordTimer = null;
|
|
120
|
+
if (isScancode(fail, complete))
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
const result = {
|
|
124
|
+
errMsg: 'startRecord:ok'
|
|
125
|
+
};
|
|
126
|
+
success(result);
|
|
127
|
+
complete(result);
|
|
128
|
+
cameraRef.current?.startRecording?.({
|
|
129
|
+
onRecordingError: (error) => {
|
|
130
|
+
if (recordTimer)
|
|
131
|
+
clearTimeout(recordTimer);
|
|
132
|
+
const errorResult = {
|
|
133
|
+
errMsg: 'startRecord:fail during recording',
|
|
134
|
+
error: error
|
|
135
|
+
};
|
|
136
|
+
timeoutCallback(errorResult);
|
|
137
|
+
},
|
|
138
|
+
onRecordingFinished: (video) => {
|
|
139
|
+
RecordRes = video;
|
|
140
|
+
if (recordTimer)
|
|
141
|
+
clearTimeout(recordTimer);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
recordTimer = setTimeout(() => {
|
|
145
|
+
cameraRef.current?.stopRecording().catch(() => {
|
|
146
|
+
// 忽略停止录制时的错误
|
|
147
|
+
});
|
|
148
|
+
}, timeout * 1000);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (recordTimer)
|
|
152
|
+
clearTimeout(recordTimer);
|
|
153
|
+
const result = {
|
|
154
|
+
errMsg: 'startRecord:fail ' + (error.message || 'unknown error')
|
|
155
|
+
};
|
|
156
|
+
fail(result);
|
|
157
|
+
complete(result);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
stopRecord: (options = {}) => {
|
|
161
|
+
const { success = noop, fail = noop, complete = noop } = options;
|
|
162
|
+
if (isScancode(fail, complete))
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
cameraRef.current?.stopRecording().then(() => {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
if (RecordRes) {
|
|
168
|
+
const result = {
|
|
169
|
+
errMsg: 'stopRecord:ok',
|
|
170
|
+
tempVideoPath: RecordRes?.path,
|
|
171
|
+
duration: RecordRes.duration * 1000 // 转成ms
|
|
172
|
+
};
|
|
173
|
+
RecordRes = null;
|
|
174
|
+
success(result);
|
|
175
|
+
complete(result);
|
|
176
|
+
}
|
|
177
|
+
}, 200); // 延时200ms,确保录制结果已准备好
|
|
178
|
+
}).catch((e) => {
|
|
179
|
+
const result = {
|
|
180
|
+
errMsg: 'stopRecord:fail ' + (e.message || 'promise rejected')
|
|
181
|
+
};
|
|
182
|
+
fail(result);
|
|
183
|
+
complete(result);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const result = {
|
|
188
|
+
errMsg: 'stopRecord:fail ' + (error.message || 'unknown error')
|
|
189
|
+
};
|
|
190
|
+
fail(result);
|
|
191
|
+
complete(result);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}), []);
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
let unWatch;
|
|
197
|
+
if (pageId && hasOwn(global.__mpxPageStatusMap, String(pageId))) {
|
|
198
|
+
unWatch = watch(() => global.__mpxPageStatusMap[pageId], (newVal) => {
|
|
199
|
+
if (newVal === 'show') {
|
|
200
|
+
if (page.id === pageId) {
|
|
201
|
+
setIsActive(true);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (newVal === 'hide') {
|
|
205
|
+
setIsActive(false);
|
|
206
|
+
}
|
|
207
|
+
}, { sync: true });
|
|
208
|
+
}
|
|
209
|
+
const checkCameraPermission = async () => {
|
|
210
|
+
try {
|
|
211
|
+
const cameraPermission = global?.__mpx?.config?.rnConfig?.cameraPermission;
|
|
212
|
+
if (typeof cameraPermission === 'function') {
|
|
213
|
+
const permissionResult = await cameraPermission();
|
|
214
|
+
setHasPermission(permissionResult === true);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
setHasPermission(true);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
setHasPermission(false);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
checkCameraPermission();
|
|
225
|
+
return () => {
|
|
226
|
+
if (navigation?.camera === camera) {
|
|
227
|
+
delete navigation.camera;
|
|
228
|
+
}
|
|
229
|
+
unWatch && unWatch();
|
|
230
|
+
};
|
|
231
|
+
}, []);
|
|
232
|
+
const innerProps = useInnerProps(extendObject({}, props, layoutProps, {
|
|
233
|
+
ref: cameraRef,
|
|
234
|
+
style: extendObject({}, normalStyle, layoutStyle),
|
|
235
|
+
isActive,
|
|
236
|
+
photo: true,
|
|
237
|
+
video: true,
|
|
238
|
+
onInitialized,
|
|
239
|
+
onStopped,
|
|
240
|
+
device,
|
|
241
|
+
format,
|
|
242
|
+
codeScanner: !isPhoto.current ? codeScanner : undefined,
|
|
243
|
+
zoom: zoomValue,
|
|
244
|
+
torch: flash
|
|
245
|
+
}), [
|
|
246
|
+
'mode',
|
|
247
|
+
'resolution',
|
|
248
|
+
'frame-size',
|
|
249
|
+
'bindinitdone',
|
|
250
|
+
'bindstop',
|
|
251
|
+
'flash',
|
|
252
|
+
'bindscancode',
|
|
253
|
+
'binderror'
|
|
254
|
+
], {
|
|
255
|
+
layoutRef
|
|
256
|
+
});
|
|
257
|
+
if (navigation && navigation.camera && navigation.camera !== camera) {
|
|
258
|
+
warn('<camera>: 一个页面只能插入一个');
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
else if (navigation) {
|
|
262
|
+
navigation.camera = camera;
|
|
263
|
+
}
|
|
264
|
+
if (!hasPermission || !device) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return createElement(Camera, innerProps);
|
|
268
|
+
});
|
|
269
|
+
_camera.displayName = 'MpxCamera';
|
|
270
|
+
export default _camera;
|
|
@@ -160,19 +160,96 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
}, [refresherTriggered]);
|
|
163
|
-
function scrollTo({ top = 0, left = 0, animated = false }) {
|
|
164
|
-
|
|
163
|
+
function scrollTo({ top = 0, left = 0, animated = false, duration }) {
|
|
164
|
+
// 如果指定了 duration 且需要动画,使用自定义动画
|
|
165
|
+
if (animated && duration && duration > 0) {
|
|
166
|
+
// 获取当前滚动位置
|
|
167
|
+
const currentY = scrollOptions.current.scrollTop || 0;
|
|
168
|
+
const currentX = scrollOptions.current.scrollLeft || 0;
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
const deltaY = top - currentY;
|
|
171
|
+
const deltaX = left - currentX;
|
|
172
|
+
// 缓动函数:easeInOutCubic
|
|
173
|
+
const easing = (t) => {
|
|
174
|
+
return t < 0.5
|
|
175
|
+
? 4 * t * t * t
|
|
176
|
+
: 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
177
|
+
};
|
|
178
|
+
// 使用 requestAnimationFrame 实现平滑动画
|
|
179
|
+
const animate = () => {
|
|
180
|
+
const elapsed = Date.now() - startTime;
|
|
181
|
+
const progress = Math.min(elapsed / duration, 1); // 0 到 1
|
|
182
|
+
const easeProgress = easing(progress);
|
|
183
|
+
const nextY = currentY + deltaY * easeProgress;
|
|
184
|
+
const nextX = currentX + deltaX * easeProgress;
|
|
185
|
+
if (scrollViewRef.current) {
|
|
186
|
+
scrollViewRef.current.scrollTo({ y: nextY, x: nextX, animated: false });
|
|
187
|
+
}
|
|
188
|
+
if (progress < 1) {
|
|
189
|
+
requestAnimationFrame(animate);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// 确保最终位置准确
|
|
193
|
+
if (scrollViewRef.current) {
|
|
194
|
+
scrollViewRef.current.scrollTo({ y: top, x: left, animated: false });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
requestAnimationFrame(animate);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// 使用原生的 scrollTo
|
|
202
|
+
scrollToOffset(left, top, animated);
|
|
203
|
+
}
|
|
165
204
|
}
|
|
166
|
-
function handleScrollIntoView(selector = '', { offset = 0, animated = true } = {}) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
function handleScrollIntoView(selector = '', { offset = 0, animated = true, duration = undefined } = {}) {
|
|
206
|
+
try {
|
|
207
|
+
const currentSelectRef = propsRef.current.__selectRef;
|
|
208
|
+
if (!currentSelectRef) {
|
|
209
|
+
const errMsg = '__selectRef is not available. Please ensure the scroll-view component is properly initialized.';
|
|
210
|
+
warn(errMsg);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const targetScrollView = scrollViewRef.current;
|
|
214
|
+
if (!targetScrollView) {
|
|
215
|
+
const errMsg = 'scrollViewRef is not ready';
|
|
216
|
+
warn(errMsg);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// scroll-into-view prop 按微信规范直传裸 id(如 "section-1"),而 __refs 注册时 key 带 # 或 . 前缀,需补齐才能命中;
|
|
220
|
+
// pageScrollTo 调用方已自带前缀(如 "#section-1")
|
|
221
|
+
const normalizedSelector = selector.startsWith('#') || selector.startsWith('.') ? selector : `#${selector}`;
|
|
222
|
+
// 调用 __selectRef 查找元素
|
|
223
|
+
const refs = currentSelectRef(normalizedSelector, 'node');
|
|
224
|
+
if (!refs) {
|
|
225
|
+
const errMsg = `Element not found for selector: ${normalizedSelector}`;
|
|
226
|
+
warn(errMsg);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const { nodeRef } = refs.getNodeInstance();
|
|
230
|
+
if (!nodeRef?.current) {
|
|
231
|
+
const errMsg = `Node ref not available for selector: ${normalizedSelector}`;
|
|
232
|
+
warn(errMsg);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
nodeRef.current.measureLayout(targetScrollView, (left, top) => {
|
|
236
|
+
const adjustedLeft = scrollX ? left + offset : left;
|
|
237
|
+
const adjustedTop = scrollY ? top + offset : top;
|
|
238
|
+
// 使用 scrollTo 方法,支持 duration 参数
|
|
239
|
+
if (duration !== undefined) {
|
|
240
|
+
scrollTo({ left: adjustedLeft, top: adjustedTop, animated, duration });
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
scrollToOffset(adjustedLeft, adjustedTop, animated);
|
|
244
|
+
}
|
|
245
|
+
}, (error) => {
|
|
246
|
+
warn(`Failed to measure layout for selector ${normalizedSelector}: ${error}`);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const errMsg = `handleScrollIntoView error for selector ${selector}: ${error?.message || error}`;
|
|
251
|
+
warn(errMsg);
|
|
252
|
+
}
|
|
176
253
|
}
|
|
177
254
|
function selectLength(size) {
|
|
178
255
|
return !scrollX ? size.height : size.width;
|
|
@@ -457,9 +534,9 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
457
534
|
}
|
|
458
535
|
// 处理下拉刷新的手势 - 使用 useMemo 避免每次渲染都创建
|
|
459
536
|
const panGesture = useMemo(() => {
|
|
460
|
-
if (!hasRefresher)
|
|
461
|
-
return Gesture.Pan(); // 返回空手势
|
|
462
537
|
return Gesture.Pan()
|
|
538
|
+
.activeOffsetY([-5, 5])
|
|
539
|
+
.failOffsetX([-5, 5])
|
|
463
540
|
.onUpdate((event) => {
|
|
464
541
|
'worklet';
|
|
465
542
|
if (enhanced && !!bounces) {
|
|
@@ -519,7 +596,7 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
519
596
|
}
|
|
520
597
|
})
|
|
521
598
|
.simultaneousWithExternalGesture(scrollViewRef);
|
|
522
|
-
}, [
|
|
599
|
+
}, [enhanced, bounces, refreshing, refresherThreshold]);
|
|
523
600
|
const scrollAdditionalProps = extendObject({
|
|
524
601
|
style: extendObject(hasOwn(innerStyle, 'flex') || hasOwn(innerStyle, 'flexGrow')
|
|
525
602
|
? {}
|
|
@@ -184,7 +184,7 @@ const _WebView = forwardRef((props, ref) => {
|
|
|
184
184
|
}
|
|
185
185
|
break;
|
|
186
186
|
case 'postMessage':
|
|
187
|
-
bindmessage && bindmessage(getCustomEvent('
|
|
187
|
+
bindmessage && bindmessage(getCustomEvent('message', {}, {
|
|
188
188
|
detail: {
|
|
189
189
|
data: params[0]?.data
|
|
190
190
|
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { createElement, forwardRef, useRef, useCallback, useContext, useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { getCurrentPage, useTransformStyle, useLayout, extendObject } from './utils'
|
|
3
|
+
import useInnerProps, { getCustomEvent } from './getInnerListeners'
|
|
4
|
+
import { Camera, useCameraDevice, useCodeScanner, useCameraFormat } from 'react-native-vision-camera'
|
|
5
|
+
import { noop, warn, hasOwn } from '@mpxjs/utils'
|
|
6
|
+
import { RouteContext } from './context'
|
|
7
|
+
import { watch, WatchOptions } from '@mpxjs/core'
|
|
8
|
+
|
|
9
|
+
const qualityValue = {
|
|
10
|
+
high: 90,
|
|
11
|
+
normal: 75,
|
|
12
|
+
low: 50,
|
|
13
|
+
original: 100
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CameraProps {
|
|
17
|
+
mode?: 'normal' | 'scanCode'
|
|
18
|
+
resolution?: 'low' | 'medium' | 'high'
|
|
19
|
+
'device-position'?: 'front' | 'back'
|
|
20
|
+
flash?: 'auto' | 'on' | 'off'
|
|
21
|
+
'frame-size'?: 'small' | 'medium' | 'large'
|
|
22
|
+
style?: Record<string, any>
|
|
23
|
+
bindstop?: () => void
|
|
24
|
+
binderror?: (error: { message: string }) => void
|
|
25
|
+
bindinitdone?: (result: { type: string, data: string }) => void
|
|
26
|
+
bindscancode?: (result: { type: string, data: string }) => void
|
|
27
|
+
'parent-font-size'?: number
|
|
28
|
+
'parent-width'?: number
|
|
29
|
+
'parent-height'?: number
|
|
30
|
+
'enable-var'?: boolean
|
|
31
|
+
'external-var-context'?: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface TakePhotoOptions {
|
|
35
|
+
quality?: 'high' | 'normal' | 'low' | 'original'
|
|
36
|
+
success?: (result: { errMsg: string, tempImagePath: string }) => void
|
|
37
|
+
fail?: (result: { errMsg: string }) => void
|
|
38
|
+
complete?: (result: { errMsg: string, tempImagePath?: string }) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RecordOptions {
|
|
42
|
+
timeout?: number
|
|
43
|
+
success?: (result: { errMsg: string }) => void
|
|
44
|
+
fail?: (result: { errMsg: string, error?: any }) => void
|
|
45
|
+
complete?: (result: { errMsg: string }) => void
|
|
46
|
+
timeoutCallback?: (result: { errMsg: string, error?: any }) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface StopRecordOptions {
|
|
50
|
+
success?: (result: { errMsg: string, tempVideoPath: string, duration: number }) => void
|
|
51
|
+
fail?: (result: { errMsg: string }) => void
|
|
52
|
+
complete?: (result: { errMsg: string, tempVideoPath?: string, duration?: number }) => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CameraRef {
|
|
56
|
+
setZoom: (zoom: number) => void
|
|
57
|
+
takePhoto: (options?: TakePhotoOptions) => void
|
|
58
|
+
startRecord: (options?: RecordOptions) => void
|
|
59
|
+
stopRecord: (options?: StopRecordOptions) => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type HandlerRef<T, P> = {
|
|
63
|
+
current: T | null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let RecordRes: any = null
|
|
67
|
+
|
|
68
|
+
const _camera = forwardRef<HandlerRef<any, CameraProps>, CameraProps>((props: CameraProps, ref): JSX.Element | null => {
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
70
|
+
const cameraRef = useRef<any>(null)
|
|
71
|
+
const {
|
|
72
|
+
mode = 'normal',
|
|
73
|
+
resolution = 'medium',
|
|
74
|
+
'device-position': devicePosition = 'back',
|
|
75
|
+
flash = 'auto',
|
|
76
|
+
'frame-size': frameSize = 'medium',
|
|
77
|
+
bindinitdone,
|
|
78
|
+
bindstop,
|
|
79
|
+
bindscancode,
|
|
80
|
+
'parent-font-size': parentFontSize,
|
|
81
|
+
'parent-width': parentWidth,
|
|
82
|
+
'parent-height': parentHeight,
|
|
83
|
+
'enable-var': enableVar,
|
|
84
|
+
'external-var-context': externalVarContext,
|
|
85
|
+
style = {}
|
|
86
|
+
} = props
|
|
87
|
+
const styleObj = extendObject(
|
|
88
|
+
{},
|
|
89
|
+
style
|
|
90
|
+
)
|
|
91
|
+
const {
|
|
92
|
+
normalStyle,
|
|
93
|
+
hasSelfPercent,
|
|
94
|
+
setWidth,
|
|
95
|
+
setHeight
|
|
96
|
+
} = useTransformStyle(styleObj, {
|
|
97
|
+
enableVar,
|
|
98
|
+
externalVarContext,
|
|
99
|
+
parentFontSize,
|
|
100
|
+
parentWidth,
|
|
101
|
+
parentHeight
|
|
102
|
+
})
|
|
103
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: cameraRef })
|
|
104
|
+
const isPhoto = useRef<boolean>(false)
|
|
105
|
+
isPhoto.current = mode === 'normal'
|
|
106
|
+
const device = useCameraDevice(devicePosition || 'back')
|
|
107
|
+
const { navigation, pageId } = useContext(RouteContext) || {}
|
|
108
|
+
const [zoomValue, setZoomValue] = useState<number>(1)
|
|
109
|
+
const [isActive, setIsActive] = useState<boolean>(true)
|
|
110
|
+
const [hasPermission, setHasPermission] = useState<boolean | null>(null)
|
|
111
|
+
const page = getCurrentPage(pageId)
|
|
112
|
+
|
|
113
|
+
// 先定义常量,避免在条件判断后使用
|
|
114
|
+
const maxZoom = device?.maxZoom || 1
|
|
115
|
+
const RESOLUTION_MAPPING: Record<string, { width: number, height: number } | 'max'> = {
|
|
116
|
+
low: { width: 1280, height: 720 },
|
|
117
|
+
medium: { width: 1920, height: 1080 },
|
|
118
|
+
high: 'max'
|
|
119
|
+
}
|
|
120
|
+
const FRAME_SIZE_MAPPING: Record<string, { width: number, height: number } | 'max'> = {
|
|
121
|
+
small: { width: 1280, height: 720 },
|
|
122
|
+
medium: { width: 1920, height: 1080 },
|
|
123
|
+
large: 'max'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const format = useCameraFormat(device, [
|
|
127
|
+
{
|
|
128
|
+
photoResolution: RESOLUTION_MAPPING[resolution],
|
|
129
|
+
videoResolution: FRAME_SIZE_MAPPING[frameSize] || RESOLUTION_MAPPING[resolution]
|
|
130
|
+
}
|
|
131
|
+
])
|
|
132
|
+
const isScancode = useCallback((fail: (res: { errMsg: string }) => void, complete: (res: { errMsg: string }) => void) => {
|
|
133
|
+
if (!isPhoto.current) {
|
|
134
|
+
const result = {
|
|
135
|
+
errMsg: 'Not allow to invoke takePhoto in \'scanCode\' mode.'
|
|
136
|
+
}
|
|
137
|
+
fail(result)
|
|
138
|
+
complete(result)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
return false
|
|
142
|
+
}, [])
|
|
143
|
+
const codeScanner = useCodeScanner({
|
|
144
|
+
codeTypes: ['qr'],
|
|
145
|
+
onCodeScanned: (codes: any[]) => {
|
|
146
|
+
codes.forEach(code => {
|
|
147
|
+
const type = code.type === 'qr' ? 'QR_CODE' : code.type?.toUpperCase()
|
|
148
|
+
const frame = code.frame || {}
|
|
149
|
+
bindscancode && bindscancode(getCustomEvent('scancode', {}, {
|
|
150
|
+
detail: {
|
|
151
|
+
result: code.value,
|
|
152
|
+
type,
|
|
153
|
+
scanArea: [parseInt(frame.x) || 0, parseInt(frame.y) || 0, parseInt(frame.width) || 0, parseInt(frame.height) || 0]
|
|
154
|
+
}
|
|
155
|
+
}))
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const onInitialized = useCallback(() => {
|
|
161
|
+
bindinitdone && bindinitdone(getCustomEvent('initdone', {}, {
|
|
162
|
+
detail: {
|
|
163
|
+
maxZoom
|
|
164
|
+
}
|
|
165
|
+
}))
|
|
166
|
+
}, [bindinitdone, maxZoom])
|
|
167
|
+
|
|
168
|
+
const onStopped = useCallback(() => {
|
|
169
|
+
bindstop && bindstop()
|
|
170
|
+
}, [bindstop])
|
|
171
|
+
|
|
172
|
+
const camera: CameraRef = useMemo(() => ({
|
|
173
|
+
setZoom: (zoom: number) => {
|
|
174
|
+
setZoomValue(zoom)
|
|
175
|
+
},
|
|
176
|
+
takePhoto: (options: TakePhotoOptions = {}) => {
|
|
177
|
+
const { success = noop, fail = noop, complete = noop } = options
|
|
178
|
+
if (isScancode(fail, complete)) return
|
|
179
|
+
cameraRef.current?.takePhoto?.({
|
|
180
|
+
quality: qualityValue[options.quality || 'normal'] as number
|
|
181
|
+
} as any).then((res: { path: any }) => {
|
|
182
|
+
const result = {
|
|
183
|
+
errMsg: 'takePhoto:ok',
|
|
184
|
+
tempImagePath: res.path
|
|
185
|
+
}
|
|
186
|
+
success(result)
|
|
187
|
+
complete(result)
|
|
188
|
+
}).catch(() => {
|
|
189
|
+
const result = {
|
|
190
|
+
errMsg: 'takePhoto:fail'
|
|
191
|
+
}
|
|
192
|
+
fail(result)
|
|
193
|
+
complete(result)
|
|
194
|
+
})
|
|
195
|
+
},
|
|
196
|
+
startRecord: (options: RecordOptions = {}) => {
|
|
197
|
+
let { timeout = 30, success = noop, fail = noop, complete = noop, timeoutCallback = noop } = options
|
|
198
|
+
timeout = timeout > 300 ? 300 : timeout
|
|
199
|
+
let recordTimer: NodeJS.Timeout | null = null
|
|
200
|
+
if (isScancode(fail, complete)) return
|
|
201
|
+
try {
|
|
202
|
+
const result = {
|
|
203
|
+
errMsg: 'startRecord:ok'
|
|
204
|
+
}
|
|
205
|
+
success(result)
|
|
206
|
+
complete(result)
|
|
207
|
+
|
|
208
|
+
cameraRef.current?.startRecording?.({
|
|
209
|
+
onRecordingError: (error: any) => {
|
|
210
|
+
if (recordTimer) clearTimeout(recordTimer)
|
|
211
|
+
const errorResult = {
|
|
212
|
+
errMsg: 'startRecord:fail during recording',
|
|
213
|
+
error: error
|
|
214
|
+
}
|
|
215
|
+
timeoutCallback(errorResult)
|
|
216
|
+
},
|
|
217
|
+
onRecordingFinished: (video: any) => {
|
|
218
|
+
RecordRes = video
|
|
219
|
+
if (recordTimer) clearTimeout(recordTimer)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
recordTimer = setTimeout(() => { // 超时自动停止
|
|
224
|
+
cameraRef.current?.stopRecording().catch(() => {
|
|
225
|
+
// 忽略停止录制时的错误
|
|
226
|
+
})
|
|
227
|
+
}, timeout * 1000)
|
|
228
|
+
} catch (error: any) {
|
|
229
|
+
if (recordTimer) clearTimeout(recordTimer)
|
|
230
|
+
const result = {
|
|
231
|
+
errMsg: 'startRecord:fail ' + (error.message || 'unknown error')
|
|
232
|
+
}
|
|
233
|
+
fail(result)
|
|
234
|
+
complete(result)
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
stopRecord: (options: StopRecordOptions = {}) => {
|
|
238
|
+
const { success = noop, fail = noop, complete = noop } = options
|
|
239
|
+
if (isScancode(fail, complete)) return
|
|
240
|
+
try {
|
|
241
|
+
cameraRef.current?.stopRecording().then(() => {
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
if (RecordRes) {
|
|
244
|
+
const result = {
|
|
245
|
+
errMsg: 'stopRecord:ok',
|
|
246
|
+
tempVideoPath: RecordRes?.path,
|
|
247
|
+
duration: RecordRes.duration * 1000 // 转成ms
|
|
248
|
+
}
|
|
249
|
+
RecordRes = null
|
|
250
|
+
success(result)
|
|
251
|
+
complete(result)
|
|
252
|
+
}
|
|
253
|
+
}, 200) // 延时200ms,确保录制结果已准备好
|
|
254
|
+
}).catch((e: any) => {
|
|
255
|
+
const result = {
|
|
256
|
+
errMsg: 'stopRecord:fail ' + (e.message || 'promise rejected')
|
|
257
|
+
}
|
|
258
|
+
fail(result)
|
|
259
|
+
complete(result)
|
|
260
|
+
})
|
|
261
|
+
} catch (error: any) {
|
|
262
|
+
const result = {
|
|
263
|
+
errMsg: 'stopRecord:fail ' + (error.message || 'unknown error')
|
|
264
|
+
}
|
|
265
|
+
fail(result)
|
|
266
|
+
complete(result)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}), [])
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
let unWatch: any
|
|
273
|
+
if (pageId && hasOwn(global.__mpxPageStatusMap, String(pageId))) {
|
|
274
|
+
unWatch = watch(() => global.__mpxPageStatusMap[pageId], (newVal: string) => {
|
|
275
|
+
if (newVal === 'show') {
|
|
276
|
+
if (page.id === pageId) {
|
|
277
|
+
setIsActive(true)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (newVal === 'hide') {
|
|
281
|
+
setIsActive(false)
|
|
282
|
+
}
|
|
283
|
+
}, { sync: true } as WatchOptions)
|
|
284
|
+
}
|
|
285
|
+
const checkCameraPermission = async () => {
|
|
286
|
+
try {
|
|
287
|
+
const cameraPermission = global?.__mpx?.config?.rnConfig?.cameraPermission
|
|
288
|
+
if (typeof cameraPermission === 'function') {
|
|
289
|
+
const permissionResult = await cameraPermission()
|
|
290
|
+
setHasPermission(permissionResult === true)
|
|
291
|
+
} else {
|
|
292
|
+
setHasPermission(true)
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
setHasPermission(false)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
checkCameraPermission()
|
|
299
|
+
return () => {
|
|
300
|
+
if (navigation?.camera === camera) {
|
|
301
|
+
delete navigation.camera
|
|
302
|
+
}
|
|
303
|
+
unWatch && unWatch()
|
|
304
|
+
}
|
|
305
|
+
}, [])
|
|
306
|
+
|
|
307
|
+
const innerProps:any = useInnerProps(
|
|
308
|
+
extendObject(
|
|
309
|
+
{},
|
|
310
|
+
props,
|
|
311
|
+
layoutProps,
|
|
312
|
+
{
|
|
313
|
+
ref: cameraRef,
|
|
314
|
+
style: extendObject({}, normalStyle, layoutStyle),
|
|
315
|
+
isActive,
|
|
316
|
+
photo: true,
|
|
317
|
+
video: true,
|
|
318
|
+
onInitialized,
|
|
319
|
+
onStopped,
|
|
320
|
+
device,
|
|
321
|
+
format,
|
|
322
|
+
codeScanner: !isPhoto.current ? codeScanner : undefined,
|
|
323
|
+
zoom: zoomValue,
|
|
324
|
+
torch: flash
|
|
325
|
+
}
|
|
326
|
+
),
|
|
327
|
+
[
|
|
328
|
+
'mode',
|
|
329
|
+
'resolution',
|
|
330
|
+
'frame-size',
|
|
331
|
+
'bindinitdone',
|
|
332
|
+
'bindstop',
|
|
333
|
+
'flash',
|
|
334
|
+
'bindscancode',
|
|
335
|
+
'binderror'
|
|
336
|
+
],
|
|
337
|
+
{
|
|
338
|
+
layoutRef
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if (navigation && navigation.camera && navigation.camera !== camera) {
|
|
343
|
+
warn('<camera>: 一个页面只能插入一个')
|
|
344
|
+
return null
|
|
345
|
+
} else if (navigation) {
|
|
346
|
+
navigation.camera = camera
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!hasPermission || !device) {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return createElement(Camera, innerProps)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
_camera.displayName = 'MpxCamera'
|
|
357
|
+
|
|
358
|
+
export default _camera
|
|
@@ -299,22 +299,112 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
299
299
|
}
|
|
300
300
|
}, [refresherTriggered])
|
|
301
301
|
|
|
302
|
-
function scrollTo ({ top = 0, left = 0, animated = false }: { top?: number; left?: number; animated?: boolean }) {
|
|
303
|
-
|
|
302
|
+
function scrollTo ({ top = 0, left = 0, animated = false, duration }: { top?: number; left?: number; animated?: boolean; duration?: number }) {
|
|
303
|
+
// 如果指定了 duration 且需要动画,使用自定义动画
|
|
304
|
+
if (animated && duration && duration > 0) {
|
|
305
|
+
// 获取当前滚动位置
|
|
306
|
+
const currentY = scrollOptions.current.scrollTop || 0
|
|
307
|
+
const currentX = scrollOptions.current.scrollLeft || 0
|
|
308
|
+
|
|
309
|
+
const startTime = Date.now()
|
|
310
|
+
const deltaY = top - currentY
|
|
311
|
+
const deltaX = left - currentX
|
|
312
|
+
|
|
313
|
+
// 缓动函数:easeInOutCubic
|
|
314
|
+
const easing = (t: number) => {
|
|
315
|
+
return t < 0.5
|
|
316
|
+
? 4 * t * t * t
|
|
317
|
+
: 1 - Math.pow(-2 * t + 2, 3) / 2
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 使用 requestAnimationFrame 实现平滑动画
|
|
321
|
+
const animate = () => {
|
|
322
|
+
const elapsed = Date.now() - startTime
|
|
323
|
+
const progress = Math.min(elapsed / duration, 1) // 0 到 1
|
|
324
|
+
|
|
325
|
+
const easeProgress = easing(progress)
|
|
326
|
+
const nextY = currentY + deltaY * easeProgress
|
|
327
|
+
const nextX = currentX + deltaX * easeProgress
|
|
328
|
+
|
|
329
|
+
if (scrollViewRef.current) {
|
|
330
|
+
scrollViewRef.current.scrollTo({ y: nextY, x: nextX, animated: false })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (progress < 1) {
|
|
334
|
+
requestAnimationFrame(animate)
|
|
335
|
+
} else {
|
|
336
|
+
// 确保最终位置准确
|
|
337
|
+
if (scrollViewRef.current) {
|
|
338
|
+
scrollViewRef.current.scrollTo({ y: top, x: left, animated: false })
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
requestAnimationFrame(animate)
|
|
344
|
+
} else {
|
|
345
|
+
// 使用原生的 scrollTo
|
|
346
|
+
scrollToOffset(left, top, animated)
|
|
347
|
+
}
|
|
304
348
|
}
|
|
305
349
|
|
|
306
|
-
function handleScrollIntoView (selector = '', { offset = 0, animated = true } = {}) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const adjustedTop = scrollY ? top + offset : top
|
|
315
|
-
scrollToOffset(adjustedLeft, adjustedTop, animated)
|
|
350
|
+
function handleScrollIntoView (selector = '', { offset = 0, animated = true, duration = undefined }: { offset?: number; animated?: boolean; duration?: number } = {}) {
|
|
351
|
+
try {
|
|
352
|
+
const currentSelectRef = propsRef.current.__selectRef
|
|
353
|
+
|
|
354
|
+
if (!currentSelectRef) {
|
|
355
|
+
const errMsg = '__selectRef is not available. Please ensure the scroll-view component is properly initialized.'
|
|
356
|
+
warn(errMsg)
|
|
357
|
+
return
|
|
316
358
|
}
|
|
317
|
-
|
|
359
|
+
|
|
360
|
+
const targetScrollView = scrollViewRef.current
|
|
361
|
+
|
|
362
|
+
if (!targetScrollView) {
|
|
363
|
+
const errMsg = 'scrollViewRef is not ready'
|
|
364
|
+
warn(errMsg)
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// scroll-into-view prop 按微信规范直传裸 id(如 "section-1"),而 __refs 注册时 key 带 # 或 . 前缀,需补齐才能命中;
|
|
369
|
+
// pageScrollTo 调用方已自带前缀(如 "#section-1")
|
|
370
|
+
const normalizedSelector = selector.startsWith('#') || selector.startsWith('.') ? selector : `#${selector}`
|
|
371
|
+
|
|
372
|
+
// 调用 __selectRef 查找元素
|
|
373
|
+
const refs = currentSelectRef(normalizedSelector, 'node')
|
|
374
|
+
if (!refs) {
|
|
375
|
+
const errMsg = `Element not found for selector: ${normalizedSelector}`
|
|
376
|
+
warn(errMsg)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const { nodeRef } = refs.getNodeInstance()
|
|
381
|
+
if (!nodeRef?.current) {
|
|
382
|
+
const errMsg = `Node ref not available for selector: ${normalizedSelector}`
|
|
383
|
+
warn(errMsg)
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
nodeRef.current.measureLayout(
|
|
388
|
+
targetScrollView,
|
|
389
|
+
(left: number, top: number) => {
|
|
390
|
+
const adjustedLeft = scrollX ? left + offset : left
|
|
391
|
+
const adjustedTop = scrollY ? top + offset : top
|
|
392
|
+
|
|
393
|
+
// 使用 scrollTo 方法,支持 duration 参数
|
|
394
|
+
if (duration !== undefined) {
|
|
395
|
+
scrollTo({ left: adjustedLeft, top: adjustedTop, animated, duration })
|
|
396
|
+
} else {
|
|
397
|
+
scrollToOffset(adjustedLeft, adjustedTop, animated)
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
(error: any) => {
|
|
401
|
+
warn(`Failed to measure layout for selector ${normalizedSelector}: ${error}`)
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
} catch (error: any) {
|
|
405
|
+
const errMsg = `handleScrollIntoView error for selector ${selector}: ${error?.message || error}`
|
|
406
|
+
warn(errMsg)
|
|
407
|
+
}
|
|
318
408
|
}
|
|
319
409
|
|
|
320
410
|
function selectLength (size: { height: number; width: number }) {
|
|
@@ -641,9 +731,9 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
641
731
|
|
|
642
732
|
// 处理下拉刷新的手势 - 使用 useMemo 避免每次渲染都创建
|
|
643
733
|
const panGesture = useMemo(() => {
|
|
644
|
-
if (!hasRefresher) return Gesture.Pan() // 返回空手势
|
|
645
|
-
|
|
646
734
|
return Gesture.Pan()
|
|
735
|
+
.activeOffsetY([-5, 5])
|
|
736
|
+
.failOffsetX([-5, 5])
|
|
647
737
|
.onUpdate((event) => {
|
|
648
738
|
'worklet'
|
|
649
739
|
if (enhanced && !!bounces) {
|
|
@@ -703,7 +793,7 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
703
793
|
}
|
|
704
794
|
})
|
|
705
795
|
.simultaneousWithExternalGesture(scrollViewRef)
|
|
706
|
-
}, [
|
|
796
|
+
}, [enhanced, bounces, refreshing, refresherThreshold])
|
|
707
797
|
|
|
708
798
|
const scrollAdditionalProps: ScrollAdditionalProps = extendObject(
|
|
709
799
|
{
|
|
@@ -234,7 +234,7 @@ const _WebView = forwardRef<HandlerRef<WebView, WebViewProps>, WebViewProps>((pr
|
|
|
234
234
|
}
|
|
235
235
|
break
|
|
236
236
|
case 'postMessage':
|
|
237
|
-
bindmessage && bindmessage(getCustomEvent('
|
|
237
|
+
bindmessage && bindmessage(getCustomEvent('message', {}, { // RN组件销毁顺序与小程序不一致,所以改成和支付宝消息一致
|
|
238
238
|
detail: {
|
|
239
239
|
data: params[0]?.data
|
|
240
240
|
}
|
|
@@ -91,7 +91,7 @@ const isBuildInReactTag = makeMap(
|
|
|
91
91
|
'mpx-movable-area,mpx-label,mpx-input,' +
|
|
92
92
|
'mpx-image,mpx-form,mpx-checkbox,mpx-checkbox-group,mpx-button,' +
|
|
93
93
|
'mpx-rich-text,mpx-picker-view-column,mpx-picker-view,mpx-picker,' +
|
|
94
|
-
'mpx-icon,mpx-canvas'
|
|
94
|
+
'mpx-icon,mpx-canvas,mpx-camera'
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
const isSpace = makeMap('ensp,emsp,nbsp')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpxjs/webpack-plugin",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.19",
|
|
4
4
|
"description": "mpx compile core",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mpx"
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@d11/react-native-fast-image": "^8.6.12",
|
|
87
|
-
"@mpxjs/api-proxy": "^2.10.
|
|
87
|
+
"@mpxjs/api-proxy": "^2.10.19",
|
|
88
88
|
"@types/babel-traverse": "^6.25.4",
|
|
89
89
|
"@types/babel-types": "^7.0.4",
|
|
90
90
|
"@types/glob": "^8.1.0",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"react-native-safe-area-context": "^4.12.0",
|
|
98
98
|
"react-native-svg": "^15.8.0",
|
|
99
99
|
"react-native-video": "^6.9.0",
|
|
100
|
+
"react-native-vision-camera": "^4.7.2",
|
|
100
101
|
"react-native-webview": "^13.12.2",
|
|
101
102
|
"rimraf": "^6.0.1",
|
|
102
103
|
"webpack": "^5.75.0"
|
|
@@ -104,5 +105,5 @@
|
|
|
104
105
|
"engines": {
|
|
105
106
|
"node": ">=14.14.0"
|
|
106
107
|
},
|
|
107
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "ca50ba6b1361f3c7790746476dd8b8e6be802ea3"
|
|
108
109
|
}
|