手撕 Vue3 瀑布流列表组件

1,480 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

页面展示列表,列表项的高度不统一的情况下(如图片高度不同,文字行数不同等),瀑布流布局就显得非常优雅。 文章手撕瀑布流布局列表,不使用插件,纯 js 封装

2371660117529_.pic.jpg

瀑布流封装思路

  1. 计算该列表垂直列的数量(固定列表项的宽度)
  2. 初始化每列的高度,初始值都是 0,存放在数组中。如 4 列则数组为 [0,0,0,0]
  3. 各项使用 css 绝对定位,初始位置为 left:0; top:0
  4. 找出总列高最小的列,将下一项放入该列,累加该列列高。如 [200,0,0,0]
  5. 遍历各项重复上一步
  6. 最终以列高最大值设置列表容器高度
  7. 浏览器窗口变化,重绘瀑布流布局

核心函数

绘制瀑布流为核心函数,获取列的数量初始化列高变量flowHeight 数组;计算每列加起来的总宽度,赋值给容器以居中。

重点在遍历每个 item 绘制,绝对定位到每列总列高的最小值的列后面,然后将item的高度累加到该列。最后将列高最大值设为容器总高度。

/// 绘制瀑布流
const flowDraw = () => {
  if (!content.value) return;
  // 初始化列高
  const columnCount = getColumnCount();
  flowHeight.length = columnCount;
  for (let i = 0; i < columnCount; i++) {
    flowHeight[i] = 0;
  }
  // 设置容器宽(居中布局)
  const itemW = props.columnWidth + props.columnGap;
  content.value.style.width = itemW * columnCount - props.columnGap + 'px';

  // 绘制 item 位置
  const doms = content.value.querySelectorAll('.WaterfallItem');
  doms.forEach((dom: any) => {
    const minIdx = getMinIndex(flowHeight);
    dom.style.left = `${minIdx * itemW}px`;
    dom.style.top = `${flowHeight[minIdx]}px`;
    // 累加列高度
    flowHeight[minIdx] += dom.offsetHeight;
  });
  // 设置容器高
  content.value.style.height = Math.max(...flowHeight) + 'px';
};

组件完整代码

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, withDefaults } from 'vue';

const props = withDefaults(
  defineProps<{
    list: any[];
    columnWidth?: number; // 列宽
    columnGap?: number; // 列间距
  }>(),
  {
    columnWidth: 240,
    columnGap: 24,
  }
);

const wrapper = ref<HTMLElement | null>(null);
const content = ref<HTMLElement | null>(null);
// waterfall flow 瀑布流列高 [0,0,0,...]
const flowHeight: number[] = [];

/// 绘制瀑布流
const flowDraw = () => {
  if (!content.value) return;
  // 初始化列高
  const columnCount = getColumnCount();
  flowHeight.length = columnCount;
  for (let i = 0; i < columnCount; i++) {
    flowHeight[i] = 0;
  }
  // 设置容器宽(居中布局)
  const itemW = props.columnWidth + props.columnGap;
  content.value.style.width = itemW * columnCount - props.columnGap + 'px';

  // 绘制 item 位置
  const doms = content.value.querySelectorAll('.WaterfallItem');
  doms.forEach((dom: any) => {
    const minIdx = getMinIndex(flowHeight);
    dom.style.left = `${minIdx * itemW}px`;
    dom.style.top = `${flowHeight[minIdx]}px`;
    flowHeight[minIdx] += dom.offsetHeight;
  });
  // 设置容器高
  content.value.style.height = Math.max(...flowHeight) + 'px';
};

/// 获取列的数量
const getColumnCount = (): number => {
  if (!wrapper.value) return 0;
  const itemW = props.columnWidth + props.columnGap;
  const num = (wrapper.value.offsetWidth + props.columnGap) / itemW;
  return Math.min(Math.floor(num), props.list.length);
};

/// 获取最小值的索引 index
const getMinIndex = (list: number[]) => {
  const min = Math.min(...list);
  return list.indexOf(min);
};

/// 监听窗口变化重绘瀑布流布局
let timer: number | null = null;
const onResize = () => {
  if (timer) {
    clearTimeout(timer);
    timer = null;
  }
  timer = setTimeout(() => {
    flowDraw();
  }, 300);
};

onMounted(() => {
  flowDraw();
  window.addEventListener('resize', onResize);
});

onBeforeUnmount(() => {
  window.removeEventListener('resize', onResize)
});
</script>

<template>
  <div class="WaterfallList" ref="wrapper">
    <div class="WaterfallContent" ref="content">
      <div class="WaterfallItem" v-for="(item, index) in list" :key="index">
        <slot name="item" :index="index" :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<style lang="sass" scoped>
.WaterfallContent
  margin-left: auto
  margin-right: auto
  position: relative

.WaterfallItem
  padding-bottom: 24px
  width: v-bind("columnWidth + 'px'")
  position: absolute
  top: 0
  left: 0
</style>

在 vue 中使用

由于瀑布流需要计算元素的高度进行绘制,图片的加载是异步的,无法第一时间计算,需要为其设置高度,使用了具名插槽 item <template #item="{ item }">,在这里写 item 的 html

<template>
  <WaterfallList :list="list">
    <template #item="{ item }">
      <div class="item">
        <img :src="item.url" :style="{ height: item.height + 'px' }" />
        <p>{{ item.title }}</p>
      </div>
    </template>
  </WaterfallList>
</template>

<script setup lang="ts">
import { ref } from 'vue';

// 图片设置高度
const list = ref([
  { url: 'http://img1.jpg', height: 200, title: '图片1' },
  { url: 'http://img1.jpg', height: 400, title: '图片2' },
  { url: 'http://img1.jpg', height: 320, title: '图片3' },
  { url: 'http://img1.jpg', height: 660, title: '图片4' },
]);
</script>

结语

博观而约取,厚积而薄发。

转载声明: 请注明作者,注明原文链接,有疑问致邮 kingwyh1993@163.com