@mpxjs/webpack-plugin 2.10.15-4 → 2.10.15

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.
Files changed (27) hide show
  1. package/LICENSE +433 -0
  2. package/lib/index.js +2 -5
  3. package/lib/platform/template/wx/component-config/progress.js +12 -0
  4. package/lib/platform/template/wx/component-config/unsupported.js +1 -1
  5. package/lib/platform/template/wx/index.js +3 -1
  6. package/lib/runtime/components/react/dist/getInnerListeners.js +35 -21
  7. package/lib/runtime/components/react/dist/mpx-movable-view.jsx +102 -34
  8. package/lib/runtime/components/react/dist/mpx-portal/portal-manager.jsx +3 -5
  9. package/lib/runtime/components/react/dist/mpx-progress.jsx +159 -0
  10. package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +51 -9
  11. package/lib/runtime/components/react/dist/mpx-swiper.jsx +9 -16
  12. package/lib/runtime/components/react/dist/mpx-web-view.jsx +20 -1
  13. package/lib/runtime/components/react/getInnerListeners.ts +41 -22
  14. package/lib/runtime/components/react/mpx-movable-view.tsx +156 -48
  15. package/lib/runtime/components/react/mpx-portal/portal-manager.tsx +4 -8
  16. package/lib/runtime/components/react/mpx-progress.tsx +257 -0
  17. package/lib/runtime/components/react/mpx-scroll-view.tsx +54 -9
  18. package/lib/runtime/components/react/mpx-swiper.tsx +9 -16
  19. package/lib/runtime/components/react/mpx-web-view.tsx +22 -1
  20. package/lib/runtime/components/react/types/getInnerListeners.d.ts +7 -2
  21. package/lib/runtime/components/web/mpx-movable-area.vue +43 -19
  22. package/lib/runtime/components/web/mpx-movable-view.vue +93 -3
  23. package/lib/runtime/components/web/mpx-swiper.vue +1 -2
  24. package/lib/runtime/components/web/mpx-web-view.vue +3 -3
  25. package/lib/template-compiler/compiler.js +61 -31
  26. package/lib/wxss/utils.js +1 -1
  27. package/package.json +3 -3
@@ -10,11 +10,13 @@ import {
10
10
  RemoveProps,
11
11
  InnerRef,
12
12
  LayoutRef,
13
- ExtendedNativeTouchEvent
13
+ ExtendedNativeTouchEvent,
14
+ GlobalEventState
14
15
  } from './types/getInnerListeners'
15
16
 
