@mpxjs/webpack-plugin 2.10.15-4 → 2.10.15-prelease.1
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/dependencies/DynamicEntryDependency.js +1 -1
- package/lib/dependencies/ImportDependency.js +102 -0
- package/lib/{retry-runtime-module.js → dependencies/RetryRuntimeModule.js} +1 -1
- package/lib/index.js +13 -15
- package/lib/platform/template/wx/component-config/progress.js +12 -0
- package/lib/platform/template/wx/component-config/slider.js +12 -0
- package/lib/platform/template/wx/component-config/unsupported.js +1 -1
- package/lib/platform/template/wx/index.js +3 -1
- package/lib/resolver/AddEnvPlugin.js +13 -0
- package/lib/resolver/AddModePlugin.js +18 -0
- package/lib/runtime/components/react/dist/getInnerListeners.js +35 -21
- package/lib/runtime/components/react/dist/mpx-movable-view.jsx +102 -34
- package/lib/runtime/components/react/dist/mpx-portal/portal-manager.jsx +3 -5
- package/lib/runtime/components/react/dist/mpx-progress.jsx +163 -0
- package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +51 -9
- package/lib/runtime/components/react/dist/mpx-slider.jsx +321 -0
- package/lib/runtime/components/react/dist/mpx-swiper.jsx +9 -16
- package/lib/runtime/components/react/dist/mpx-view.jsx +7 -10
- package/lib/runtime/components/react/dist/mpx-web-view.jsx +20 -1
- package/lib/runtime/components/react/getInnerListeners.ts +41 -22
- package/lib/runtime/components/react/mpx-movable-view.tsx +156 -48
- package/lib/runtime/components/react/mpx-portal/portal-manager.tsx +4 -8
- package/lib/runtime/components/react/mpx-progress.tsx +259 -0
- package/lib/runtime/components/react/mpx-scroll-view.tsx +54 -9
- package/lib/runtime/components/react/mpx-slider.tsx +444 -0
- package/lib/runtime/components/react/mpx-swiper.tsx +9 -16
- package/lib/runtime/components/react/mpx-view.tsx +7 -10
- package/lib/runtime/components/react/mpx-web-view.tsx +22 -1
- package/lib/runtime/components/react/types/getInnerListeners.d.ts +7 -2
- package/lib/runtime/components/web/mpx-input.vue +14 -0
- package/lib/runtime/components/web/mpx-movable-area.vue +43 -19
- package/lib/runtime/components/web/mpx-movable-view.vue +93 -3
- package/lib/runtime/components/web/mpx-scroll-view.vue +7 -1
- package/lib/runtime/components/web/mpx-swiper.vue +1 -2
- package/lib/runtime/components/web/mpx-video.vue +12 -1
- package/lib/runtime/components/web/mpx-web-view.vue +3 -3
- package/lib/runtime/optionProcessor.js +3 -1
- package/lib/runtime/optionProcessorReact.js +4 -2
- package/lib/template-compiler/compiler.js +69 -35
- package/lib/utils/chain-assign.js +47 -0
- package/lib/utils/check-core-version-match.js +75 -15
- package/lib/wxs/pre-loader.js +5 -5
- package/lib/wxss/utils.js +1 -1
- package/package.json +3 -2
- package/lib/dependencies/ImportDependencyTemplate.js +0 -50
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✔ percent 进度百分比 0-100
|
|
3
|
+
* ✘ show-info 在进度条右侧显示百分比
|
|
4
|
+
* ✘ border-radius 圆角大小
|
|
5
|
+
* ✘ font-size 右侧百分比字体大小
|
|
6
|
+
* ✔ stroke-width 进度条线的宽度
|
|
7
|
+
* ✔ color 进度条颜色(请使用activeColor)
|
|
8
|
+
* ✔ activeColor 已选择的进度条的颜色
|
|
9
|
+
* ✔ backgroundColor 未选择的进度条的颜色
|
|
10
|
+
* ✔ active 进度条从左往右的动画
|
|
11
|
+
* ✔ active-mode backwards: 动画从头播;forwards:动画从上次结束点接着播
|
|
12
|
+
* ✔ duration 进度增加1%所需毫秒数
|
|
13
|
+
* ✔ bindactiveend 动画完成事件
|
|
14
|
+
*/
|
|
15
|
+
import { useRef, forwardRef, useEffect, useState, createElement } from 'react';
|
|
16
|
+
import { View } from 'react-native';
|
|
17
|
+
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, runOnJS } from 'react-native-reanimated';
|
|
18
|
+
import useInnerProps from './getInnerListeners';
|
|
19
|
+
import useNodesRef from './useNodesRef';
|
|
20
|
+
import { useLayout, useTransformStyle, extendObject, useRunOnJSCallback } from './utils';
|
|
21
|
+
import Portal from './mpx-portal';
|
|
22
|
+
const Progress = forwardRef((props, ref) => {
|
|
23
|
+
const { percent = 0, 'stroke-width': strokeWidth = 6, color, activeColor = color || '#09BB07', backgroundColor = '#EBEBEB', active = false, 'active-mode': activeMode = 'backwards', duration = 30, style = {}, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props;
|
|
24
|
+
const nodeRef = useRef(null);
|
|
25
|
+
const propsRef = useRef(props);
|
|
26
|
+
propsRef.current = props;
|
|
27
|
+
// 进度值状态
|
|
28
|
+
const [lastPercent, setLastPercent] = useState(0);
|
|
29
|
+
const progressWidth = useSharedValue(0);
|
|
30
|
+
const { normalStyle, hasSelfPercent, setWidth, setHeight, hasPositionFixed } = useTransformStyle(style, {
|
|
31
|
+
enableVar,
|
|
32
|
+
externalVarContext,
|
|
33
|
+
parentFontSize,
|
|
34
|
+
parentWidth,
|
|
35
|
+
parentHeight
|
|
36
|
+
});
|
|
37
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({
|
|
38
|
+
props,
|
|
39
|
+
hasSelfPercent,
|
|
40
|
+
setWidth,
|
|
41
|
+
setHeight,
|
|
42
|
+
nodeRef
|
|
43
|
+
});
|
|
44
|
+
useNodesRef(props, ref, nodeRef, {
|
|
45
|
+
style: normalStyle
|
|
46
|
+
});
|
|
47
|
+
// 使用 useRunOnJSCallback 处理动画回调
|
|
48
|
+
const runOnJSCallbackRef = useRef({
|
|
49
|
+
triggerActiveEnd: (percent) => {
|
|
50
|
+
const currentProps = propsRef.current;
|
|
51
|
+
if (currentProps.bindactiveend) {
|
|
52
|
+
currentProps.bindactiveend({
|
|
53
|
+
type: 'activeend',
|
|
54
|
+
detail: {
|
|
55
|
+
percent: percent
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef);
|
|
62
|
+
// 进度条动画函数
|
|
63
|
+
const startProgressAnimation = (targetPercent, startPercent, animationDuration, onFinished) => {
|
|
64
|
+
// 根据 active-mode 设置起始位置
|
|
65
|
+
progressWidth.value = startPercent;
|
|
66
|
+
progressWidth.value = withTiming(targetPercent, {
|
|
67
|
+
duration: animationDuration,
|
|
68
|
+
easing: Easing.linear
|
|
69
|
+
}, (finished) => {
|
|
70
|
+
if (finished && onFinished) {
|
|
71
|
+
// 在动画回调中,执行传入的worklet函数
|
|
72
|
+
onFinished();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
// 进度变化时的动画效果
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const targetPercent = Math.max(0, Math.min(100, percent));
|
|
79
|
+
if (active) {
|
|
80
|
+
// 根据 active-mode 确定起始位置
|
|
81
|
+
let startPercent;
|
|
82
|
+
if (activeMode === 'backwards') {
|
|
83
|
+
startPercent = 0;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// forwards 模式:使用上次记录的百分比作为起始位置
|
|
87
|
+
startPercent = lastPercent;
|
|
88
|
+
}
|
|
89
|
+
// 计算动画持续时间
|
|
90
|
+
const percentDiff = Math.abs(targetPercent - startPercent);
|
|
91
|
+
const animationDuration = percentDiff * duration;
|
|
92
|
+
// 执行动画
|
|
93
|
+
startProgressAnimation(targetPercent, startPercent, animationDuration, () => {
|
|
94
|
+
'worklet';
|
|
95
|
+
// 在worklet函数内部执行runOnJS调用runOnJSCallback
|
|
96
|
+
runOnJS(runOnJSCallback)('triggerActiveEnd', targetPercent);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
progressWidth.value = targetPercent;
|
|
101
|
+
}
|
|
102
|
+
setLastPercent(targetPercent);
|
|
103
|
+
}, [percent, active, activeMode, duration]);
|
|
104
|
+
// 初始化时设置进度值
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!active) {
|
|
107
|
+
progressWidth.value = Math.max(0, Math.min(100, percent));
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
// 进度条动画样式
|
|
111
|
+
const animatedProgressStyle = useAnimatedStyle(() => {
|
|
112
|
+
return {
|
|
113
|
+
width: `${progressWidth.value}%`
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
// 确保数值类型正确
|
|
117
|
+
const strokeWidthNum = typeof strokeWidth === 'number' ? strokeWidth : parseInt(strokeWidth, 10) || 6;
|
|
118
|
+
// 容器样式
|
|
119
|
+
const containerStyle = extendObject({}, {
|
|
120
|
+
flexDirection: 'row',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
width: '100%',
|
|
123
|
+
minHeight: Math.max(strokeWidthNum, 20)
|
|
124
|
+
}, normalStyle, layoutStyle);
|
|
125
|
+
// 进度条背景样式
|
|
126
|
+
const progressBgStyle = {
|
|
127
|
+
width: '100%',
|
|
128
|
+
height: strokeWidthNum,
|
|
129
|
+
backgroundColor,
|
|
130
|
+
overflow: 'hidden'
|
|
131
|
+
};
|
|
132
|
+
// 进度条填充样式
|
|
133
|
+
const progressFillStyle = {
|
|
134
|
+
height: '100%',
|
|
135
|
+
backgroundColor: activeColor
|
|
136
|
+
};
|
|
137
|
+
const innerProps = useInnerProps(extendObject({}, props, layoutProps, {
|
|
138
|
+
ref: nodeRef
|
|
139
|
+
}), [
|
|
140
|
+
'percent',
|
|
141
|
+
'stroke-width',
|
|
142
|
+
'color',
|
|
143
|
+
'activeColor',
|
|
144
|
+
'backgroundColor',
|
|
145
|
+
'active',
|
|
146
|
+
'active-mode',
|
|
147
|
+
'duration',
|
|
148
|
+
'bindactiveend'
|
|
149
|
+
], { layoutRef });
|
|
150
|
+
const progressComponent = createElement(View, extendObject({}, innerProps, { style: containerStyle }),
|
|
151
|
+
// 进度条背景
|
|
152
|
+
createElement(View, { style: progressBgStyle },
|
|
153
|
+
// 进度条填充
|
|
154
|
+
createElement(Animated.View, {
|
|
155
|
+
style: [progressFillStyle, animatedProgressStyle]
|
|
156
|
+
})));
|
|
157
|
+
if (hasPositionFixed) {
|
|
158
|
+
return createElement(Portal, null, progressComponent);
|
|
159
|
+
}
|
|
160
|
+
return progressComponent;
|
|
161
|
+
});
|
|
162
|
+
Progress.displayName = 'MpxProgress';
|
|
163
|
+
export default Progress;
|
|
@@ -74,11 +74,22 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
74
74
|
black: ['#000'],
|
|
75
75
|
white: ['#fff']
|
|
76
76
|
};
|
|
77
|
+
const isContentSizeChange = useRef(false);
|
|
77
78
|
const { refresherContent, otherContent } = getRefresherContent(props.children);
|
|
78
79
|
const hasRefresher = refresherContent && refresherEnabled;
|
|
79
80
|
const { normalStyle, hasVarDec, varContextRef, hasSelfPercent, hasPositionFixed, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
|
|
80
81
|
const { textStyle, innerStyle = {} } = splitStyle(normalStyle);
|
|
81
82
|
const scrollViewRef = useRef(null);
|
|
83
|
+
const propsRef = useRef(props);
|
|
84
|
+
const refresherStateRef = useRef({
|
|
85
|
+
hasRefresher,
|
|
86
|
+
refresherTriggered
|
|
87
|
+
});
|
|
88
|
+
propsRef.current = props;
|
|
89
|
+
refresherStateRef.current = {
|
|
90
|
+
hasRefresher,
|
|
91
|
+
refresherTriggered
|
|
92
|
+
};
|
|
82
93
|
const runOnJSCallbackRef = useRef({
|
|
83
94
|
setEnableScroll,
|
|
84
95
|
setScrollBounces,
|
|
@@ -213,7 +224,22 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
function onContentSizeChange(width, height) {
|
|
216
|
-
|
|
227
|
+
isContentSizeChange.current = true;
|
|
228
|
+
const newContentLength = selectLength({ height, width });
|
|
229
|
+
const oldContentLength = scrollOptions.current.contentLength;
|
|
230
|
+
scrollOptions.current.contentLength = newContentLength;
|
|
231
|
+
// 内容高度变化时,Animated.event 的映射可能会有不生效的场景,所以需要手动设置一下 scrollOffset 的值
|
|
232
|
+
if (enableSticky && (__mpx_mode__ === 'android' || __mpx_mode__ === 'ios')) {
|
|
233
|
+
// 当内容变少时,检查当前滚动位置是否超出新的内容范围
|
|
234
|
+
if (newContentLength < oldContentLength) {
|
|
235
|
+
const { visibleLength, offset } = scrollOptions.current;
|
|
236
|
+
const maxOffset = Math.max(0, newContentLength - visibleLength);
|
|
237
|
+
// 如果当前滚动位置超出了新的内容范围,调整滚动offset
|
|
238
|
+
if (offset > maxOffset && scrollY) {
|
|
239
|
+
scrollOffset.setValue(maxOffset);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
217
243
|
}
|
|
218
244
|
function onLayout(e) {
|
|
219
245
|
const layout = e.nativeEvent.layout || {};
|
|
@@ -233,8 +259,9 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
233
259
|
}
|
|
234
260
|
function onScroll(e) {
|
|
235
261
|
const { bindscroll } = props;
|
|
236
|
-
const {
|
|
237
|
-
const {
|
|
262
|
+
const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
|
|
263
|
+
const { x: scrollLeft, y: scrollTop } = contentOffset;
|
|
264
|
+
const { width: scrollWidth, height: scrollHeight } = contentSize;
|
|
238
265
|
isAtTop.value = scrollTop <= 0;
|
|
239
266
|
bindscroll &&
|
|
240
267
|
bindscroll(getCustomEvent('scroll', e, {
|
|
@@ -244,7 +271,8 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
244
271
|
scrollHeight,
|
|
245
272
|
scrollWidth,
|
|
246
273
|
deltaX: scrollLeft - scrollOptions.current.scrollLeft,
|
|
247
|
-
deltaY: scrollTop - scrollOptions.current.scrollTop
|
|
274
|
+
deltaY: scrollTop - scrollOptions.current.scrollTop,
|
|
275
|
+
layoutMeasurement
|
|
248
276
|
},
|
|
249
277
|
layoutRef
|
|
250
278
|
}, props));
|
|
@@ -257,8 +285,9 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
257
285
|
}
|
|
258
286
|
function onScrollEnd(e) {
|
|
259
287
|
const { bindscrollend } = props;
|
|
260
|
-
const {
|
|
261
|
-
const {
|
|
288
|
+
const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
|
|
289
|
+
const { x: scrollLeft, y: scrollTop } = contentOffset;
|
|
290
|
+
const { width: scrollWidth, height: scrollHeight } = contentSize;
|
|
262
291
|
isAtTop.value = scrollTop <= 0;
|
|
263
292
|
bindscrollend &&
|
|
264
293
|
bindscrollend(getCustomEvent('scrollend', e, {
|
|
@@ -266,7 +295,8 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
266
295
|
scrollLeft,
|
|
267
296
|
scrollTop,
|
|
268
297
|
scrollHeight,
|
|
269
|
-
scrollWidth
|
|
298
|
+
scrollWidth,
|
|
299
|
+
layoutMeasurement
|
|
270
300
|
},
|
|
271
301
|
layoutRef
|
|
272
302
|
}, props));
|
|
@@ -313,6 +343,16 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
313
343
|
const scrollHandler = RNAnimated.event([{ nativeEvent: { contentOffset: { y: scrollOffset } } }], {
|
|
314
344
|
useNativeDriver: true,
|
|
315
345
|
listener: (event) => {
|
|
346
|
+
const y = event.nativeEvent.contentOffset.y || 0;
|
|
347
|
+
// 内容高度变化时,鸿蒙中 listener 回调通过scrollOffset.__getValue获取值一直等于event.nativeEvent.contentOffset.y,值是正确的,但是无法触发 sticky 动画执行,所以需要手动再 set 一次
|
|
348
|
+
if (__mpx_mode__ === 'harmony') {
|
|
349
|
+
if (isContentSizeChange.current) {
|
|
350
|
+
scrollOffset.setValue(y);
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
isContentSizeChange.current = false;
|
|
353
|
+
}, 100);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
316
356
|
onScroll(event);
|
|
317
357
|
}
|
|
318
358
|
});
|
|
@@ -347,6 +387,7 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
347
387
|
}
|
|
348
388
|
// 处理刷新
|
|
349
389
|
function onRefresh() {
|
|
390
|
+
const { hasRefresher, refresherTriggered } = refresherStateRef.current;
|
|
350
391
|
if (hasRefresher && refresherTriggered === undefined) {
|
|
351
392
|
// 处理使用了自定义刷新组件,又没设置 refresherTriggered 的情况
|
|
352
393
|
setRefreshing(true);
|
|
@@ -358,9 +399,9 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
358
399
|
}
|
|
359
400
|
}, 500);
|
|
360
401
|
}
|
|
361
|
-
const { bindrefresherrefresh } =
|
|
402
|
+
const { bindrefresherrefresh } = propsRef.current;
|
|
362
403
|
bindrefresherrefresh &&
|
|
363
|
-
bindrefresherrefresh(getCustomEvent('refresherrefresh', {}, { layoutRef },
|
|
404
|
+
bindrefresherrefresh(getCustomEvent('refresherrefresh', {}, { layoutRef }, propsRef.current));
|
|
364
405
|
}
|
|
365
406
|
function getRefresherContent(children) {
|
|
366
407
|
let refresherContent = null;
|
|
@@ -499,6 +540,7 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
499
540
|
showsVerticalScrollIndicator: scrollY && showScrollbar,
|
|
500
541
|
scrollEnabled: !enableScroll ? false : !!(scrollX || scrollY),
|
|
501
542
|
bounces: false,
|
|
543
|
+
overScrollMode: 'never',
|
|
502
544
|
ref: scrollViewRef,
|
|
503
545
|
onScroll: enableSticky ? scrollHandler : onScroll,
|
|
504
546
|
onContentSizeChange: onContentSizeChange,
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✔ min 最小值
|
|
3
|
+
* ✔ max 最大值
|
|
4
|
+
* ✔ step 步长
|
|
5
|
+
* ✔ disabled 是否禁用
|
|
6
|
+
* ✔ value 当前取值
|
|
7
|
+
* ✔ color 背景条的颜色(已废弃,使用 backgroundColor)
|
|
8
|
+
* ✔ selected-color 已选择的颜色(已废弃,使用 activeColor)
|
|
9
|
+
* ✔ activeColor 已选择的颜色
|
|
10
|
+
* ✔ backgroundColor 背景条的颜色
|
|
11
|
+
* ✔ block-size 滑块的大小
|
|
12
|
+
* ✔ block-color 滑块的颜色
|
|
13
|
+
* ✘ show-value 是否显示当前 value
|
|
14
|
+
* ✔ bindchange 完成一次拖动后触发的事件
|
|
15
|
+
* ✔ bindchanging 拖动过程中触发的事件
|
|
16
|
+
*/
|
|
17
|
+
import { useRef, forwardRef, useEffect, useState, createElement, useContext, useMemo } from 'react';
|
|
18
|
+
import { View } from 'react-native';
|
|
19
|
+
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
|
|
20
|
+
import Animated, { useSharedValue, useAnimatedStyle, runOnJS } from 'react-native-reanimated';
|
|
21
|
+
import { warn } from '@mpxjs/utils';
|
|
22
|
+
import useInnerProps, { getCustomEvent } from './getInnerListeners';
|
|
23
|
+
import useNodesRef from './useNodesRef';
|
|
24
|
+
import { useLayout, useTransformStyle, extendObject, useRunOnJSCallback } from './utils';
|
|
25
|
+
import Portal from './mpx-portal';
|
|
26
|
+
import { FormContext } from './context';
|
|
27
|
+
const Slider = forwardRef((props, ref) => {
|
|
28
|
+
const { min: rawMin = 0, max: rawMax = 100, step: rawStep = 1, disabled = false, value: rawValue, color, 'selected-color': selectedColor, activeColor = selectedColor || color || '#1aad19', backgroundColor = color || '#e9e9e9', 'block-size': rawBlockSize = 28, 'block-color': blockColor = '#ffffff', name, style = {}, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props;
|
|
29
|
+
// 确保数值类型正确
|
|
30
|
+
const min = typeof rawMin === 'string' ? parseFloat(rawMin) : rawMin;
|
|
31
|
+
const max = typeof rawMax === 'string' ? parseFloat(rawMax) : rawMax;
|
|
32
|
+
const step = typeof rawStep === 'string' ? parseFloat(rawStep) : rawStep;
|
|
33
|
+
const value = rawValue !== undefined ? (typeof rawValue === 'string' ? parseFloat(rawValue) : rawValue) : undefined;
|
|
34
|
+
const blockSize = typeof rawBlockSize === 'string' ? parseFloat(rawBlockSize) : rawBlockSize;
|
|
35
|
+
// 如果没有提供 value,则使用 min 作为默认值
|
|
36
|
+
const defaultValue = value !== undefined ? value : min;
|
|
37
|
+
const nodeRef = useRef(null);
|
|
38
|
+
const trackRef = useRef(null);
|
|
39
|
+
const [currentValue, setCurrentValue] = useState(defaultValue);
|
|
40
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
41
|
+
const thumbPosition = useSharedValue(0);
|
|
42
|
+
const isDragging = useSharedValue(false);
|
|
43
|
+
const startDragPosition = useSharedValue(0); // 记录拖拽开始时的位置
|
|
44
|
+
const startDragValue = useSharedValue(0); // 记录拖拽开始时的值
|
|
45
|
+
let formValuesMap;
|
|
46
|
+
const propsRef = useRef(props);
|
|
47
|
+
propsRef.current = props;
|
|
48
|
+
const formContext = useContext(FormContext);
|
|
49
|
+
if (formContext) {
|
|
50
|
+
formValuesMap = formContext.formValuesMap;
|
|
51
|
+
}
|
|
52
|
+
const { normalStyle, hasSelfPercent, setWidth, setHeight, hasPositionFixed } = useTransformStyle(style, {
|
|
53
|
+
enableVar,
|
|
54
|
+
externalVarContext,
|
|
55
|
+
parentFontSize,
|
|
56
|
+
parentWidth,
|
|
57
|
+
parentHeight
|
|
58
|
+
});
|
|
59
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({
|
|
60
|
+
props,
|
|
61
|
+
hasSelfPercent,
|
|
62
|
+
setWidth,
|
|
63
|
+
setHeight,
|
|
64
|
+
nodeRef
|
|
65
|
+
});
|
|
66
|
+
useNodesRef(props, ref, nodeRef, {
|
|
67
|
+
style: normalStyle
|
|
68
|
+
});
|
|
69
|
+
// 使用 useRunOnJSCallback 处理手势回调
|
|
70
|
+
const runOnJSCallbackRef = useRef({
|
|
71
|
+
triggerChangeEvent: (newValue) => {
|
|
72
|
+
setCurrentValue(newValue);
|
|
73
|
+
const currentProps = propsRef.current;
|
|
74
|
+
const changeHandler = currentProps.bindchange || currentProps.catchchange;
|
|
75
|
+
if (changeHandler) {
|
|
76
|
+
changeHandler(getCustomEvent('change', {}, { layoutRef, detail: { value: newValue } }, currentProps));
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
triggerChangingEvent: (newValue) => {
|
|
80
|
+
const currentProps = propsRef.current;
|
|
81
|
+
const changingHandler = currentProps.bindchanging || currentProps.catchchanging;
|
|
82
|
+
if (changingHandler) {
|
|
83
|
+
changingHandler(getCustomEvent('changing', {}, { layoutRef, detail: { value: newValue } }, currentProps));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef);
|
|
88
|
+
// 限制步长,确保 step 大于 0,并且可被 (max - min) 整除
|
|
89
|
+
const validateStep = (step, min, max) => {
|
|
90
|
+
if (step <= 0)
|
|
91
|
+
return 1;
|
|
92
|
+
if ((max - min) % step !== 0) {
|
|
93
|
+
warn(`Step ${step} is not a divisor of range ${max - min}`);
|
|
94
|
+
}
|
|
95
|
+
return step;
|
|
96
|
+
};
|
|
97
|
+
const validStep = validateStep(step, min, max);
|
|
98
|
+
// 将值约束在 min-max 范围内,并按步长对齐
|
|
99
|
+
const constrainValue = (val, minVal = min, maxVal = max, stepVal = validStep) => {
|
|
100
|
+
const constrained = Math.max(minVal, Math.min(maxVal, val));
|
|
101
|
+
const steps = Math.round((constrained - minVal) / stepVal);
|
|
102
|
+
return minVal + steps * stepVal;
|
|
103
|
+
};
|
|
104
|
+
// 计算滑块位置
|
|
105
|
+
const getThumbPosition = (val, trackW = trackWidth, minVal = min, maxVal = max) => {
|
|
106
|
+
if (trackW === 0)
|
|
107
|
+
return 0;
|
|
108
|
+
const percentage = (val - minVal) / (maxVal - minVal);
|
|
109
|
+
const position = percentage * trackW;
|
|
110
|
+
return position;
|
|
111
|
+
};
|
|
112
|
+
// 手势处理
|
|
113
|
+
const panGesture = useMemo(() => {
|
|
114
|
+
const getThumbPositionWorklet = (val, trackW, minVal, maxVal) => {
|
|
115
|
+
'worklet';
|
|
116
|
+
if (trackW === 0)
|
|
117
|
+
return 0;
|
|
118
|
+
const percentage = (val - minVal) / (maxVal - minVal);
|
|
119
|
+
return percentage * trackW;
|
|
120
|
+
};
|
|
121
|
+
const constrainValueWorklet = (val, minVal, maxVal, stepVal) => {
|
|
122
|
+
'worklet';
|
|
123
|
+
const constrained = Math.max(minVal, Math.min(maxVal, val));
|
|
124
|
+
const steps = Math.round((constrained - minVal) / stepVal);
|
|
125
|
+
return minVal + steps * stepVal;
|
|
126
|
+
};
|
|
127
|
+
return Gesture.Pan()
|
|
128
|
+
.enabled(!disabled) // 通过手势启用状态控制是否可拖拽
|
|
129
|
+
.onBegin(() => {
|
|
130
|
+
'worklet';
|
|
131
|
+
if (trackWidth === 0)
|
|
132
|
+
return;
|
|
133
|
+
isDragging.value = true;
|
|
134
|
+
// 记录拖拽开始时的位置 - 使用当前的动画位置
|
|
135
|
+
startDragPosition.value = thumbPosition.value;
|
|
136
|
+
// 根据当前位置反推值
|
|
137
|
+
const percentage = thumbPosition.value / trackWidth;
|
|
138
|
+
const currentVal = min + percentage * (max - min);
|
|
139
|
+
startDragValue.value = constrainValueWorklet(currentVal, min, max, validStep);
|
|
140
|
+
})
|
|
141
|
+
.onUpdate((event) => {
|
|
142
|
+
'worklet';
|
|
143
|
+
if (trackWidth === 0)
|
|
144
|
+
return;
|
|
145
|
+
// 基于拖拽开始位置计算新位置
|
|
146
|
+
const newX = startDragPosition.value + event.translationX;
|
|
147
|
+
const clampedX = Math.max(0, Math.min(trackWidth, newX));
|
|
148
|
+
// 计算新值
|
|
149
|
+
const percentage = clampedX / trackWidth;
|
|
150
|
+
const rawValue = min + percentage * (max - min);
|
|
151
|
+
const newValue = constrainValueWorklet(rawValue, min, max, validStep);
|
|
152
|
+
// 更新滑块位置 - 使用约束后的值对应的位置
|
|
153
|
+
const constrainedPosition = getThumbPositionWorklet(newValue, trackWidth, min, max);
|
|
154
|
+
thumbPosition.value = constrainedPosition;
|
|
155
|
+
// 只触发 changing 事件,不更新 currentValue(避免干扰拖拽)
|
|
156
|
+
runOnJS(runOnJSCallback)('triggerChangingEvent', newValue);
|
|
157
|
+
})
|
|
158
|
+
.onEnd((event) => {
|
|
159
|
+
'worklet';
|
|
160
|
+
isDragging.value = false;
|
|
161
|
+
// 基于拖拽开始位置计算最终位置
|
|
162
|
+
const newX = startDragPosition.value + event.translationX;
|
|
163
|
+
const clampedX = Math.max(0, Math.min(trackWidth, newX));
|
|
164
|
+
const percentage = clampedX / trackWidth;
|
|
165
|
+
const rawValue = min + percentage * (max - min);
|
|
166
|
+
const finalValue = constrainValueWorklet(rawValue, min, max, validStep);
|
|
167
|
+
// 确保滑块位置与最终值匹配
|
|
168
|
+
const finalPosition = getThumbPositionWorklet(finalValue, trackWidth, min, max);
|
|
169
|
+
thumbPosition.value = finalPosition;
|
|
170
|
+
// 更新 currentValue 并触发 change 事件
|
|
171
|
+
runOnJS(runOnJSCallback)('triggerChangeEvent', finalValue);
|
|
172
|
+
});
|
|
173
|
+
}, [disabled, trackWidth, min, max, validStep, runOnJSCallback]);
|
|
174
|
+
// 当 value 属性变化时更新位置
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const newValue = constrainValue(defaultValue);
|
|
177
|
+
setCurrentValue(newValue);
|
|
178
|
+
// 同时更新动画位置
|
|
179
|
+
thumbPosition.value = getThumbPosition(newValue);
|
|
180
|
+
}, [defaultValue, min, max, validStep]);
|
|
181
|
+
// 当 trackWidth 变化时更新滑块位置
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
// 只在非拖拽状态下更新位置
|
|
184
|
+
if (!isDragging.value) {
|
|
185
|
+
thumbPosition.value = getThumbPosition(currentValue);
|
|
186
|
+
}
|
|
187
|
+
}, [trackWidth, currentValue]);
|
|
188
|
+
// 动画样式
|
|
189
|
+
const animatedThumbStyle = useAnimatedStyle(() => {
|
|
190
|
+
const blockSizeNum = Math.max(12, Math.min(28, blockSize));
|
|
191
|
+
const trackHeight = 4;
|
|
192
|
+
return {
|
|
193
|
+
position: 'absolute',
|
|
194
|
+
top: -((blockSizeNum - trackHeight) / 2),
|
|
195
|
+
left: Math.max(0, Math.min(trackWidth - blockSizeNum, thumbPosition.value - (blockSizeNum / 2))),
|
|
196
|
+
width: blockSizeNum,
|
|
197
|
+
height: blockSizeNum,
|
|
198
|
+
justifyContent: 'center',
|
|
199
|
+
alignItems: 'center'
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
// 轨道布局回调
|
|
203
|
+
const onTrackLayout = (event) => {
|
|
204
|
+
const { width } = event.nativeEvent.layout;
|
|
205
|
+
setTrackWidth(width);
|
|
206
|
+
};
|
|
207
|
+
// 表单相关处理
|
|
208
|
+
const resetValue = () => {
|
|
209
|
+
const currentProps = propsRef.current;
|
|
210
|
+
const currentValue = currentProps.value !== undefined ? currentProps.value : currentProps.min || 0;
|
|
211
|
+
const parsedValue = typeof currentValue === 'string' ? parseFloat(currentValue) : currentValue;
|
|
212
|
+
const currentMin = typeof currentProps.min === 'string' ? parseFloat(currentProps.min) : (currentProps.min || 0);
|
|
213
|
+
const currentMax = typeof currentProps.max === 'string' ? parseFloat(currentProps.max) : (currentProps.max || 100);
|
|
214
|
+
const currentStep = typeof currentProps.step === 'string' ? parseFloat(currentProps.step) : (currentProps.step || 1);
|
|
215
|
+
const resetVal = parsedValue !== undefined ? parsedValue : currentMin;
|
|
216
|
+
const validatedStep = validateStep(currentStep, currentMin, currentMax);
|
|
217
|
+
const constrainedVal = constrainValue(resetVal, currentMin, currentMax, validatedStep);
|
|
218
|
+
setCurrentValue(constrainedVal);
|
|
219
|
+
thumbPosition.value = getThumbPosition(constrainedVal, trackWidth, currentMin, currentMax);
|
|
220
|
+
};
|
|
221
|
+
const getValue = () => {
|
|
222
|
+
return currentValue;
|
|
223
|
+
};
|
|
224
|
+
if (formValuesMap) {
|
|
225
|
+
if (!name) {
|
|
226
|
+
warn('If a form component is used, the name attribute is required.');
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
formValuesMap.set(name, { getValue, resetValue });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
return () => {
|
|
234
|
+
if (formValuesMap && name) {
|
|
235
|
+
formValuesMap.delete(name);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}, []);
|
|
239
|
+
// 样式定义
|
|
240
|
+
const blockSizeNum = Math.max(12, Math.min(28, blockSize));
|
|
241
|
+
const trackHeight = 4;
|
|
242
|
+
const containerStyle = extendObject({}, {
|
|
243
|
+
flexDirection: 'row',
|
|
244
|
+
alignItems: 'center',
|
|
245
|
+
minHeight: Math.max(blockSizeNum + 8, 40),
|
|
246
|
+
paddingHorizontal: 14 // 固定内边距,不受 block-size 影响
|
|
247
|
+
}, normalStyle, layoutStyle);
|
|
248
|
+
const trackStyle = {
|
|
249
|
+
flex: 1,
|
|
250
|
+
height: trackHeight,
|
|
251
|
+
backgroundColor,
|
|
252
|
+
borderRadius: trackHeight / 2,
|
|
253
|
+
position: 'relative'
|
|
254
|
+
};
|
|
255
|
+
// 动画进度条样式
|
|
256
|
+
const animatedProgressStyle = useAnimatedStyle(() => {
|
|
257
|
+
return {
|
|
258
|
+
height: '100%',
|
|
259
|
+
backgroundColor: activeColor,
|
|
260
|
+
borderRadius: trackHeight / 2,
|
|
261
|
+
width: Math.max(0, thumbPosition.value)
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
const thumbStyle = {
|
|
265
|
+
width: blockSizeNum,
|
|
266
|
+
height: blockSizeNum,
|
|
267
|
+
backgroundColor: blockColor,
|
|
268
|
+
borderRadius: blockSizeNum / 2,
|
|
269
|
+
shadowColor: '#000',
|
|
270
|
+
shadowOffset: { width: 0, height: 2 },
|
|
271
|
+
shadowOpacity: 0.2,
|
|
272
|
+
shadowRadius: 4,
|
|
273
|
+
elevation: 4
|
|
274
|
+
};
|
|
275
|
+
const innerProps = useInnerProps(extendObject({}, props, layoutProps, {
|
|
276
|
+
ref: nodeRef
|
|
277
|
+
}), [
|
|
278
|
+
'min',
|
|
279
|
+
'max',
|
|
280
|
+
'step',
|
|
281
|
+
'disabled',
|
|
282
|
+
'value',
|
|
283
|
+
'color',
|
|
284
|
+
'selected-color',
|
|
285
|
+
'activeColor',
|
|
286
|
+
'backgroundColor',
|
|
287
|
+
'block-size',
|
|
288
|
+
'block-color',
|
|
289
|
+
'bindchange',
|
|
290
|
+
'catchchange',
|
|
291
|
+
'bindchanging',
|
|
292
|
+
'catchchanging'
|
|
293
|
+
], { layoutRef });
|
|
294
|
+
const sliderContent = createElement(View, extendObject({}, innerProps, { style: containerStyle }),
|
|
295
|
+
// 轨道容器
|
|
296
|
+
createElement(View, {
|
|
297
|
+
style: trackStyle,
|
|
298
|
+
onLayout: onTrackLayout,
|
|
299
|
+
ref: trackRef
|
|
300
|
+
},
|
|
301
|
+
// 进度条 - 使用动画样式
|
|
302
|
+
createElement(Animated.View, {
|
|
303
|
+
style: animatedProgressStyle
|
|
304
|
+
}),
|
|
305
|
+
// 滑块容器
|
|
306
|
+
createElement(GestureDetector, {
|
|
307
|
+
gesture: panGesture
|
|
308
|
+
}, createElement(Animated.View, {
|
|
309
|
+
style: [animatedThumbStyle]
|
|
310
|
+
},
|
|
311
|
+
// 滑块
|
|
312
|
+
createElement(View, {
|
|
313
|
+
style: thumbStyle
|
|
314
|
+
})))));
|
|
315
|
+
if (hasPositionFixed) {
|
|
316
|
+
return createElement(Portal, null, sliderContent);
|
|
317
|
+
}
|
|
318
|
+
return sliderContent;
|
|
319
|
+
});
|
|
320
|
+
Slider.displayName = 'MpxSlider';
|
|
321
|
+
export default Slider;
|