文章目录
前言
最近做项目需要实现uni-app、H5实现瀑布流效果封装,网上搜索有很多的例子,但是代码都是不够完整的,下面来封装一个uni-app、H5都能用的代码。在小程序中,一个个item渲染可能出现问题,也通过加锁来解决问题。
一、效果
1、下面看一下实现的效果,我这里的商品图片是正方形是固定大小的,如果你想要图片不同效果,也是可以适配的。
二、使用代码
1、下面是封装的组件如何使用
<TBody
refresher
:data="goodsList"
:is-end="isEnd"
:is-loading="isLoading"
:is-refreshing="isRefreshing"
@refresh="reset"
@lower="fetchGoodsNextPage"
>
<TTMultiColumnList
class="bg-#fafafa goods"
column-gap="16rpx"
:list="[]"
:column-size="2"
@ready="updateColumnOperator"
>
<template #default="{ data, index }">
<view
class="items_content"
>
//这个是你的商品item,自己封装
<TTGoodsCellPure
:key="index"
:obj="data"
arrangement="imageCenter"
@click-item="onClickItem"
/>
</view>
</template>
</TTMultiColumnList>
</TBody>
2、关键是updateColumnOperator方法,需要请求数据的时候把数据放进去渲染。
const goodsListQuery = {
limit: 30,
offset: undefined as string | undefined,
}
const isLoading = ref(false)
const goodsList = ref<Array<any>>([])
const isEnd = ref(false)
const isRefreshing = ref(false)
// 获取商品列表
async function fetchGoodsList(options: { offset?: string; limit?: number } = {}) {
const { offset, limit = goodsListQuery.limit } = options
//接口自己替换自己的
const { data } = await $apis.xxxxxx({
categoryId: categoryId.value === -1 ? undefined : categoryId.value,
keyword: '',
offset,
limit,
})
return { offset: data?.offset, list: data?.list ?? [] }
}
// 获取商品列表
async function fetchGoodsPage() {
if (isLoading.value || isEnd.value)
return
try {
goodsListQuery.offset = undefined
isLoading.value = true
const { list, offset } = await fetchGoodsList({ offset: goodsListQuery.offset })
if (list?.length) {
goodsList.value = list
if (list.length < goodsListQuery.limit)
isEnd.value = true
}
else {
isEnd.value = true
}
goodsListQuery.offset = offset
nextTick(() => {
columnOperator?.reset(list)
})
}
finally {
isLoading.value = false
}
}
//下一页
async function fetchGoodsNextPage() {
if (isLoading.value || isEnd.value)
return
try {
isLoading.value = true
isRefreshing.value = true
const { list, offset } = await fetchGoodsList({ offset: goodsListQuery.offset })
if (list?.length) {
goodsList.value.push(...list)
if (list.length < goodsListQuery.limit)
isEnd.value = true
}
else {
isEnd.value = true
}
goodsListQuery.offset = offset
columnOperator?.append(list)
}
finally {
isRefreshing.value = false
isLoading.value = false
}
}
三、核心代码
1、核心代码TTMultiColumnList代码
<script lang="ts" setup>
import type { Ref } from 'vue'
import { getCurrentInstance, nextTick, onMounted, ref } from 'vue'
import type { ColumnItem, ColumnOperator, ColumnOperatorPredictor, ListItem } from '@/utils/multiColumn'
const props = withDefaults(
defineProps<{
list: Array<ListItem>
columnSize: number
columnGap: string
rowGap: string
}>(),
{
columnSize: 2,
columnGap: 'normal',
rowGap: 'normal',
},
)
const emit = defineEmits<{
(e: 'ready', operator: ColumnOperator): void
}>()
function range(count: number) {
return Array.from({ length: count }, (_, i) => i)
}
function getEmptyColumns(columnSize: number) {
return range(columnSize).map(() => [])
}
let appendColumnDataPromise = Promise.resolve(true)
const columns = ref<Array<Array<ColumnItem>>>(getEmptyColumns(props.columnSize))
const ctx = getCurrentInstance()
const columnRefs: Ref<Array<() => Promise<number>>> = computed(() => columns.value.map((_, i) => () => new Promise((resolve, reject) => {
const className = `.s_${i}_ccList`
// #ifdef H5
const rect = document
.querySelector(className)
?.getBoundingClientRect()
resolve(rect?.height || 0 as number)
// #endif
// #ifndef H5
uni.createSelectorQuery().in(ctx).select(className).boundingClientRect().exec(([rect]) => {
resolve(rect.height as number)
})
// #endif
})))
// 获取高度最小一列的索引
async function getMinHeightColumnIndex(): Promise<number> {
const columnHeights = await Promise.all(columnRefs.value.map(async (getHeight, index) => ({ height: await getHeight(), index })))
return columnHeights.reduce((index, item, i) => {
const height = columnHeights[index].height
const siblingHeight = item.height
return siblingHeight < height ? i : index
}, 0)
}
// 将元素一个一个地插入到高度最小的一列
async function gradientAppendToColumn(startIndex: number, list: Array<ListItem>) {
if (startIndex >= list.length)
return false
const targetColumnIndex = await getMinHeightColumnIndex()
const item = { index: startIndex, data: list[startIndex] }
const targetColumn = columns.value[targetColumnIndex]
if (Array.isArray(targetColumn))
targetColumn.push(item)
else columns.value[targetColumnIndex] = [item]
// render next item
return await new Promise((resolve) => {
nextTick(async () => {
// #ifndef H5
// 解决小程序渲染问题
await new Promise(resolve => nextTick(() => resolve(true)))
// #endif
await gradientAppendToColumn(startIndex + 1, list)
resolve(true)
})
})
}
async function appendColumnDataInQueue(list: Array<ListItem>) {
// 解决小程序渲染问题
const oldAppendColumnDataPromise = appendColumnDataPromise
appendColumnDataPromise = new Promise((resolve) => {
const cb = () => {
appendColumnData(list).then(() => resolve(true)).catch(() => resolve(false))
}
oldAppendColumnDataPromise.then(() => cb()).catch(() => cb())
})
return appendColumnDataPromise
}
async function appendColumnData(list: Array<ListItem>): Promise<boolean> {
return await new Promise((resolve) => {
nextTick(async () => {
await gradientAppendToColumn(0, list)
resolve(true)
})
})
}
// 重置
async function resetColumnData(list?: Array<ListItem>): Promise<void> {
if (list) {
await appendColumnDataInQueue([])
columns.value = getEmptyColumns(props.columnSize)
await appendColumnDataInQueue(list)
}
}
// 移除元素
function removeColumnData(fn: (v: any) => boolean) {
const staled = [] as Array<{ row: number; col: number }>
columns.value.forEach((cols, colIndex) => {
cols.forEach((d, rowIndex) => {
if (fn(d.data))
staled.push({ row: rowIndex, col: colIndex })
})
})
staled.forEach(({ row, col }) => {
columns.value[col].splice(row, 1)
})
}
// 更新元素
function updateColumnData(fn: ColumnOperatorPredictor, data: ListItem) {
let done = false
for (let col = 0; col < columns.value.length; col++) {
if (done)
break
const rows = columns.value[col]
for (let row = 0; row < rows.length; row++) {
if (fn(rows[row].data)) {
rows[row] = { index: rows[row].index, data }
done = true
break
}
}
}
}
onMounted(() => resetColumnData(props.list))
emit('ready', {
append: appendColumnDataInQueue,
reset: resetColumnData,
remove: removeColumnData,
update: updateColumnData,
})
</script>
<template>
<view
:style="{
'display': 'grid',
'grid-template-columns': `repeat(${columns.length}, 1fr)`,
'column-gap': props.columnGap,
'row-gap': props.rowGap,
'padding-left': '18rpx',
'padding-right': '18rpx',
'margin-top': '16rpx',
}"
>
<view
v-for="(rows, colIndex) in columns"
:key="colIndex"
>
<view
:key="`${colIndex}_list`"
:class="`s_${colIndex}_ccList`"
>
<view
v-for="(row, rowIndex) in rows"
:key="`${colIndex}_${rowIndex}`"
>
<slot
:data="row.data"
:index="row.index"
:column-index="colIndex"
:row-index="rowIndex"
/>
</view>
</view>
</view>
</view>
</template>
2、核心代码multiColumn代码
export type ListItem = unknown
export interface ColumnItem {
index: number
data: ListItem
}
export type ColumnOperatorPredictor = (item: ListItem) => boolean
export interface ColumnOperator {
readonly append: (list: Array<ListItem>) => void
readonly remove: (predict: ColumnOperatorPredictor) => void
readonly update: (predict: ColumnOperatorPredictor, data: ListItem) => void
readonly reset: (list?: Array<ListItem>) => void
}
总结
这就是uni-app、H5实现瀑布流效果封装,希望能帮助到你,有什么问题可以私信给我。