瀑布流布局的多种实现方式

892 阅读4分钟

方法一:纯css(column-count 多栏布局)

1732695111449-1732695090446-1732694456725-20241127_160005.gif

<template>
  <div class="container">
    <div class="list">
      <div class="item" v-for="(item, index) in list" :key="index">
        <img :src="item.url" alt="" />
        {{ item.id }}
      </div>
    </div>
    <div class="btn" @click="getList">加载更多</div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      list: [],
      id: 1
    }
  },
  created() {
    this.getList()
  },
  methods: {
    async getList() {
      const imgList = []
      for (let i = 0; i < 10; i++) {
        const { data: res } = await axios.get(
          'https://dog.ceo/api/breed/pembroke/images/random'
        )
        imgList.push({
          id: this.id++,
          url: res.message
        })
      }
      this.list = [...this.list, ...imgList]
    }
  }
}
</script>

<style lang="scss" scoped>
.list {
  width: 800px;
  border: 1px solid #ccc;
  margin: 0 auto;
  column-count: 4; // 列数
  column-gap: 10px; // 列间距

  .item {
    background-color: pink;
    margin-bottom: 10px;
    text-align: center;
    break-inside: avoid; // 避免在列表项内部进行分页,以保持内容的完整性
    img {
      width: 100%;
      display: block;
    }
  }
}
.btn {
  width: 200px;
  height: 40px;
  line-height: 40px;
  border: 1px solid #ccc;
  margin: 20px auto;
  text-align: center;
  cursor: pointer;
  &:hover {
    background-color: #ccc;
  }
}
</style>

缺点

首先布局是从上到下、从左到右的,并不符合传统的从左到右排列。并且是只能数据固定的时候使用,因为如果需要动态加载数据的话,所有数据就会重新排列,导致和上一次渲染的页面是不一样的,如上图所示

注意点

页面可能出现某一列的最后一个元素的内容被自动断开,一部分在当前列尾,一部分在下一列的列头。这时候子元素可以用 break-inside设置为不被截断 avoid来控制,默认值是auto,会被截断

方法二:绝对定位布局

1732693939546-1732693878107-20241127_155038.gif

<template>
  <div class="waterfall-container">
    <div class="list">
      <div class="item" v-for="(item, index) in list" :key="index">
        <img :src="item.url" alt="" @load="imgLoad" @error="imgLoad"/>
        {{ item.id }}
      </div>
    </div>
    <div class="loading" v-if="loader">
      <div class="loader"></div>
    </div>
    <div class="btn" v-else @click="getList">加载更多</div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      list: [],
      id: 1,
      loader: true,
      imgLoadNum: 0
    }
  },
  created() {
    this.getList()
  },
  methods: {
    setDomPosition() {
      // 获取瀑布流容器
      const listDom = document.querySelector('.waterfall-container .list')
      // 获取所有瀑布流子元素
      const itemDoms = document.querySelectorAll('.waterfall-container .item')

      // 创建列数和间距
      const columns = 4
      const margin = 15
      // 创建一个数组,用来存储每一列的高度
      const hrr = []

      // 遍历每个项目,计算并设置其在瀑布流布局中的位置
      Array.from(itemDoms).forEach((item, index) => {
        // 获取当前项目的宽度和高度
        const { offsetWidth, offsetHeight } = item

        // // 如果有图片宽高,则计算出图片的实际高度并赋值,就可以避免图片加载导致的高度变化,也可以直接在行内样式中通过计算属性设置,避免多次计算
        // const img = item.querySelector('img')
        // if (img && this.list[index].height && this.list[index].width) {
        //   img.style.height = (this.list[index].height * offsetWidth) / this.list[index].width
        // }

        // 判断当前项目是否属于第一行
        if (index < columns) {
          // 计算第一行项目的x轴和y轴位置
          const x = (offsetWidth + margin) * index + 'px'
          const y = 0
          // 设置项目的位置
          item.style.transform = `translate(${x}, ${y})`

          // 将当前项目的高度加上边距,添加到高度记录数组中
          hrr.push(offsetHeight + margin)
        } else {
          // 找到当前高度最小的列
          const minH = Math.min(...hrr)
          const i = hrr.indexOf(minH)

          // 计算非第一行项目的x轴和y轴位置
          const x = (offsetWidth + margin) * i + 'px'
          const y = minH + 'px'

          // 设置项目的位置
          item.style.transform = `translate(${x}, ${y})`

          // 更新高度记录数组中对应列的高度
          hrr[i] += offsetHeight + margin
        }

        // 设置项目的不透明度,使其可见
        item.style.opacity = 1
      })

      // 根据最大列高设置列表的高度
      listDom.style.height = Math.max(...hrr) - margin + 'px'
    },
    imgLoad() {
      this.imgLoadNum += 1
      if(this.imgLoadNum === this.list.length) {
        this.loader = false
        this.setDomPosition()
      }
    },
    async getList() {
      this.loader = true
      const imgList = []
      for (let i = 0; i < 10; i++) {
        const { data: res } = await axios.get(
          'https://dog.ceo/api/breed/pembroke/images/random'
        )
        imgList.push({
          id: this.id++,
          url: res.message
        })
      }
      this.list = [...this.list, ...imgList]
    }
  }
}
</script>

<style lang="scss" scoped>
.list {
  width: 800px;
  margin: 0 auto;
  position: relative;
  padding: 10px;

  .item {
    position: absolute;
    width: calc((100% - 45px) / 4); // 父元素宽度减去列与列的间距再除于4得到每一列的宽度
    transform: translate(0, 0);
    opacity: 0;
    transition: all 0.5s; // 添加动画效果
    background-color: pink;
    text-align: center;
    border-radius: 5px;
    img {
      width: 100%;
      display: block;
    }
  }
}
.loading {
  margin-top: 60px;
  margin-bottom: 60px;
  .loader {
    --d: 22px;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    color: $brandColor;
    box-shadow: calc(1 * var(--d)) calc(0 * var(--d)) 0 0,
      calc(0.707 * var(--d)) calc(0.707 * var(--d)) 0 1px,
      calc(0 * var(--d)) calc(1 * var(--d)) 0 2px,
      calc(-0.707 * var(--d)) calc(0.707 * var(--d)) 0 3px,
      calc(-1 * var(--d)) calc(0 * var(--d)) 0 4px,
      calc(-0.707 * var(--d)) calc(-0.707 * var(--d)) 0 5px,
      calc(0 * var(--d)) calc(-1 * var(--d)) 0 6px;
    animation: l27 1s infinite steps(8);
    margin: 0 auto;
  }
  @keyframes l27 {
    100% {
      transform: rotate(1turn);
    }
  }
}
.btn {
  width: 200px;
  height: 40px;
  line-height: 40px;
  border: 1px solid #ccc;
  margin: 60px auto 20px;
  text-align: center;
  cursor: pointer;
  &:hover {
    background-color: #ccc;
  }
}
</style>

注释:如果知道图片的宽高,就不需要根据图片加载的事件来判断图片是否渲染完成,可以获取到数据后直接执行 setDomPosition方法 ,否则就需要使用图片加载事件来判断图片是否渲染完成,完成后在进行计算位置,否则可能出现位置不准确的问题,导致布局错乱

以上两种方式通过动图可以很明显的看出区别,纯css 每次加载都会重新布局,导致顺序每一次都会不一样,而通过 js绝对定位 的方式就可以达到我们想要的效果