16
- const globalEventState = {
17
- needPress: true
17
+ const globalEventState: GlobalEventState = {
18
+ needPress: true,
19
+ identifier: null
18
20
  }
19
21
 
20
22
  const getTouchEvent = (
@@ -137,39 +139,54 @@ function checkIsNeedPress (e: ExtendedNativeTouchEvent, type: 'bubble' | 'captur
137
139
  }
138
140
  }
139
141
 
142
+ function shouldHandleTapEvent (e: ExtendedNativeTouchEvent, eventConfig: EventConfig) {
143
+ const { identifier } = e.nativeEvent.changedTouches[0]
144
+ return eventConfig.tap && globalEventState.identifier === identifier
145
+ }
146
+
140
147
  function handleTouchstart (e: ExtendedNativeTouchEvent, type: EventType, eventConfig: EventConfig) {
141
- // 阻止事件被释放放回对象池,导致对象复用 _stoppedEventTypes 状态被保留
142
148
  e.persist()
143
149
  const { innerRef } = eventConfig
144
- globalEventState.needPress = true
145
- innerRef.current.mpxPressInfo.detail = {
146
- x: e.nativeEvent.changedTouches[0].pageX,
147
- y: e.nativeEvent.changedTouches[0].pageY
150
+ const touch = e.nativeEvent.changedTouches[0]
151
+ const { identifier } = touch
152
+
153
+ const isSingle = e.nativeEvent.touches.length <= 1
154
+
155
+ if (isSingle) {
156
+ // 仅在 touchstart 记录第一个单指触摸点
157
+ globalEventState.identifier = identifier
158
+ globalEventState.needPress = true
159
+ innerRef.current.mpxPressInfo.detail = {
160
+ x: touch.pageX,
161
+ y: touch.pageY
162
+ }
148
163
  }
149
164
 
150
165
  handleEmitEvent('touchstart', e, type, eventConfig)
151
166
 
152
167
  if (eventConfig.longpress) {
153
- if (e._stoppedEventTypes?.has('longpress')) {
154
- return
155
- }
156
- if (eventConfig.longpress.hasCatch) {
157
- e._stoppedEventTypes = e._stoppedEventTypes || new Set()
158
- e._stoppedEventTypes.add('longpress')
168
+ // 只有单指触摸时才启动长按定时器
169
+ if (isSingle) {
170
+ if (e._stoppedEventTypes?.has('longpress')) {
171
+ return
172
+ }
173
+ if (eventConfig.longpress.hasCatch) {
174
+ e._stoppedEventTypes = e._stoppedEventTypes || new Set()
175
+ e._stoppedEventTypes.add('longpress')
176
+ }
177
+ innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type] as unknown as number)
178
+ innerRef.current.startTimer[type] = setTimeout(() => {
179
+ globalEventState.needPress = false
180
+ handleEmitEvent('longpress', e, type, eventConfig)
181
+ }, 350)
159
182
  }
160
- innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type] as unknown as number)
161
- innerRef.current.startTimer[type] = setTimeout(() => {
162
- // 只要触发过longpress, 全局就不再触发tap
163
- globalEventState.needPress = false
164
- handleEmitEvent('longpress', e, type, eventConfig)
165
- }, 350)
166
183
  }
167
184
  }
168
185
 
169
186
  function handleTouchmove (e: ExtendedNativeTouchEvent, type: EventType, eventConfig: EventConfig) {
170
187
  const { innerRef } = eventConfig
171
188
  handleEmitEvent('touchmove', e, type, eventConfig)
172
- if (eventConfig.tap) {
189
+ if (shouldHandleTapEvent(e, eventConfig)) {
173
190
  checkIsNeedPress(e, type, innerRef)
174
191
  }
175
192
  }
@@ -178,7 +195,9 @@ function handleTouchend (e: ExtendedNativeTouchEvent, type: EventType, eventConf
178
195
  const { innerRef, disableTap } = eventConfig
179
196
  handleEmitEvent('touchend', e, type, eventConfig)
180
197
  innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type] as unknown as number)
