小程序瀑布流升级版

小程序瀑布流升级版
Dans Roh本文实现了uniapp和taro两个版本的小程序瀑布流。
上文使用双列布局实现了简单版的瀑布流,但是在小程序上面由于css不兼容,以及一次请求数据过多,图片高度不确定,会导致下滑加载数据过程中,页面上下抽搐,卡顿。
于是我换了一种相对复杂的方式,来完成小程序瀑布流的实现。
实现思路
- 使用float布局,分为左侧和右侧两个列表
- 在请求拿到分页数据后,遍历数据,计算leftBox 和 rightBox的高度。
- 比较高度,将数据item push到高度小的那个盒子
uniapp版本
代码
新建WaterfallList文件夹,及文件夹下的index.vue文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65<template>
<view class="waterfallList">
<view id="leftList" class="leftList">
<ProductItem v-for="item in leftData" :key="item.id" :productData="item"></ProductItem>
</view>
<view id="rightList" class="rightList">
<ProductItem v-for="item in rightData" :key="item.id" :productData="item"></ProductItem>
</view>
</view>
</template>
<script>
import ProductItem from "./ProductItem.vue";
export default {
components: {ProductItem},
data() {
return {
leftData: [],
rightData: []
};
},
methods: {
async loadMore(data) {
for (const item of data) {
// 获取高度
const leftListHeight = await this.getDomHeight('#leftList')
const rightListHeight = await this.getDomHeight('#rightList')
if (leftListHeight < rightListHeight) {
this.leftData.push(item)
} else {
this.rightData.push(item)
}
}
},
getDomHeight(selector) {
return new Promise(resolve => {
uni.createSelectorQuery().in(this).select(selector).fields(
{
size: true,
},
(data) => {
resolve(data.height)
}
).exec();
})
}
}
}
</script>
<style lang="scss" scoped>
.waterfallList {
overflow: hidden;
}
.leftList {
width: 48%;
float: left;
}
.rightList {
width: 48%;
float: right;
}
</style>ProductItem组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138<template>
<view class="productItem">
<image class="productImage" :src="productData.pic" lazy-load="true" mode="widthFix"></image>
<view class="container">
<view class="productName">{{ productData.name }}</view>
<view class="priceBox">
<view>
<text class="priceIcon">¥</text>
{{ productData.price }}
<text class="unit" v-if="productData.tag">/箱</text>
</view>
<view class="iconBox">
<uni-icons color="#fff" type="cart" size="32rpx"></uni-icons>
</view>
</view>
<view v-if="productData.tag" class="tag">
<view class="iconBoxTag">
<image class="medalsIcon" :src="getStatic('/index/medals.png')" mode="scaleToFill"></image>
</view>
<view class="tagText">{{productData.tag}}</view>
<view class="tagOrder">TOP 1</view>
</view>
</view>
</view>
</template>
<script>
import {getStatic} from "@/utils/global";
export default {
methods: {getStatic},
props: {
productData: {
type: Object,
default: {}
}
},
data() {
return {};
}
}
</script>
<style lang="scss" scoped>
.productItem {
width: 344rpx;
border-radius: 15rpx;
overflow: hidden;
background-color: #fff;
margin-bottom: 20rpx;
break-inside: avoid;
}
.productItem .productImage {
width: 344rpx;
}
.container {
padding: 0 22rpx 30rpx 22rpx;
font-size: 24rpx;
.productName {
color: #212121;
font-size: 28rpx;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow:hidden;
/*! autoprefixer: off */
-webkit-box-orient: vertical;
}
.priceBox {
color: #ff0000;
font-size: 40rpx;
line-height: 32rpx;
font-weight: bold;
padding-top: 22rpx;
display: flex;
justify-content: space-between;
align-items: center;
.priceIcon {
font-size: 26rpx;
}
.unit {
font-size: 28rpx;
color: #CFCFCF;
}
.iconBox {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
overflow: hidden;
background: linear-gradient( 180deg, #FA2C19 0%, #FE5617 100%);
display: flex;
justify-content: center;
align-items: center;
}
}
.tag {
margin-top: 20rpx;
width: 304rpx;
height: 48rpx;
background: #FAF5EC;
border-radius: 8rpx;
display: flex;
align-items: center;
.iconBoxTag {
width: 52rpx;
height: 48rpx;
overflow: hidden;
background: linear-gradient( 166deg, #E3CB8B 0%, #D5A443 100%);
border-radius: 8rpx 0rpx 8rpx 8rpx;
display: flex;
align-items: center;
justify-content: center;
.medalsIcon {
width: 36rpx;
height: 36rpx;
}
}
.tagText{
margin-left: 16rpx;
font-weight: 500;
font-size: 26rpx;
color: #C89839;
}
.tagOrder {
margin-left: 8rpx;
font-size: 26rpx;
color: #C89839;
font-weight: 500;
}
}
}
</style>在index中引入WaterfallList组件,通过this.$refs调用WaterfallList组件的loadMore方法,将请求的分页数据传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// 下滑加载推荐商品数据
<template>
<WaterfallList ref="waterfallRef"></WaterfallList>
</template>
<script >
import WaterfallList from '@/components/WaterfallList/index.vue'
export default {
components: {
WaterfallList
},
data() {
return {
pageNum: 1,
loadingType: 'more'
}
},
methods: {
async loadListData() {
if (this.loadingType === 'more') {
this.loadingType = 'loading'
// 请求数据
const { data } = await fetchProductList({pageNum: this.pageNum, pageSize:10})
if (data.length === 0) {
this.loadingType = 'nomore'
return
}
// 调用loadMore更新列表
await this.$refs.waterfallRef.loadMore(data)
this.pageNum++
this.loadingType = 'more'
}
},
}
}
</script>
taro版本
tsx部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134import React, { useState, forwardRef, useImperativeHandle, useRef } from 'react';
import Taro from '@tarojs/taro';
import { View } from '@tarojs/components';
import styles from './index.module.less';
type getElementSizeType = (selector: string) => Promise<{ width: number; height: number }>;
interface WaterfallListProps {
renderItem: (item: any[], index: number) => React.ReactNode; // 渲染函数,外部传入的 item 组件
}
/**
* @desc 获取元素大小
* @param selector - DOM 选择器
* @returns Promise<{width: number; height: number}> - 返回元素的宽高
*/
const getElementSize: getElementSizeType = (selector: string) => {
return new Promise((resolve, reject) => {
try {
Taro.createSelectorQuery()
.select(selector)
.boundingClientRect((rect) => {
resolve(rect as { width: number; height: number });
})
.exec();
} catch (e) {
reject(e);
}
});
};
const WaterfallList = ({ renderItem }: WaterfallListProps, ref: React.Ref<any>) => {
// 左右两列数据状态
const [leftColumn, setLeftColumn] = useState<any[]>([]);
const [rightColumn, setRightColumn] = useState<any[]>([]);
// 用于控制异步加载的标志
const loadingFlagRef = useRef<symbol | null>(null);
/**
* @desc 更新指定 ID 项的数据
* @param id - 要更新项的 ID
* @param updatedItem - 更新后的数据
*/
const updateItem = (id: string, updatedItem: any) => {
setLeftColumn((prev) => prev.map((item) => (item.id === id ? updatedItem : item)));
setRightColumn((prev) => prev.map((item) => (item.id === id ? updatedItem : item)));
};
/**
* @desc 重置瀑布流列表状态
*/
const reset = () => {
loadingFlagRef.current = null;
setLeftColumn([]);
setRightColumn([]);
};
useImperativeHandle(ref, () => {
return {
loadMore,
updateItem,
reset,
};
});
/**
* @desc 加载更多数据
* @param data - 要加载的数据数组
* @description
* 1. 创建新的加载标志,防止重复加载
* 2. 遍历数据,对比左右列高度
* 3. 将新数据添加到较短的一列
*/
const loadMore = async (data: Array<any>) => {
const currentLoadingFlag = Symbol('loading');
loadingFlagRef.current = currentLoadingFlag;
for (const item of data) {
// 检查是否是最新的加载请求
if (loadingFlagRef.current !== currentLoadingFlag) {
break;
}
try {
// 获取左右列的高度
const leftRect = await getElementSize('#waterfallListLeftColumn');
const rightRect = await getElementSize('#waterfallListRightColumn');
// 再次检查加载标志,确保数据一致性
if (loadingFlagRef.current !== currentLoadingFlag) {
break;
}
// 比较左右列高度差,将新项添加到较短的一列
if (leftRect.height - rightRect.height <= 5) {
setLeftColumn((prevState) => [...prevState, item]);
} else {
setRightColumn((prevState) => [...prevState, item]);
}
} catch (error) {
console.error('加载出错:', error);
}
}
};
return (
<View className={styles.waterfallList}>
<View
id="waterfallListLeftColumn"
className={`${styles.waterfallColumn} ${styles.leftColumn}`}
>
{leftColumn.map((item, index) => (
<View key={item.id} className={styles.waterfallItem}>
{renderItem(item, index)}
</View>
))}
</View>
<View
id="waterfallListRightColumn"
className={`${styles.waterfallColumn} ${styles.rightColumn}`}
>
{rightColumn.map((item, index) => (
<View key={index} className={styles.waterfallItem}>
{renderItem(item, index)}
</View>
))}
</View>
</View>
);
};
export default forwardRef(WaterfallList);css部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20.waterfallList {
overflow: hidden;
padding: 30px 30px;
min-height: 100vh;
}
.waterfallColumn {
width: 334px;
}
.leftColumn {
float: left;
}
.rightColumn {
float: right;
}
.waterfallItem {
margin-bottom: 20px;
}
评论
匿名评论隐私政策
✅ 你无需删除空行,直接评论以获取最佳展示效果