hy-app 0.7.1 → 0.7.2

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.
@@ -1,228 +1,346 @@
1
- <template>
2
- <scroll-view
3
- ref="hyVirtualContainer"
4
- @scroll="onScroll"
5
- @scrolltolower="scrollToLower"
6
- :lower-threshold="showDivider ? 100 : 50"
7
- :scroll-y="true"
8
- scroll-with-animation
9
- class="hy-virtual-container"
10
- >
11
- <view class="hy-virtual-container__list">
12
- <slot v-if="slotDefault" :record="line === 1 ? virtualData : waterfall"></slot>
13
- <template v-else>
14
- <view
15
- v-if="line === 1"
16
- class="hy-virtual-container__list--item"
17
- v-for="(item, i) in virtualData"
18
- :key="typeof item === 'string' ? i : item[keyField]"
19
- :style="itemStyle"
20
- @click="handleClick(item)"
21
- >
22
- <slot style="height: 100%" name="content" :record="item"></slot>
23
- </view>
24
-
25
- <view
26
- v-if="line === 2"
27
- class="hy-virtual-container__list--left hy-virtual-container__list--box"
28
- >
29
- <view
30
- v-if="slots.left"
31
- class="hy-virtual-container__list--box-item"
32
- v-for="item in waterfall.left"
33
- :key="item[keyField]"
34
- :style="itemStyle"
35
- @click="handleClick(item)"
36
- >
37
- <slot name="left" :record="item"></slot>
38
- </view>
39
- <slot v-else name="left-list" :record="waterfall.left"></slot>
40
- </view>
41
- <view
42
- v-if="line === 2"
43
- class="hy-virtual-container__list--right hy-virtual-container__list--box"
44
- >
45
- <view
46
- v-if="slots.right"
47
- class="hy-virtual-container__list--box-item"
48
- v-for="item in waterfall.right"
49
- :key="item[keyField]"
50
- :style="itemStyle"
51
- @click="handleClick(item)"
52
- >
53
- <slot name="right" :record="item"></slot>
54
- </view>
55
- <slot v-else name="right-list" :record="waterfall.right"></slot>
56
- </view>
57
- </template>
58
- <!--加载更多样式-->
59
- </view>
60
- <slot v-if="$slots.footer" name="footer"></slot>
61
- <hy-divider :text="load" v-else-if="showDivider"></hy-divider>
62
- </scroll-view>
63
- </template>
64
-
65
- <script lang="ts">
66
- export default {
67
- name: 'hy-list',
68
- options: {
69
- addGlobalClass: true,
70
- virtualHost: true,
71
- styleIsolation: 'shared'
72
- }
73
- }
74
- </script>
75
-
76
- <script lang="ts" setup>
77
- import {
78
- computed,
79
- getCurrentInstance,
80
- nextTick,
81
- onMounted,
82
- reactive,
83
- ref,
84
- useSlots,
85
- watch
86
- } from 'vue'
87
- import type { CSSProperties } from 'vue'
88
- import { addUnit, getPx, getRect } from '../../libs'
89
- import type { IListEmits } from './typing'
90
- import listProps from './props'
91
- // 组件
92
- import HyDivider from '../hy-divider/hy-divider.vue'
93
-
94
- /**
95
- * 实现只展示可视内容的dom,减少dom创建,优化滚动性能
96
- * @displayName hy-list
97
- */
98
- defineOptions({})
99
-
100
- const props = defineProps(listProps)
101
- const emit = defineEmits<IListEmits>()
102
-
103
- const slots = useSlots()
104
- // 滚动条距离顶部距离
105
- const scrollTop = ref(0)
106
- // 可视区域的高度
107
- const viewHeight = ref(0)
108
- const waterfall: {
109
- left: AnyObject[]
110
- right: AnyObject[]
111
- } = reactive({
112
- left: [],
113
- right: []
114
- })
115
- // 排列方式
116
- const arrange = computed(() => (props.line === 1 ? 'column' : 'row'))
117
- const listHeight = addUnit(props.containerHeight)
118
- const instance = getCurrentInstance()
119
-
120
- onMounted(() => {
121
- nextTick(async () => {
122
- const res = await getRect('.hy-virtual-container', false, instance)
123
- viewHeight.value = (res as UniApp.NodeInfo).height || 0
124
- })
125
- })
126
-
127
- const boxHeight = computed(() => {
128
- return getPx(props.itemHeight) + getPx(props.marginBottom)
129
- })
130
- const itemStyle = computed((): CSSProperties => {
131
- return {
132
- height: addUnit(props.itemHeight),
133
- padding: addUnit(props.padding),
134
- marginBottom: addUnit(props.marginBottom),
135
- borderRadius: addUnit(props.borderRadius),
136
- background: props.background,
137
- border: props.border ? '1px solid #dadbde' : ''
138
- }
139
- })
140
-
141
- /**
142
- * 虚拟列表真实展示数据:起始下标
143
- */
144
- const start = computed(() => {
145
- const s = Math.floor(scrollTop.value / boxHeight.value)
146
- return Math.max(0, s * props.line)
147
- })
148
-
149
- /**
150
- * 虚拟列表真实展示数据:结束下标
151
- */
152
- const over = computed(() => {
153
- const o = Math.floor((scrollTop.value + viewHeight.value + 1) / boxHeight.value + 5)
154
- return Math.min(props.list.length, o * props.line)
155
- })
156
-
157
- /**
158
- * 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动)
159
- */
160
- const paddingAttr = computed(() => {
161
- const paddingTop = start.value * boxHeight.value
162
- const paddingBottom = (props.list.length - over.value) * boxHeight.value
163
- return `${paddingTop / props.line}px 0 ${paddingBottom / props.line}px`
164
- })
165
-
166
- /**
167
- * 虚拟列表真实展示数据
168
- */
169
- const virtualData = computed<(string | Record<string, any>)[]>(() => {
170
- return props.list.slice(start.value, over.value)
171
- })
172
-
173
- watch(
174
- () => virtualData.value,
175
- (newVal) => {
176
- waterfall.left.length = 0
177
- waterfall.right.length = 0
178
- if (props.line === 2 && newVal!.every((item) => typeof item !== 'string')) {
179
- newVal.forEach((item, i) => {
180
- if (i % 2 === 0) {
181
- waterfall.left.push(item as AnyObject)
182
- } else {
183
- waterfall.right.push(item as AnyObject)
184
- }
185
- })
186
- }
187
- },
188
- { immediate: true, deep: true }
189
- )
190
-
191
- /**
192
- * 监听滚动条距离顶部距离,实时更新
193
- */
194
- const onScroll = async (e: any) => {
195
- scrollTop.value = e.detail.scrollTop || 0
196
- }
197
-
198
- /**
199
- * 滚动底部函数
200
- * */
201
- const scrollToLower = () => {
202
- emit('scrollToLower')
203
- }
204
-
205
- /**
206
- * 点击行触发函数
207
- * */
208
- const handleClick = (temp: string | AnyObject) => {
209
- emit('click', temp)
210
- }
211
-
212
- /**
213
- * 获取默认插槽
214
- */
215
- const slotDefault = useSlots().default
216
- </script>
217
-
218
- <style lang="scss" scoped>
219
- @import './index.scss';
220
- .hy-virtual-container {
221
- height: v-bind(listHeight);
222
- &__list {
223
- padding: v-bind(paddingAttr);
224
- display: flex;
225
- flex-direction: v-bind(arrange);
226
- }
227
- }
228
- </style>
1
+ <template>
2
+ <scroll-view
3
+ ref="hyVirtualContainer"
4
+ @scroll="onScroll"
5
+ @scrolltolower="scrollToLower"
6
+ :lower-threshold="showDivider ? 100 : 50"
7
+ :scroll-y="true"
8
+ scroll-with-animation
9
+ class="hy-virtual-container"
10
+ >
11
+ <view class="hy-virtual-container__list">
12
+ <slot v-if="slotDefault" :record="line === 1 ? virtualData : waterfall"></slot>
13
+ <template v-else>
14
+ <view
15
+ v-if="line === 1"
16
+ class="hy-virtual-container__list--item"
17
+ v-for="(item, i) in virtualData"
18
+ :key="typeof item === 'string' ? i : item[keyField]"
19
+ :style="itemStyle"
20
+ @click="handleClick(item)"
21
+ >
22
+ <slot style="height: 100%" name="content" :record="item"></slot>
23
+ </view>
24
+
25
+ <view
26
+ v-if="line === 2"
27
+ class="hy-virtual-container__list--left hy-virtual-container__list--box"
28
+ >
29
+ <view
30
+ v-if="slots.left"
31
+ class="hy-virtual-container__list--box-item"
32
+ v-for="item in waterfall.left"
33
+ :key="item[keyField]"
34
+ :style="itemStyle"
35
+ @click="handleClick(item)"
36
+ >
37
+ <slot name="left" :record="item"></slot>
38
+ </view>
39
+ <slot v-else name="left-list" :record="waterfall.left"></slot>
40
+ </view>
41
+ <view
42
+ v-if="line === 2"
43
+ class="hy-virtual-container__list--right hy-virtual-container__list--box"
44
+ >
45
+ <view
46
+ v-if="slots.right"
47
+ class="hy-virtual-container__list--box-item"
48
+ v-for="item in waterfall.right"
49
+ :key="item[keyField]"
50
+ :style="itemStyle"
51
+ @click="handleClick(item)"
52
+ >
53
+ <slot name="right" :record="item"></slot>
54
+ </view>
55
+ <slot v-else name="right-list" :record="waterfall.right"></slot>
56
+ </view>
57
+ </template>
58
+ <!--加载更多样式-->
59
+ </view>
60
+ <slot v-if="$slots.footer" name="footer"></slot>
61
+ <hy-divider :text="load" v-else-if="showDivider"></hy-divider>
62
+ </scroll-view>
63
+ </template>
64
+
65
+ <script lang="ts">
66
+ export default {
67
+ name: 'hy-list',
68
+ options: {
69
+ addGlobalClass: true,
70
+ virtualHost: true,
71
+ styleIsolation: 'shared'
72
+ }
73
+ }
74
+ </script>
75
+
76
+ <script lang="ts" setup>
77
+ import {
78
+ computed,
79
+ getCurrentInstance,
80
+ nextTick,
81
+ onMounted,
82
+ onUnmounted,
83
+ reactive,
84
+ ref,
85
+ useSlots,
86
+ watch
87
+ } from 'vue'
88
+ import type { CSSProperties } from 'vue'
89
+ import { addUnit, getPx, getRect, throttle } from '../../libs'
90
+ import type { IListEmits } from './typing'
91
+ import listProps from './props'
92
+ // 组件
93
+ import HyDivider from '../hy-divider/hy-divider.vue'
94
+
95
+ /**
96
+ * 实现只展示可视内容的dom,减少dom创建,优化滚动性能
97
+ * @displayName hy-list
98
+ */
99
+ defineOptions({})
100
+
101
+ const props = defineProps(listProps)
102
+ const emit = defineEmits<IListEmits>()
103
+
104
+ const slots = useSlots()
105
+ // 滚动容器引用
106
+ const hyVirtualContainer = ref<UniApp.NodeInfo | null>(null)
107
+ // 滚动条距离顶部距离
108
+ const scrollTop = ref(0)
109
+ // 可视区域的高度
110
+ const viewHeight = ref(0)
111
+ // 瀑布流数据
112
+ const waterfall: {
113
+ left: AnyObject[]
114
+ right: AnyObject[]
115
+ } = reactive({
116
+ left: [],
117
+ right: []
118
+ })
119
+ // 排列方式
120
+ const arrange = computed(() => (props.line === 1 ? 'column' : 'row'))
121
+ const listHeight = addUnit(props.containerHeight)
122
+ const instance = getCurrentInstance()
123
+ // 高度缓存映射表(支持动态高度)
124
+ const heightCache = reactive<Record<number, number>>({})
125
+ // 预估高度(用于首屏渲染)
126
+ const estimatedHeight = computed(() => getPx(props.itemHeight) + getPx(props.marginBottom))
127
+
128
+ onMounted(() => {
129
+ nextTick(async () => {
130
+ const res = await getRect('.hy-virtual-container', false, instance)
131
+ viewHeight.value = (res as UniApp.NodeInfo).height || 0
132
+ })
133
+ })
134
+
135
+ /**
136
+ * 获取指定索引的实际高度(优先从缓存获取)
137
+ */
138
+ const getItemHeight = (index: number): number => {
139
+ return heightCache[index] ?? estimatedHeight.value
140
+ }
141
+
142
+ /**
143
+ * 计算从0到指定索引的累计高度
144
+ */
145
+ const getCumulativeHeight = (endIndex: number): number => {
146
+ let total = 0
147
+ for (let i = 0; i < endIndex && i < props.list.length; i++) {
148
+ total += getItemHeight(i)
149
+ }
150
+ return total
151
+ }
152
+
153
+ /**
154
+ * 计算虚拟列表的总高度
155
+ */
156
+ const totalHeight = computed(() => {
157
+ return getCumulativeHeight(props.list.length)
158
+ })
159
+ const itemStyle = computed((): CSSProperties => {
160
+ return {
161
+ height: addUnit(props.itemHeight),
162
+ padding: addUnit(props.padding),
163
+ marginBottom: addUnit(props.marginBottom),
164
+ borderRadius: addUnit(props.borderRadius),
165
+ background: props.background,
166
+ border: props.border ? '1px solid #dadbde' : ''
167
+ }
168
+ })
169
+
170
+ /**
171
+ * 虚拟列表真实展示数据:起始下标(支持动态高度)
172
+ */
173
+ const start = computed(() => {
174
+ if (props.list.length === 0) return 0
175
+
176
+ let cumulativeHeight = 0
177
+ let startIndex = 0
178
+
179
+ // 向后查找第一个累计高度超过scrollTop的位置
180
+ for (let i = 0; i < props.list.length; i++) {
181
+ cumulativeHeight += getItemHeight(i)
182
+ if (cumulativeHeight > scrollTop.value) {
183
+ startIndex = i
184
+ break
185
+ }
186
+ }
187
+
188
+ // 向上多取几个作为缓冲,避免快速滚动时出现空白
189
+ const buffer = props.line * 2
190
+ return Math.max(0, startIndex - buffer)
191
+ })
192
+
193
+ /**
194
+ * 虚拟列表真实展示数据:结束下标(支持动态高度)
195
+ */
196
+ const over = computed(() => {
197
+ if (props.list.length === 0) return 0
198
+
199
+ const targetHeight = scrollTop.value + viewHeight.value + 100 // 额外缓冲高度
200
+ let cumulativeHeight = 0
201
+
202
+ // 从start开始查找第一个累计高度超过targetHeight的位置
203
+ for (let i = start.value; i < props.list.length; i++) {
204
+ cumulativeHeight += getItemHeight(i)
205
+ if (cumulativeHeight > targetHeight) {
206
+ // 多取几个作为缓冲
207
+ return Math.min(props.list.length, i + props.line * 2)
208
+ }
209
+ }
210
+
211
+ return props.list.length
212
+ })
213
+
214
+ /**
215
+ * 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动)
216
+ */
217
+ const paddingAttr = computed(() => {
218
+ const paddingTop = getCumulativeHeight(start.value)
219
+ const paddingBottom = Math.max(0, totalHeight.value - getCumulativeHeight(over.value))
220
+ return `${paddingTop}px 0 ${paddingBottom}px`
221
+ })
222
+
223
+ /**
224
+ * 虚拟列表真实展示数据
225
+ */
226
+ const virtualData = computed<(string | Record<string, any>)[]>(() => {
227
+ return props.list.slice(start.value, over.value)
228
+ })
229
+
230
+ /**
231
+ * 更新瀑布流数据
232
+ */
233
+ const updateWaterfall = (data: (string | Record<string, any>)[]) => {
234
+ // 使用splice替代length=0,性能更好
235
+ waterfall.left.splice(0, waterfall.left.length)
236
+ waterfall.right.splice(0, waterfall.right.length)
237
+
238
+ if (props.line === 2 && data.length > 0 && typeof data[0] !== 'string') {
239
+ // 优化:使用push批量添加
240
+ const leftItems: AnyObject[] = []
241
+ const rightItems: AnyObject[] = []
242
+
243
+ for (let i = 0; i < data.length; i++) {
244
+ if (i % 2 === 0) {
245
+ leftItems.push(data[i] as AnyObject)
246
+ } else {
247
+ rightItems.push(data[i] as AnyObject)
248
+ }
249
+ }
250
+
251
+ waterfall.left.push(...leftItems)
252
+ waterfall.right.push(...rightItems)
253
+ }
254
+ }
255
+
256
+ watch(
257
+ () => virtualData.value,
258
+ (newVal) => {
259
+ updateWaterfall(newVal)
260
+ },
261
+ { immediate: true }
262
+ )
263
+
264
+ /**
265
+ * 监听滚动条距离顶部距离,实时更新(带节流)
266
+ */
267
+ const onScroll = throttle((e: any) => {
268
+ scrollTop.value = e.detail.scrollTop || 0
269
+ }, 50)
270
+
271
+ /**
272
+ * 滚动底部函数
273
+ * */
274
+ const scrollToLower = () => {
275
+ emit('scrollToLower')
276
+ }
277
+
278
+ /**
279
+ * 点击行触发函数
280
+ * */
281
+ const handleClick = (temp: string | AnyObject) => {
282
+ emit('click', temp)
283
+ }
284
+
285
+ /**
286
+ * 获取默认插槽
287
+ */
288
+ const slotDefault = useSlots().default
289
+
290
+ /**
291
+ * 滚动到指定索引位置
292
+ * @param index 目标索引
293
+ * @param offset 偏移量(可选)
294
+ */
295
+ const scrollToIndex = (index: number, offset: number = 0) => {
296
+ if (index < 0 || index >= props.list.length) {
297
+ console.warn(`hy-list: scrollToIndex index ${index} out of bounds`)
298
+ return
299
+ }
300
+
301
+ const scrollPosition = getCumulativeHeight(index) + offset
302
+ uni.createSelectorQuery().select('.hy-virtual-container').scrollIntoView({
303
+ scrollTop: scrollPosition,
304
+ duration: 300
305
+ })
306
+ }
307
+
308
+ /**
309
+ * 滚动到顶部
310
+ */
311
+ const scrollToTop = () => {
312
+ uni.createSelectorQuery().select('.hy-virtual-container').scrollIntoView({
313
+ scrollTop: 0,
314
+ duration: 300
315
+ })
316
+ }
317
+
318
+ /**
319
+ * 刷新高度缓存(当列表内容变化时调用)
320
+ */
321
+ const refreshHeightCache = () => {
322
+ // 清除缓存,下次渲染时重新计算
323
+ Object.keys(heightCache).forEach((key) => {
324
+ delete heightCache[parseInt(key)]
325
+ })
326
+ }
327
+
328
+ // 暴露方法给父组件
329
+ defineExpose({
330
+ scrollToIndex,
331
+ scrollToTop,
332
+ refreshHeightCache
333
+ })
334
+ </script>
335
+
336
+ <style lang="scss" scoped>
337
+ @import './index.scss';
338
+ .hy-virtual-container {
339
+ height: v-bind(listHeight);
340
+ &__list {
341
+ padding: v-bind(paddingAttr);
342
+ display: flex;
343
+ flex-direction: v-bind(arrange);
344
+ }
345
+ }
346
+ </style>
@@ -2,7 +2,6 @@
2
2
  @use "../../libs/css/mixin" as *;
3
3
 
4
4
  @include b(virtual-container) {
5
- padding: 0 $hy-border-margin-padding-base;
6
5
  box-sizing: border-box;
7
6
 
8
7
  @include e(list) {