181
- if (eventConfig.tap) {
198
+
199
+ // 只有单指触摸结束时才触发 tap
200
+ if (shouldHandleTapEvent(e, eventConfig)) {
182
201
  checkIsNeedPress(e, type, innerRef)
183
202
  if (!globalEventState.needPress || (type === 'bubble' && disableTap) || e._stoppedEventTypes?.has('tap')) {
184
203
  return
@@ -4,8 +4,8 @@
4
4
  * ✔ out-of-bounds
5
5
  * ✔ x
6
6
  * ✔ y
7
- * damping
8
- * friction
7
+ * damping
8
+ * friction
9
9
  * ✔ disabled
10
10
  * ✘ scale
11
11
  * ✘ scale-min
@@ -27,13 +27,125 @@ import { GestureDetector, Gesture, GestureTouchEvent, GestureStateChangeEvent, P
27
27
  import Animated, {
28
28
  useSharedValue,
29
29
  useAnimatedStyle,
30
- withDecay,
31
30
  runOnJS,
32
31
  runOnUI,
33
- withSpring
32
+ withTiming,
33
+ Easing
34
34
  } from 'react-native-reanimated'
35
35
  import { collectDataset, noop } from '@mpxjs/utils'
36
36
 
37
+ // 超出边界处理函数,参考微信小程序的超出边界衰减效果
38
+ const applyBoundaryDecline = (
39
+ newValue: number,
40
+ range: [min: number, max: number]
41
+ ): number => {
42
+ 'worklet'
43
+
44
+ const decline = (distance: number): number => {
45
+ 'worklet'
46
+ return Math.sqrt(Math.abs(distance))
47
+ }
48
+
49
+ if (newValue < range[0]) {
50
+ const overDistance = range[0] - newValue
51
+ return range[0] - decline(overDistance)
52
+ } else if (newValue > range[1]) {
53
+ const overDistance = newValue - range[1]
54
+ return range[1] + decline(overDistance)
55
+ }
56
+ return newValue
57
+ }
58
+
59
+ // 参考微信小程序的弹簧阻尼系统实现
60
+ const withWechatSpring = (
61
+ toValue: number,
62
+ dampingParam = 20,
63
+ callback?: () => void
64
+ ) => {
65
+ 'worklet'
66
+
67
+ // 弹簧参数计算
68
+ const m = 1 // 质量
69
+ const k = 9 * Math.pow(dampingParam, 2) / 40 // 弹簧系数
70
+ const c = dampingParam // 阻尼系数
71
+
72
+ // 判别式:r = c² - 4mk
73
+ const discriminant = c * c - 4 * m * k
74
+
75
+ // 计算动画持续时间和缓动函数
76
+ let duration: number
77
+ let easingFunction: any
78
+
79
+ if (Math.abs(discriminant) < 0.01) {
80
+ // 临界阻尼 (discriminant ≈ 0)
81
+ // 使用cubic-out模拟临界阻尼的平滑过渡
82
+ duration = Math.max(350, Math.min(800, 2000 / dampingParam))
83
+ easingFunction = Easing.out(Easing.cubic)
84
+ } else if (discriminant > 0) {
85
+ // 过阻尼 (discriminant > 0)
86
+ // 使用指数缓动模拟过阻尼的缓慢收敛
87
+ duration = Math.max(450, Math.min(1000, 2500 / dampingParam))
88
+ easingFunction = Easing.out(Easing.exp)
89
+ } else {
90
+ // 欠阻尼 (discriminant < 0) - 会产生振荡
91
+ // 计算振荡频率和衰减率
92
+ const dampingRatio = c / (2 * Math.sqrt(m * k)) // 阻尼比
93
+
94
+ // 根据阻尼比调整动画参数
95
+ if (dampingRatio < 0.7) {
96
+ // 明显振荡
97
+ duration = Math.max(600, Math.min(1200, 3000 / dampingParam))
98
+ // 创建带振荡的贝塞尔曲线
99
+ easingFunction = Easing.bezier(0.175, 0.885, 0.32, 1.275)
100
+ } else {
101
+ // 轻微振荡
102
+ duration = Math.max(400, Math.min(800, 2000 / dampingParam))
103
+ easingFunction = Easing.bezier(0.25, 0.46, 0.45, 0.94)
104
+ }
105
+ }
106
+
107
+ return withTiming(toValue, {
108
+ duration,
109
+ easing: easingFunction
110
+ }, callback)
111
+ }
112
+
113
+ // 参考微信小程序friction的惯性动画
114
+ const withWechatDecay = (
115
+ velocity: number,
116
+ currentPosition: number,
117
+ clampRange: [min: number, max: number],
118
+ frictionValue = 2,
119
+ callback?: () => void
120
+ ) => {
121
+ 'worklet'
122
+
123
+ // 微信小程序friction算法: delta = -1.5 * v² / a, 其中 a = -f * v / |v|
124
+ // 如果friction小于等于0,设置为默认值2
125
+ const validFriction = frictionValue <= 0 ? 2 : frictionValue
126
+ const f = 1000 * validFriction
127
+ const acceleration = velocity !== 0 ? -f * velocity / Math.abs(velocity) : 0
128
+ const delta = acceleration !== 0 ? (-1.5 * velocity * velocity) / acceleration : 0
129
+
130
+ let finalPosition = currentPosition + delta
131
+
132
+ // 边界限制
133
+ if (finalPosition < clampRange[0]) {
134
+ finalPosition = clampRange[0]
135
+ } else if (finalPosition > clampRange[1]) {
136
+ finalPosition = clampRange[1]
137
+ }
138
+
139
+ // 计算动画时长
140
+ const distance = Math.abs(finalPosition - currentPosition)
141
+ const duration = Math.min(1500, Math.max(200, distance * 8))
142
+
143
+ return withTiming(finalPosition, {
144
+ duration,
145
+ easing: Easing.out(Easing.cubic)
146
+ }, callback)
147
+ }
148
+
37
149
  interface MovableViewProps {
38
150
  children: ReactNode
39
151
  style?: Record<string, any>
@@ -42,6 +154,8 @@ interface MovableViewProps {
42
154
  y?: number
43
155
  disabled?: boolean
44
156
  animation?: boolean
157
+ damping?: number
158
+ friction?: number
45
159
  id?: string
46
160
  changeThrottleTime?:number
47
161
  bindchange?: (event: unknown) => void
@@ -95,6 +209,8 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
95
209
  inertia = false,
96
210
  disabled = false,
97
211
  animation = true,
212
+ damping = 20,
213
+ friction = 2,
98
214
  'out-of-bounds': outOfBounds = false,
99
215
  'enable-var': enableVar,
100
216
  'external-var-context': externalVarContext,
@@ -206,18 +322,12 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
206
322
  const { x: newX, y: newY } = checkBoundaryPosition({ positionX: Number(x), positionY: Number(y) })
207
323
  if (direction === 'horizontal' || direction === 'all') {
208
324
  offsetX.value = animation
209
- ? withSpring(newX, {
210
- duration: 1500,
211
- dampingRatio: 0.8
212
- })
325
+ ? withWechatSpring(newX, damping)
213
326
  : newX
214
327
  }
215
328
  if (direction === 'vertical' || direction === 'all') {
216
329
  offsetY.value = animation
217
- ? withSpring(newY, {
218
- duration: 1500,
219
- dampingRatio: 0.8
220
- })
330
+ ? withWechatSpring(newY, damping)
221
331
  : newY
222
332
  }
223
333
  if (bindchange) {
@@ -404,7 +514,7 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
404
514
  })
405
515
  const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef)
406
516
 
407
- // 节流版本的 change 事件触发
517
+ // 节流版本的change事件触发
408
518
  const handleTriggerChangeThrottled = useCallback(({ x, y, type }: { x: number; y: number; type?: string }) => {
409
519
  'worklet'
410
520
  const now = Date.now()
@@ -468,7 +578,7 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
468
578
  const { x } = checkBoundaryPosition({ positionX: newX, positionY: offsetY.value })
469
579
  offsetX.value = x
470
580
  } else {
471
- offsetX.value = newX
581
+ offsetX.value = applyBoundaryDecline(newX, draggableXRange.value)
472
582
  }
473
583
  }
474
584
  if (direction === 'vertical' || direction === 'all') {
@@ -477,7 +587,7 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
477
587
  const { y } = checkBoundaryPosition({ positionX: offsetX.value, positionY: newY })
478
588
  offsetY.value = y
479
589
  } else {
480
- offsetY.value = newY
590
+ offsetY.value = applyBoundaryDecline(newY, draggableYRange.value)
481
591
  }
482
592
  }
483
593
  if (bindchange) {
@@ -506,18 +616,12 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
506
616
  if (x !== offsetX.value || y !== offsetY.value) {
507
617
  if (x !== offsetX.value) {
508
618
  offsetX.value = animation
509
- ? withSpring(x, {
510
- duration: 1500,
511
- dampingRatio: 0.8
512
- })
619
+ ? withWechatSpring(x, damping)
513
620
  : x
514
621
  }
515
622
  if (y !== offsetY.value) {
516
623
  offsetY.value = animation
517
- ? withSpring(y, {
518
- duration: 1500,
519
- dampingRatio: 0.8
520
- })
624
+ ? withWechatSpring(y, damping)
521
625
  : y
522
626
  }
523
627
  if (bindchange) {
@@ -528,38 +632,42 @@ const _MovableView = forwardRef<HandlerRef<View, MovableViewProps>, MovableViewP
528
632
  }
529
633
  }
530
634
  } else if (inertia) {
531
- // 惯性处理
635
+ // 惯性处理 - 使用微信小程序friction算法
532
636
  if (direction === 'horizontal' || direction === 'all') {
533
637
  xInertialMotion.value = true
534
- offsetX.value = withDecay({
535
- velocity: e.velocityX / 10,
536
- rubberBandEffect: outOfBounds,
537
- clamp: draggableXRange.value
538
- }, () => {
539
- xInertialMotion.value = false
540
- if (bindchange) {
541
- runOnJS(runOnJSCallback)('handleTriggerChange', {
542
- x: offsetX.value,
543
- y: offsetY.value
544
- })
638
+ offsetX.value = withWechatDecay(
639
+ e.velocityX / 10,
640
+ offsetX.value,
641
+ draggableXRange.value,
642
+ friction,
643
+ () => {
644
+ xInertialMotion.value = false
645
+ if (bindchange) {
646
+ runOnJS(runOnJSCallback)('handleTriggerChange', {
647
+ x: offsetX.value,
648
+ y: offsetY.value
649
+ })
650
+ }
545
651
  }
546
- })
652
+ )
547
653
  }
548
654
  if (direction === 'vertical' || direction === 'all') {
549
655
  yInertialMotion.value = true
550
- offsetY.value = withDecay({
551
- velocity: e.velocityY / 10,
552
- rubberBandEffect: outOfBounds,
553
- clamp: draggableYRange.value
554
- }, () => {
555
- yInertialMotion.value = false
556
- if (bindchange) {
557
- runOnJS(runOnJSCallback)('handleTriggerChange', {
558
- x: offsetX.value,
559
- y: offsetY.value
560
- })
656
+ offsetY.value = withWechatDecay(
657
+ e.velocityY / 10,
658
+ offsetY.value,
659
+ draggableYRange.value,
660
+ friction,
661
+ () => {
662
+ yInertialMotion.value = false
663
+ if (bindchange) {
664
+ runOnJS(runOnJSCallback)('handleTriggerChange', {
665
+ x: offsetX.value,
666
+ y: offsetY.value
667
+ })
668
+ }
561
669
  }
562
- })
670
+ )
563
671
  }
564
672
  }
565
673
  })
@@ -1,5 +1,4 @@
1
- import { useState, useCallback, forwardRef, ForwardedRef, useImperativeHandle, ReactNode, ReactElement } from 'react'
2
- import { View, StyleSheet } from 'react-native'
1
+ import { useState, useCallback, forwardRef, ForwardedRef, useImperativeHandle, ReactNode, ReactElement, Fragment } from 'react'
3
2
 
4
3
  export type State = {
5
4
  portals: Array<{
@@ -48,13 +47,10 @@ const _PortalManager = forwardRef((props: PortalManagerProps, ref:ForwardedRef<u
48
47
 
49
48
  return (
50
49
  <>
51
- {state.portals.map(({ key, children }, i) => (
52
- <View
53
- key={key}
54
- collapsable={false} // Need collapsable=false here to clip the elevations
55
- style={[StyleSheet.absoluteFill, { zIndex: 1000 + i, pointerEvents: 'box-none' }]}>
50
+ {state.portals.map(({ key, children }) => (
51
+ <Fragment key={key}>
56
52
  {children}
57
- </View>
53
+ </Fragment>
58
54
  ))}
59
55
  </>
60
56
  )
@@ -0,0 +1,257 @@
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 {
16
+ JSX,
17
+ useRef,
18
+ forwardRef,
19
+ useEffect,
20
+ useState,
21
+ createElement,
22
+ ForwardedRef
23
+ } from 'react'
24
+ import {
25
+ View,
26
+ ViewStyle
27
+ } from 'react-native'
28
+ import Animated, {
29
+ useSharedValue,
30
+ useAnimatedStyle,
31
+ withTiming,
32
+ Easing,
33
+ runOnJS
34
+ } from 'react-native-reanimated'
35
+
36
+ import useInnerProps from './getInnerListeners'
37
+ import useNodesRef, { HandlerRef } from './useNodesRef'
38
+ import { useLayout, useTransformStyle, extendObject } from './utils'
39
+ import Portal from './mpx-portal'
40
+
41
+ export interface ProgressProps {
42
+ percent?: number
43
+ 'stroke-width'?: number | string
44
+ color?: string
45
+ activeColor?: string
46
+ backgroundColor?: string
47
+ active?: boolean
48
+ 'active-mode'?: 'backwards' | 'forwards'
49
+ duration?: number
50
+ bindactiveend?: (event: any) => void
51
+ style?: ViewStyle & Record<string, any>
52
+ 'enable-offset'?: boolean
53
+ 'enable-var'?: boolean
54
+ 'external-var-context'?: Record<string, any>
55
+ 'parent-font-size'?: number
56
+ 'parent-width'?: number
57
+ 'parent-height'?: number
58
+ }
59
+
60
+ const Progress = forwardRef<
61
+ HandlerRef<View, ProgressProps>,
62
+ ProgressProps
63
+ >((props: ProgressProps, ref: ForwardedRef<HandlerRef<View, ProgressProps>>): JSX.Element => {
64
+ const {
65
+ percent = 0,
66
+ 'stroke-width': strokeWidth = 6,
67
+ color,
68
+ activeColor = color || '#09BB07',
69
+ backgroundColor = '#EBEBEB',
70
+ active = false,
71
+ 'active-mode': activeMode = 'backwards',
72
+ duration = 30,
73
+ bindactiveend,
74
+ style = {},
75
+ 'enable-var': enableVar,
76
+ 'external-var-context': externalVarContext,
77
+ 'parent-font-size': parentFontSize,
78
+ 'parent-width': parentWidth,
79
+ 'parent-height': parentHeight
80
+ } = props
81
+
82
+ const nodeRef = useRef(null)
83
+ const propsRef = useRef({})
84
+ propsRef.current = props
85
+
86
+ // 进度值状态
87
+ const [lastPercent, setLastPercent] = useState(0)
88
+ const progressWidth = useSharedValue(0)
89
+
90
+ const {
91
+ normalStyle,
92
+ hasSelfPercent,
93
+ setWidth,
94
+ setHeight,
95
+ hasPositionFixed
96
+ } = useTransformStyle(style, {
97
+ enableVar,
98
+ externalVarContext,
99
+ parentFontSize,
100
+ parentWidth,
101
+ parentHeight
102
+ })
103
+
104
+ const { layoutRef, layoutStyle, layoutProps } = useLayout({
105
+ props,
106
+ hasSelfPercent,
107
+ setWidth,
108
+ setHeight,
109
+ nodeRef
110
+ })
111
+
112
+ useNodesRef(props, ref, nodeRef, {
113
+ style: normalStyle
114
+ })
115
+
116
+ // 进度条动画函数
117
+ const startProgressAnimation = (targetPercent: number, startPercent: number, animationDuration: number, onFinished?: () => void) => {
118
+ // 根据 active-mode 设置起始位置
119
+ progressWidth.value = startPercent
120
+ progressWidth.value = withTiming(
121
+ targetPercent,
122
+ {
123
+ duration: animationDuration,
124
+ easing: Easing.linear
125
+ },
126
+ (finished) => {
127
+ if (finished && onFinished) {
128
+ // 在动画回调中,需要使用runOnJS回到主线程
129
+ runOnJS(onFinished)()
130
+ }
131
+ }
132
+ )
133
+ }
134
+
135
+ // 创建在主线程执行的事件回调函数
136
+ const triggerActiveEnd = (percent: number) => {
137
+ if (bindactiveend) {
138
+ bindactiveend({
139
+ type: 'activeend',
140
+ detail: {
141
+ percent: percent
142
+ }
143
+ })
144
+ }
145
+ }
146
+
147
+ // 进度变化时的动画效果
148
+ useEffect(() => {
149
+ const targetPercent = Math.max(0, Math.min(100, percent))
150
+ if (active) {
151
+ // 根据 active-mode 确定起始位置
152
+ let startPercent
153
+ if (activeMode === 'backwards') {
154
+ startPercent = 0
155
+ } else {
156
+ // forwards 模式:使用上次记录的百分比作为起始位置
157
+ startPercent = lastPercent
158
+ }
159
+
160
+ // 计算动画持续时间
161
+ const percentDiff = Math.abs(targetPercent - startPercent)
162
+ const animationDuration = percentDiff * duration
163
+
164
+ // 创建动画完成回调
165
+ const onAnimationFinished = () => {
166
+ triggerActiveEnd(targetPercent)
167
+ }
168
+
169
+ // 执行动画
170
+ startProgressAnimation(targetPercent, startPercent, animationDuration, onAnimationFinished)
171
+ } else {
172
+ progressWidth.value = targetPercent
173
+ }
174
+
175
+ setLastPercent(targetPercent)
176
+ }, [percent, active, activeMode, duration, bindactiveend])
177
+
178
+ // 初始化时设置进度值
179
+ useEffect(() => {
180
+ if (!active) {
181
+ progressWidth.value = Math.max(0, Math.min(100, percent))
182
+ }
183
+ }, [])
184
+
185
+ // 进度条动画样式
186
+ const animatedProgressStyle = useAnimatedStyle(() => {
187
+ return {
188
+ width: `${progressWidth.value}%`
189
+ }
190
+ })
191
+
192
+ // 确保数值类型正确
193
+ const strokeWidthNum = typeof strokeWidth === 'number' ? strokeWidth : parseInt(strokeWidth as string, 10) || 6
194
+
195
+ // 容器样式
196
+ const containerStyle: ViewStyle = extendObject({} as ViewStyle, {
197
+ flexDirection: 'row' as const,
198
+ alignItems: 'center' as const,
199
+ width: '100%',
200
+ minHeight: Math.max(strokeWidthNum, 20)
201
+ }, normalStyle, layoutStyle)
202
+
203
+ // 进度条背景样式
204
+ const progressBgStyle: ViewStyle = {
205
+ width: '100%',
206
+ height: strokeWidthNum,
207
+ backgroundColor,
208
+ overflow: 'hidden'
209
+ }
210
+
211
+ // 进度条填充样式
212
+ const progressFillStyle: ViewStyle = {
213
+ height: '100%',
214
+ backgroundColor: activeColor
215
+ }
216
+
217
+ const innerProps = useInnerProps(
218
+ extendObject({}, props, layoutProps, {
219
+ ref: nodeRef
220
+ }),
221
+ [
222
+ 'percent',
223
+ 'stroke-width',
224
+ 'color',
225
+ 'activeColor',
226
+ 'backgroundColor',
227
+ 'active',
228
+ 'active-mode',
229
+ 'duration',
230
+ 'bindactiveend'
231
+ ],
232
+ { layoutRef }
233
+ )
234
+
235
+ const progressComponent = createElement(
236
+ View,
237
+ extendObject({}, innerProps, { style: containerStyle }),
238
+ // 进度条背景
239
+ createElement(
240
+ View,
241
+ { style: progressBgStyle },
242
+ // 进度条填充
243
+ createElement(Animated.View, {
244
+ style: [progressFillStyle, animatedProgressStyle]
245
+ })
246
+ )
247
+ )
248
+
249
+ if (hasPositionFixed) {
250
+ return createElement(Portal, null, progressComponent)
251
+ }
252
+
253
+ return progressComponent
254
+ })
255
+
256
+ Progress.displayName = 'MpxProgress'
257
+ export default Progress