uniapp 全端瀑布流布局简单实现方式

介绍

瀑布流,又称流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。

瀑布流有很多种实现方式,像抖音、小红书的PC首页用的是脱离文档流 transform 定位的模式,这种比较复杂,我们不考虑。

本文采用的是flex布局,通过img标签的load事件,在该事件中获取每栏新的总高度,算出总高度最小的那一栏,并把下一个 item 放进该栏,以此类推,实现瀑布流效果。

在子组件中,为了防止图片在加载显示的过程中出现抖动的情况,我使用了 opacity: 0 先隐藏item,在load事件中,再改为 opacity: 1,以达到更好的显示效果。

父组件使用

javascript 复制代码
<template>
  <view class="container">
    <view v-for="colNum in state.columnNum" :key="colNum" class="columnItem">
      <child-item
        v-for="(item, i) in state.columnData[colNum]"
        :key="i"
        :item="item"
        @load="imageLoad"
      />
    </view>
  </view>
  <view class="loading">加载中...</view>
</template>

<script setup>
  import { onLoad, onReachBottom } from '@dcloudio/uni-app'
  import { data } from './data.js'
  import { reactive, getCurrentInstance } from 'vue'
  import childItem from './child-item.vue'
  const { proxy } = getCurrentInstance()
  const state = reactive({
    columnNum: 2,
    minHeightColNum: 1,
    columnData: {},
    totalList: [],
    pages: {
      page: 1,
      pageSize: 10
    }
  })
  onLoad(() => {
    setDefaultData()
    getData()
  })

  // 模拟获取数据
  function getData() {
    if (state.pages.page == 1) setDefaultData()
    return new Promise((resolve) => {
      setTimeout(() => {
        state.totalList.push(...data)
        loadNextItem()
        resolve()
      }, 500)
    })
  }

  /** @加载更多 **/
  onReachBottom(() => {
    state.pages.page++
    getData()
  })

  /** @图片加载成功 **/
  function imageLoad() {
    getMinHeightColumnNum().then(() => loadNextItem())
  }

  /** @加载下一个元素 **/
  function loadNextItem() {
    const totalLength = Object.values(state.columnData).flat().length
    if (totalLength == state.totalList.length) return
    state.columnData[state.minHeightColNum].push(state.totalList[totalLength])
  }

  /** @获取最小列数 **/
  async function getMinHeightColumnNum() {
    let minHeight = Infinity // 初始化为无限大
    return new Promise((resolve) => {
      $('.columnItem', proxy).then((columItems) => {
        columItems.forEach((item, index) => {
          if (item.height < minHeight) {
            minHeight = item.height
            state.minHeightColNum = index + 1
          }
        })
        resolve()
      })
    })
  }

  /** @设置默认数据 **/
  function setDefaultData() {
    state.totalList = []
    Array(state.columnNum)
      .fill(null)
      .map((item, i) => {
        state.columnData[i + 1] = []
      })
  }

  /** @封装[jvideo](https://v.ixigua.com/iNQLjWJS/)查询DOM **/
  function $(selecter, proxy) {
    return new Promise((resolve) => {
      uni
        .createSelectorQuery()
        .in(proxy)
        .selectAll(selecter)
        .boundingClientRect()
        .exec((res) => {
          resolve(res[0])
        })
    })
  }
</script>

<style lang="scss" scoped>
  .container {
    display: flex;
    padding: 20rpx;
    .columnItem {
      width: calc(100% / v-bind('state.columnNum'));
      height: fit-content;
      & + .columnItem {
        margin-left: 20rpx;
      }
    }
  }
  .loading {
    padding: 20rpx;
    text-align: center;
    font-size: 28rpx;
    color: #aaa;
  }
</style>

子组件 child-item.vue

javascript 复制代码
<template>
  <view class="child-item" :class="{ isShow }">
    <img class="pic" :src="item.imgUrl" mode="widthFix" @load="load" @error="load" />
    <view class="info">
      <text class="title">{{ item.title }}</text>
      <text class="desc">{{ item.desc }}</text>
    </view>
  </view>
</template>

<script setup>
  import { ref } from 'vue'
  defineProps({
    item: {
      type: Object,
      default: () => ({})
    }
  })

  const isShow = ref(false)

  const emit = defineEmits(['load', 'error'])

  function load(e) {
    isShow.value = true
    emit('load', e)
  }
