uni-app、H5实现瀑布流效果封装,列可以自定义



前言

最近做项目需要实现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实现瀑布流效果封装,希望能帮助到你,有什么问题可以私信给我。

猜你喜欢

转载自blog.csdn.net/smileKH/article/details/132110321
今日推荐