@mpxjs/webpack-plugin 2.10.17-beta.7 → 2.10.17-beta.8

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.
@@ -0,0 +1,212 @@
1
+ <template>
2
+ <view class="mpx-sticky-header-container">
3
+ <view class="mpx-sticky-header" wx:ref="stickyHeader" wx:style="{{stickyHeaderStyle}}">
4
+ <slot></slot>
5
+ </view>
6
+ <view
7
+ class="mpx-sticky-header-placeholder"
8
+ id="{{stickyId}}"
9
+ wx:style="{{placeholderStyle}}"
10
+ ></view>
11
+ </view>
12
+ </template>
13
+
14
+ <script>
15
+ import mpx, { createComponent } from '@mpxjs/core'
16
+
17
+ createComponent({
18
+ properties: {
19
+ offsetTop: {
20
+ type: Number,
21
+ value: 0
22
+ },
23
+ scrollViewId: {
24
+ type: String,
25
+ value: ''
26
+ },
27
+ stickyId: {
28
+ type: String,
29
+ value: ''
30
+ },
31
+ padding: Array,
32
+ enablePolling: {
33
+ type: Boolean,
34
+ value: false
35
+ },
36
+ pollingDuration: {
37
+ type: Number,
38
+ value: 300
39
+ }
40
+ },
41
+ data: {
42
+ isStickOnTop: false,
43
+ scrollOffsetTop: 0,
44
+ headerHeight: 0,
45
+ stickyHeader: '',
46
+ headerTop: 0,
47
+ lastIntersectionRatio: -1,
48
+ pollingTimer: null
49
+ },
50
+ computed: {
51
+ paddingStyle() {
52
+ if (!this.padding || !Array.isArray(this.padding)) {
53
+ return ''
54
+ }
55
+ const [top = 0, right = 0, bottom = 0, left = 0] = this.padding
56
+ return `padding: ${top}px ${right}px ${bottom}px ${left}px;`
57
+ },
58
+ stickyHeaderStyle() {
59
+ const baseStyle = this.isStickOnTop
60
+ ? `position: fixed;top: ${this.scrollOffsetTop + this.offsetTop}px;`
61
+ : ''
62
+ return baseStyle + this.paddingStyle
63
+ },
64
+ placeholderStyle() {
65
+ const position = this.isStickOnTop ? 'relative' : 'absolute'
66
+ return `position: ${position};height: ${this.headerHeight}px;`
67
+ }
68
+ },
69
+ ready() {
70
+ if (!this.scrollViewId) {
71
+ console.error('[mpx runtime error]: scroll-view-id is necessary property in ali environment')
72
+ }
73
+ if (!this.stickyId) {
74
+ console.error('[mpx runtime error]: sticky-id is necessary property in ali environment')
75
+ }
76
+ this.initStickyHeader()
77
+ // 启动轮询
78
+ if (this.enablePolling) {
79
+ this.startPolling()
80
+ }
81
+ },
82
+ methods: {
83
+ initStickyHeader() {
84
+ this.createSelectorQuery()
85
+ .select('.mpx-sticky-header')
86
+ .boundingClientRect()
87
+ .exec((rect = []) => {
88
+ this.headerHeight = rect[0]?.height || 0
89
+ })
90
+
91
+ mpx
92
+ .createSelectorQuery()
93
+ .select(`#${this.scrollViewId}`)
94
+ .boundingClientRect()
95
+ .exec((res) => {
96
+ if (!res) return
97
+ this.scrollOffsetTop = res[0]?.top || 0
98
+ this.stickyHeader = mpx.createIntersectionObserver({
99
+ thresholds: [0.1, 0.9]
100
+ })
101
+ this.initObserver()
102
+ })
103
+ },
104
+ initObserver() {
105
+ this.stickyHeader.relativeTo(`#${this.scrollViewId}`).observe(`#${this.stickyId}`, (res) => {
106
+ const { intersectionRatio, intersectionRect, relativeRect, boundingClientRect } = res
107
+ if (intersectionRatio < this.lastIntersectionRatio) {
108
+ // boundingClientRect.top < relativeRect.top fixed,否则默认布局
109
+ this.handleStickyStateChange(boundingClientRect.top < relativeRect.top)
110
+ } else if (intersectionRatio > this.lastIntersectionRatio) {
111
+ this.handleStickyStateChange(false)
112
+ }
113
+ this.lastIntersectionRatio = intersectionRatio
114
+ })
115
+ },
116
+ refresh() {
117
+ // 并行执行两个独立的查询
118
+ Promise.all([
119
+ // 查询1:组件内部元素
120
+ new Promise((resolve) => {
121
+ this.createSelectorQuery()
122
+ .select('.mpx-sticky-header')
123
+ .boundingClientRect()
124
+ .select('.mpx-sticky-header-placeholder')
125
+ .boundingClientRect()
126
+ .exec((results) => {
127
+ resolve(results)
128
+ })
129
+ }),
130
+ // 查询2:页面级scrollView
131
+ new Promise((resolve) => {
132
+ mpx.createSelectorQuery()
133
+ .select(`#${this.scrollViewId}`)
134
+ .boundingClientRect()
135
+ .select(`#${this.scrollViewId}`)
136
+ .scrollOffset()
137
+ .exec((results) => {
138
+ resolve(results)
139
+ })
140
+ })
141
+ ]).then(([componentResults, scrollResults]) => {
142
+ const [stickyHeaderRect, placeholderRect] = componentResults
143
+ const [scrollViewRect, scrollOffsetData] = scrollResults
144
+
145
+ if (!stickyHeaderRect || !placeholderRect || !scrollViewRect || !scrollOffsetData) return
146
+
147
+ this.scrollOffsetTop = scrollViewRect.top || 0
148
+ this.headerHeight = stickyHeaderRect.height
149
+
150
+ // 模拟 offsetTop 计算:sticky placeholder具体顶部距离 - scrollView顶部 + scrollView滚动位置
151
+ // 必须用 placeholder 到顶部距离,用 sticky 如果刚好差值为 0, 区分不出是本身就在顶部还是 fixed 在顶部
152
+ this.headerTop = placeholderRect.top - this.scrollOffsetTop + scrollOffsetData.scrollTop
153
+
154
+ this.handleStickyStateChange(scrollOffsetData.scrollTop > this.headerTop)
155
+ })
156
+ },
157
+ handleStickyStateChange(shouldStick) {
158
+ // 如果状态没有变化,直接返回
159
+ if (shouldStick === this.isStickOnTop) {
160
+ return
161
+ }
162
+ // 更新状态
163
+ this.isStickOnTop = shouldStick
164
+ // 触发事件
165
+ this.triggerEvent('stickontopchange', {
166
+ isStickOnTop: shouldStick,
167
+ id: this.stickyId
168
+ })
169
+ },
170
+ // 启动轮询
171
+ startPolling() {
172
+ if (this.pollingTimer) {
173
+ clearInterval(this.pollingTimer)
174
+ }
175
+ this.pollingTimer = setInterval(() => {
176
+ this.refresh()
177
+ }, this.pollingDuration)
178
+ },
179
+ // 停止轮询
180
+ stopPolling() {
181
+ if (this.pollingTimer) {
182
+ clearInterval(this.pollingTimer)
183
+ this.pollingTimer = null
184
+ }
185
+ }
186
+ },
187
+ detached() {
188
+ // 清理观察器
189
+ if (this.stickyHeader) {
190
+ this.stickyHeader.disconnect()
191
+ }
192
+ // 清理轮询定时器
193
+ if (this.pollingTimer) {
194
+ clearInterval(this.pollingTimer)
195
+ this.pollingTimer = null
196
+ }
197
+ }
198
+ })
199
+ </script>
200
+ <style lang="stylus" scoped>
201
+ .mpx-sticky-header-container
202
+ position relative
203
+ .mpx-sticky-header-placeholder
204
+ position absolute
205
+ left 0
206
+ right 0
207
+ top 0
208
+ pointer-events none
209
+ .mpx-sticky-header
210
+ width 100%
211
+ box-sizing border-box
212
+ </style>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <view class="mpx-sticky-section">
3
+ <slot></slot>
4
+ </view>
5
+ </template>
6
+
7
+ <script>
8
+ import { createComponent } from '@mpxjs/core'
9
+
10
+ createComponent({
11
+
12
+ })
13
+ </script>
14
+ <style lang="stylus" scoped>
15
+ .mpx-sticky-section
16
+ position relative
17
+ </style>
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ interface ListItem {
3
+ isSectionHeader?: boolean;
4
+ _originalItemIndex?: number;
5
+ [key: string]: any;
6
+ }
7
+ interface ItemHeightType {
8
+ value?: number;
9
+ getter?: (item: any, index: number) => number;
10
+ }
11
+ interface RecycleViewProps {
12
+ enhanced?: boolean;
13
+ bounces?: boolean;
14
+ scrollEventThrottle?: number;
15
+ height?: number | string;
16
+ width?: number | string;
17
+ listData?: ListItem[];
18
+ generichash?: string;
19
+ style?: Record<string, any>;
20
+ itemHeight?: ItemHeightType;
21
+ sectionHeaderHeight?: ItemHeightType;
22
+ listHeaderData?: any;
23
+ listHeaderHeight?: ItemHeightType;
24
+ useListHeader?: boolean;
25
+ 'genericrecycle-item'?: string;
26
+ 'genericsection-header'?: string;
27
+ 'genericlist-header'?: string;
28
+ 'enable-var'?: boolean;
29
+ 'external-var-context'?: any;
30
+ 'parent-font-size'?: number;
31
+ 'parent-width'?: number;
32
+ 'parent-height'?: number;
33
+ 'enable-sticky'?: boolean;
34
+ 'enable-back-to-top'?: boolean;
35
+ 'end-reached-threshold'?: number;
36
+ 'refresher-enabled'?: boolean;
37
+ 'show-scrollbar'?: boolean;
38
+ 'refresher-triggered'?: boolean;
39
+ bindrefresherrefresh?: (event: any) => void;
40
+ bindscrolltolower?: (event: any) => void;
41
+ bindscroll?: (event: any) => void;
42
+ [key: string]: any;
43
+ }
44
+ declare const RecycleView: React.ForwardRefExoticComponent<Omit<RecycleViewProps, "ref"> & React.RefAttributes<any>>;
45
+ export default RecycleView;
@@ -0,0 +1,272 @@
1
+ import React, { forwardRef, useRef, useState, useEffect, useMemo, createElement, useImperativeHandle } from 'react';
2
+ import { SectionList, RefreshControl } from 'react-native';
3
+ import useInnerProps, { getCustomEvent } from './getInnerListeners';
4
+ import { extendObject, useLayout, useTransformStyle } from './utils';
5
+ const getGeneric = (generichash, generickey) => {
6
+ if (!generichash || !generickey)
7
+ return null;
8
+ const GenericComponent = global.__mpxGenericsMap?.[generichash]?.[generickey]?.();
9
+ if (!GenericComponent)
10
+ return null;
11
+ return forwardRef((props, ref) => {
12
+ return createElement(GenericComponent, extendObject({}, {
13
+ ref: ref
14
+ }, props));
15
+ });
16
+ };
17
+ const getListHeaderComponent = (generichash, generickey, data) => {
18
+ if (!generichash || !generickey)
19
+ return undefined;
20
+ const ListHeaderComponent = getGeneric(generichash, generickey);
21
+ return ListHeaderComponent ? createElement(ListHeaderComponent, { listHeaderData: data }) : null;
22
+ };
23
+ const getSectionHeaderRenderer = (generichash, generickey) => {
24
+ if (!generichash || !generickey)
25
+ return undefined;
26
+ return (sectionData) => {
27
+ if (!sectionData.section.hasSectionHeader)
28
+ return null;
29
+ const SectionHeaderComponent = getGeneric(generichash, generickey);
30
+ return SectionHeaderComponent ? createElement(SectionHeaderComponent, { itemData: sectionData.section.headerData }) : null;
31
+ };
32
+ };
33
+ const getItemRenderer = (generichash, generickey) => {
34
+ if (!generichash || !generickey)
35
+ return undefined;
36
+ return ({ item }) => {
37
+ const ItemComponent = getGeneric(generichash, generickey);
38
+ return ItemComponent ? createElement(ItemComponent, { itemData: item }) : null;
39
+ };
40
+ };
41
+ const RecycleView = forwardRef((props = {}, ref) => {
42
+ const { enhanced = false, bounces = true, scrollEventThrottle = 0, height, width, listData, generichash, style = {}, itemHeight = {}, sectionHeaderHeight = {}, listHeaderHeight = {}, listHeaderData = null, useListHeader = false, 'genericrecycle-item': genericrecycleItem, 'genericsection-header': genericsectionHeader, 'genericlist-header': genericListHeader, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, 'enable-sticky': enableSticky = false, 'enable-back-to-top': enableBackToTop = false, 'end-reached-threshold': onEndReachedThreshold = 0.1, 'refresher-enabled': refresherEnabled, 'show-scrollbar': showScrollbar = true, 'refresher-triggered': refresherTriggered } = props;
43
+ const [refreshing, setRefreshing] = useState(!!refresherTriggered);
44
+ const scrollViewRef = useRef(null);
45
+ const indexMap = useRef({});
46
+ const reverseIndexMap = useRef({});
47
+ const { hasSelfPercent, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
48
+ const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef });
49
+ useEffect(() => {
50
+ if (refreshing !== refresherTriggered) {
51
+ setRefreshing(!!refresherTriggered);
52
+ }
53
+ }, [refresherTriggered]);
54
+ const onRefresh = () => {
55
+ const { bindrefresherrefresh } = props;
56
+ bindrefresherrefresh &&
57
+ bindrefresherrefresh(getCustomEvent('refresherrefresh', {}, { layoutRef }, props));
58
+ };
59
+ const onEndReached = () => {
60
+ const { bindscrolltolower } = props;
61
+ bindscrolltolower &&
62
+ bindscrolltolower(getCustomEvent('scrolltolower', {}, { layoutRef }, props));
63
+ };
64
+ const onScroll = (event) => {
65
+ const { bindscroll } = props;
66
+ bindscroll &&
67
+ bindscroll(getCustomEvent('scroll', event.nativeEvent, { layoutRef }, props));
68
+ };
69
+ // 通过sectionIndex和rowIndex获取原始索引
70
+ const getOriginalIndex = (sectionIndex, rowIndex) => {
71
+ const key = `${sectionIndex}_${rowIndex}`;
72
+ return reverseIndexMap.current[key] ?? -1; // 如果找不到,返回-1
73
+ };
74
+ const scrollToIndex = ({ index, animated, viewOffset = 0, viewPosition = 0 }) => {
75
+ if (scrollViewRef.current) {
76
+ // 通过索引映射表快速定位位置
77
+ const position = indexMap.current[index];
78
+ const [sectionIndex, itemIndex] = position.split('_');
79
+ scrollViewRef.current.scrollToLocation?.({
80
+ itemIndex: itemIndex === 'header' ? 0 : Number(itemIndex) + 1,
81
+ sectionIndex: Number(sectionIndex) || 0,
82
+ animated,
83
+ viewOffset,
84
+ viewPosition
85
+ });
86
+ }
87
+ };
88
+ const getItemHeight = ({ sectionIndex, rowIndex }) => {
89
+ if (!itemHeight) {
90
+ return 0;
91
+ }
92
+ if (itemHeight.getter) {
93
+ const item = convertedListData[sectionIndex].data[rowIndex];
94
+ // 使用getOriginalIndex获取原始索引
95
+ const originalIndex = getOriginalIndex(sectionIndex, rowIndex);
96
+ return itemHeight.getter?.(item, originalIndex) || 0;
97
+ }
98
+ else {
99
+ return itemHeight.value || 0;
100
+ }
101
+ };
102
+ const getSectionHeaderHeight = ({ sectionIndex }) => {
103
+ const item = convertedListData[sectionIndex];
104
+ const { hasSectionHeader } = item;
105
+ // 使用getOriginalIndex获取原始索引
106
+ const originalIndex = getOriginalIndex(sectionIndex, 'header');
107
+ if (!hasSectionHeader)
108
+ return 0;
109
+ if (sectionHeaderHeight.getter) {
110
+ return sectionHeaderHeight.getter?.(item, originalIndex) || 0;
111
+ }
112
+ else {
113
+ return sectionHeaderHeight.value || 0;
114
+ }
115
+ };
116
+ const convertedListData = useMemo(() => {
117
+ const sections = [];
118
+ let currentSection = null;
119
+ // 清空之前的索引映射
120
+ indexMap.current = {};
121
+ // 清空反向索引映射
122
+ reverseIndexMap.current = {};
123
+ listData.forEach((item, index) => {
124
+ if (item.isSectionHeader) {
125
+ // 如果已经存在一个 section,先把它添加到 sections 中
126
+ if (currentSection) {
127
+ sections.push(currentSection);
128
+ }
129
+ // 创建新的 section
130
+ currentSection = {
131
+ headerData: item,
132
+ data: [],
133
+ hasSectionHeader: true,
134
+ _originalItemIndex: index
135
+ };
136
+ // 为 section header 添加索引映射
137
+ const sectionIndex = sections.length;
138
+ indexMap.current[index] = `${sectionIndex}_header`;
139
+ // 添加反向索引映射
140
+ reverseIndexMap.current[`${sectionIndex}_header`] = index;
141
+ }
142
+ else {
143
+ // 如果没有当前 section,创建一个默认的
144
+ if (!currentSection) {
145
+ // 创建默认section (无header的section)
146
+ currentSection = {
147
+ headerData: null,
148
+ data: [],
149
+ hasSectionHeader: false,
150
+ _originalItemIndex: -1
151
+ };
152
+ }
153
+ // 将 item 添加到当前 section 的 data 中
154
+ const itemIndex = currentSection.data.length;
155
+ currentSection.data.push(extendObject({}, item, {
156
+ _originalItemIndex: index
157
+ }));
158
+ let sectionIndex;
159
+ // 为 item 添加索引映射 - 存储格式为: "sectionIndex_itemIndex"
160
+ if (!currentSection.hasSectionHeader && sections.length === 0) {
161
+ // 在默认section中(第一个且无header)
162
+ sectionIndex = 0;
163
+ indexMap.current[index] = `${sectionIndex}_${itemIndex}`;
164
+ }
165
+ else {
166
+ // 在普通section中
167
+ sectionIndex = sections.length;
168
+ indexMap.current[index] = `${sectionIndex}_${itemIndex}`;
169
+ }
170
+ // 添加反向索引映射
171
+ reverseIndexMap.current[`${sectionIndex}_${itemIndex}`] = index;
172
+ }
173
+ });
174
+ // 添加最后一个 section
175
+ if (currentSection) {
176
+ sections.push(currentSection);
177
+ }
178
+ return sections;
179
+ }, [listData]);
180
+ const { getItemLayout } = useMemo(() => {
181
+ const layouts = [];
182
+ let offset = 0;
183
+ if (useListHeader) {
184
+ // 计算列表头部的高度
185
+ offset += listHeaderHeight.getter?.() || listHeaderHeight.value || 0;
186
+ }
187
+ // 遍历所有 sections
188
+ convertedListData.forEach((section, sectionIndex) => {
189
+ // 添加 section header 的位置信息
190
+ const headerHeight = getSectionHeaderHeight({ sectionIndex });
191
+ layouts.push({
192
+ length: headerHeight,
193
+ offset,
194
+ index: layouts.length
195
+ });
196
+ offset += headerHeight;
197
+ // 添加该 section 中所有 items 的位置信息
198
+ section.data.forEach((item, itemIndex) => {
199
+ const contenteight = getItemHeight({ sectionIndex, rowIndex: itemIndex });
200
+ layouts.push({
201
+ length: contenteight,
202
+ offset,
203
+ index: layouts.length
204
+ });
205
+ offset += contenteight;
206
+ });
207
+ // 添加该 section 尾部位置信息
208
+ // 因为即使 sectionList 没传 renderSectionFooter,getItemLayout 中的 index 的计算也会包含尾部节点
209
+ layouts.push({
210
+ length: 0,
211
+ offset,
212
+ index: layouts.length
213
+ });
214
+ });
215
+ return {
216
+ itemLayouts: layouts,
217
+ getItemLayout: (data, index) => layouts[index]
218
+ };
219
+ }, [convertedListData, useListHeader]);
220
+ const scrollAdditionalProps = extendObject({
221
+ alwaysBounceVertical: false,
222
+ alwaysBounceHorizontal: false,
223
+ scrollEventThrottle: scrollEventThrottle,
224
+ scrollsToTop: enableBackToTop,
225
+ showsHorizontalScrollIndicator: showScrollbar,
226
+ onEndReachedThreshold,
227
+ ref: scrollViewRef,
228
+ bounces: false,
229
+ stickySectionHeadersEnabled: enableSticky,
230
+ onScroll: onScroll,
231
+ onEndReached: onEndReached
232
+ }, layoutProps);
233
+ if (enhanced) {
234
+ Object.assign(scrollAdditionalProps, {
235
+ bounces
236
+ });
237
+ }
238
+ if (refresherEnabled) {
239
+ Object.assign(scrollAdditionalProps, {
240
+ refreshing: refreshing
241
+ });
242
+ }
243
+ useImperativeHandle(ref, () => {
244
+ return {
245
+ ...props,
246
+ scrollToIndex
247
+ };
248
+ });
249
+ const innerProps = useInnerProps(extendObject({}, props, scrollAdditionalProps), [
250
+ 'id',
251
+ 'show-scrollbar',
252
+ 'lower-threshold',
253
+ 'refresher-triggered',
254
+ 'refresher-enabled',
255
+ 'bindrefresherrefresh'
256
+ ], { layoutRef });
257
+ return createElement(SectionList, extendObject({
258
+ style: [{ height, width }, style, layoutStyle],
259
+ sections: convertedListData,
260
+ renderItem: getItemRenderer(generichash, genericrecycleItem),
261
+ getItemLayout: getItemLayout,
262
+ ListHeaderComponent: useListHeader ? getListHeaderComponent(generichash, genericListHeader, listHeaderData) : null,
263
+ renderSectionHeader: getSectionHeaderRenderer(generichash, genericsectionHeader),
264
+ refreshControl: refresherEnabled
265
+ ? React.createElement(RefreshControl, {
266
+ onRefresh: onRefresh,
267
+ refreshing: refreshing
268
+ })
269
+ : undefined
270
+ }, innerProps));
271
+ });
272
+ export default RecycleView;