</script>

<style lang="scss" scoped>
  .child-item {
    font-size: 0;
    border-radius: 10rpx;
    margin-bottom: 20rpx;
    overflow: hidden;
    transition: all ease 0.35s;
    opacity: 0;
    .pic {
      width: 100%;
    }
    .info {
      background: #fff;
      padding: 15rpx;
      .title,
      .desc {
        display: block;
        font-size: 28rpx;
        word-break: break-all; // 允许单词内自动换行,如果一个单词很长的话
        text-overflow: ellipsis; // 溢出用省略号显示
        overflow: hidden; // 超出的文本隐藏
        display: -webkit-box; // 作为弹性伸缩盒子模型显示
        -webkit-line-clamp: 2; // 显示的行
        -webkit-box-orient: vertical; // 设置伸缩盒子的子元素排列方式--从上到下垂直排列
      }
      .desc {
        margin-top: 10rpx;
        color: #666;
        font-size: 26rpx;
      }
    }
    &.isShow {
      opacity: 1;
    }
  }
</style>

数据源 data.js

javascript 复制代码
export const data = [
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679045108_thumb.jpg',
    title: '1自动驾驶汽车对交通和城市规划的未来影响与挑战',
    desc: '分析自动驾驶汽车对未来交通和城市规划的潜在影响,探讨相关挑战。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679044667_thumb.jpg',
    title: '2人工智能与机器学习:颠覆性技术对未来的巨大影响',
    desc: '探讨人工智能和机器学习如何在多个领域引发革命性变革,从工业到医疗,对未来产生深远影响。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679045190_thumb.jpg',
    title: '3消灭传染病:全球卫生领域的挑战与创新',
    desc: '探讨在全球范围内消灭传染病的挑战,突出卫生领域的创新方法。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679044581_thumb.jpg',
    title: '4可持续城市发展:构建环保城市的策略和实践',
    desc: '分析建设可持续城市的战略和实际方法,强调环保、资源利用和城市规划的重要性。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679044562_thumb.jpg',
    title: '5生命科学的新前沿:基因编辑和生物技术的伦理挑战',
    desc: '研究生命科学领域的最新发展,聚焦基因编辑和生物技术的伦理考量,探讨科技前沿的道德挑战。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679044716_thumb.jpg',
    title: '6量子计算与量子技术应用的前沿探索',
    desc: '深入研究量子计算和量子技术的前沿,展示这一领域的创新及其应用前景。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679045057_thumb.jpg',
    title: '7加密货币与区块链:重塑全球金融体系的力量',
    desc: '解析加密货币和区块链技术对金融体系的颠覆作用,重新定义了全球金融交易方式。'
  },
  {
    imgUrl: 'https://www.logosc.cn/uploads/resources/2023/03/17/1679044779_thumb.jpg',
    title: '8气候变化缓解的复杂性:全球视角下的挑战与机遇',
    desc: '探讨应对气候变化的复杂性,强调全球合作,突出应对挑战所蕴含的机遇。'
  }
]
相关推荐
williamdsy2 分钟前
【vue】关于异步函数使用不当导致的template内容完全无法渲染的问题
前端·javascript·vue.js
2402_839708052 分钟前
第十章:作业
开发语言·前端·javascript
诗水人间5 分钟前
前后端分离,解决vue+axios跨域和proxyTable不生效等问题
前端·javascript·vue.js·springboot·springsecurity·跨域·cros
恩爸编程6 分钟前
纯前端js完成游戏吃豆人
前端·javascript·游戏·游戏程序·开源软件·游戏策划·游戏机
粉03216 分钟前
用web前端写出一个高校官网
前端·css
@大迁世界6 分钟前
停止在 React 组件回调中使用箭头函数!
前端·javascript·react.js·前端框架·ecmascript
焦糖酒8 分钟前
JS精进之Hoisting(提升)
开发语言·前端·javascript
像素之间10 分钟前
`URL.createObjectURL(blob)` 和 `URL` 对象的区别
前端·javascript·网络
( •̀∀•́ )92021 分钟前
如何使用 `global.json` 管理(切换) .NET SDK 版本
前端·json·.net
小彭努力中30 分钟前
141. Sprite标签(Canvas作为贴图)
前端·深度学习·3d·webgl·three.js