uni-app实现瀑布流展示
在UniApp中封装一个可自定义列数的瀑布流组件,可以通过Flex布局结合JavaScript动态计算来实现。这种方法兼容性好,能灵活控制列数,并提供良好的用户体验。下面是一个详细的实现方案。
组件设计思路
- 布局选择:使用Flex布局创建多列容器,每列是一个独立的竖向排列的容器,项目按顺序插入到当前高度最小的列中,以实现瀑布流效果。
- 列数自定义:通过组件的columnCount属性允许用户指定列数,默认值为2。可以根据屏幕宽度动态调整列数,例如在宽屏下显示3列,窄屏下显示2列。
- 数据管理:组件接收一个数组作为数据源,每个数据项应包含用于渲染图片、标题和描述等内容的字段。同时,支持通过插槽自定义内容渲染。
- 高度计算:对于每个数据项,需要计算其高度以正确布局。如果数据项包含图片,可以通过uni.getImageInfo获取图片原始宽高,然后根据容器宽度和图片宽高比计算显示高度。如果数据项高度固定或由内容撑开,也可以直接使用固定高度或通过CSS控制。
- 性能优化:使用异步加载图片信息,避免阻塞主线程。在图片加载完成后更新布局。对于大量数据,可以考虑虚拟滚动,但本例暂不实现。
具体实现代码
组件结构 (Waterfall.vue)
点击查看代码
<template>
<view class="waterfall-container">
<view
v-for="(column, index) in columns"
:key="index"
class="waterfall-column"
:style="{ width: columnWidth, 'margin-left': index > 0 ? gap : 0 }"
>
<view
v-for="item in column"
:key="item[keyName]"
class="waterfall-item"
:style="{ 'margin-bottom': itemMarginBottom }"
>
<slot name="item" :item="item">
<!-- 默认插槽内容 -->
<image
v-if="item[imageKey]"
:src="item[imageKey]"
mode="widthFix"
:style="{ width: '100%' }"
@load="onImageLoad($event, item)"
@error="onImageError"
></image>
<text class="item-title">{{ item[nameKey] }}</text>
<text class="item-desc">{{ item[textKey] }}</text>
</slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'Waterfall',
props: {
// 数据源数组
list: {
type: Array,
default: () => []
},
// 数据项中唯一标识的字段名
keyName: {
type: String,
default: 'id'
},
// 图片字段名
imageKey: {
type: String,
default: 'image'
},
// 标题字段名
nameKey: {
type: String,
default: 'name'
},
// 描述字段名
textKey: {
type: String,
default: 'text'
},
// 列数
columnCount: {
type: Number,
default: 2
},
// 列间距(rpx)
gap: {
type: String,
default: '20rpx'
},
// 项目底部间距(rpx)
itemMarginBottom: {
type: String,
default: '20rpx'
}
},
data() {
return {
columns: [], // 存储分配后的列数据
itemHeights: {} // 存储每个项目的高度,用于动态计算布局
}
},
computed: {
// 计算每列的宽度(考虑列间距)
columnWidth() {
// 将列间距从rpx转换为像素,需要获取屏幕宽度
const systemInfo = uni.getSystemInfoSync()
const pixelRatio = 750 / systemInfo.windowWidth
const gapPx = parseInt(this.gap) / pixelRatio // 转换为px
// 总宽度减去所有间隙后均分
const totalGap = gapPx * (this.columnCount - 1)
const width = (systemInfo.windowWidth - totalGap) / this.columnCount
return `${width}px`
}
},
watch: {
// 监听数据源变化,重新分配项目
list: {
handler(newVal) {
this.distributeItems(newVal)
},
immediate: true,
deep: true
},
// 监听列数变化,重新分配项目
columnCount() {
this.distributeItems(this.list)
}
},
methods: {
// 分配项目到各列
distributeItems(items) {
// 初始化列数组和高度数组
const newColumns = Array.from({ length: this.columnCount }, () => [])
const columnHeights = new Array(this.columnCount).fill(0)
// 遍历所有项目
items.forEach(item => {
// 找到高度最小的列
const minHeight = Math.min(...columnHeights)
const columnIndex = columnHeights.indexOf(minHeight)
// 将项目添加到该列
newColumns[columnIndex].push(item)
// 更新该列的高度:加上当前项目的高度和底部间距
// 如果项目高度已知,则使用已知高度;否则使用默认高度或等待图片加载
const itemHeight = this.itemHeights[item[this.keyName]] || this.getEstimatedItemHeight(item)
columnHeights[columnIndex] += itemHeight + this.parseRpxToPx(this.itemMarginBottom)
})
this.columns = newColumns
},
// 解析rpx单位为px
parseRpxToPx(rpxValue) {
const systemInfo = uni.getSystemInfoSync()
const pixelRatio = 750 / systemInfo.windowWidth
return parseInt(rpxValue) / pixelRatio
},
// 获取预估的项目高度(用于初始布局)
getEstimatedItemHeight(item) {
// 如果项目已经有计算过的高度,则返回
if (this.itemHeights[item[this.keyName]]) {
return this.itemHeights[item[this.keyName]]
}
// 否则返回一个预估高度,例如200px
// 在实际应用中,可以根据项目内容提供更精确的预估
return 200
},
// 图片加载完成事件
onImageLoad(e, item) {
// 获取图片原始宽高
const { width: originalWidth, height: originalHeight } = e.detail
// 计算图片在容器中的显示高度(容器宽度已知,根据宽高比计算)
const columnWidthPx = this.parseWidthToPx(this.columnWidth)
const displayHeight = (originalHeight / originalWidth) * columnWidthPx
// 存储项目高度(图片高度加上标题和描述的高度)
// 这里简单起见,假设标题和描述总高度为80px,实际应根据内容计算
const totalItemHeight = displayHeight + 80
this.$set(this.itemHeights, item[this.keyName], totalItemHeight)
// 重新分配项目,以优化布局
this.$nextTick(() => {
this.distributeItems(this.list)
})
},
// 图片加载失败事件
onImageError() {
console.error('图片加载失败')
},
// 将宽度值(如'100px')转换为像素数值
parseWidthToPx(widthValue) {
if (typeof widthValue === 'string' && widthValue.includes('px')) {
return parseFloat(widthValue)
}
// 如果已经是数字,直接返回
return Number(widthValue)
}
}
}
</script>
<style scoped>
.waterfall-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.waterfall-column {
display: flex;
flex-direction: column;
}
.waterfall-item {
background: #fff;
border-radius: 10rpx;
overflow: hidden;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
.item-title {
display: block;
padding: 15rpx;
font-size: 28rpx;
font-weight: bold;
}
.item-desc {
display: block;
padding: 0 15rpx 15rpx;
font-size: 24rpx;
color: #666;
}
</style>
使用示例
<template>
<view>
<Waterfall
:list="goodsList"
:column-count="columnCount"
keyName="goodsId"
imageKey="cover"
nameKey="goodsName"
textKey="description"
gap="20rpx"
item-margin-bottom="20rpx"
>
<!-- 自定义项目内容插槽 -->
<template #item="{ item }">
<image
:src="item.cover"
mode="widthFix"
style="width: 100%;"
></image>
<view class="custom-content">
<text class="title">{{ item.goodsName }}</text>
<text class="desc">{{ item.description }}</text>
<view class="tags">
<text v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
</template>
</Waterfall>
<!-- 加载更多指示器 -->
<view class="load-more" v-if="loading">
<text>加载中...</text>
</view>
<!-- 列数切换按钮(示例) -->
<view class="column-switcher">
<button @click="columnCount = 2">2列</button>
<button @click="columnCount = 3">3列</button>
</view>
</view>
</template>
<script>
import Waterfall from '@/components/Waterfall.vue'
export default {
components: {
Waterfall
},
data() {
return {
goodsList: [],
loading: false,
columnCount: 2
}
},
onLoad() {
this.loadGoodsList()
},
onReachBottom() {
this.loadMore()
},
methods: {
async loadGoodsList() {
this.loading = true
// 模拟API调用
const res = await this.$api.getGoodsList()
this.goodsList = res.data
this.loading = false
},
async loadMore() {
if (this.loading) return
this.loading = true
// 模拟加载更多数据
const res = await this.$api.getGoodsList({ page: this.page + 1 })
this.goodsList = [...this.goodsList, ...res.data]
this.loading = false
}
}
}
</script>
<style scoped>
.column-switcher {
position: fixed;
bottom: 30rpx;
right: 30rpx;
}
.custom-content {
padding: 20rpx;
}
.title {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.desc {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.tags {
display: flex;
flex-wrap: wrap;
}
.tag {
background: #f0f0f0;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
margin-right: 10rpx;
margin-bottom: 10rpx;
}
.load-more {
text-align: center;
padding: 30rpx;
color: #999;
}
</style>
实现要点说明
-
动态列数:通过columnCount属性控制列数,当列数变化时,会重新分配项目到各列。
-
响应式布局:列宽度根据屏幕宽度和列数动态计算,确保布局适应不同屏幕尺寸。
-
高度计算:项目高度通过图片加载后的宽高比计算得出,并提供预估高度用于初始布局。
-
插槽支持:通过插槽允许用户完全自定义项目内容,增加组件的灵活性。
-
性能考虑:图片加载使用异步方式,避免阻塞主线程。同时,使用$nextTick确保在DOM更新后再重新计算布局。

浙公网安备 33010602011771号