前言

我的导航站点项目需要将每一个二级分类分成一块一块的布局,如下图所示

image-cgsd.png

不同的瀑布流布局实现方案

参考 https://juejin.cn/post/7014650146000470053#heading-5

由于每一块的高度是根据内容量动态变化的,传统的css解决方案无法处理,而且也不够灵活,所以选择使用js实现

代码实现

思路:封装一个vue钩子函数,通过传入分类模块的容器div,获取每一个模块进行计算绝对定位位置,使得每一个分类排列整齐;添加resize监听器,监听视窗 大小变化实时调整瀑布流;添加防抖函数,降低视窗大小变化时瀑布流调整的频率,减 少css重排;

瀑布流算法如下

  1. 容器宽度、间距宽度、元素个数固定,元素宽度自适应

// 分类模块的瀑布流布局
// 容器宽度、间距宽度、元素个数固定,元素宽度自适应
import { onBeforeUnmount, onMounted } from 'vue';

/**
 *
 * @param container 瀑布流容器
 * @param gapWidth 元素间隙宽度
 * @param columnCount 每一行元素个数
 */

function useWaterfall(originalContainer: HTMLElement, gapWidth: number) {
  let container: HTMLElement = originalContainer;
  let itemList: HTMLElement[];
  let columnCount: number;

  let timer: number = null!;

  const setContainer = (newContainer: HTMLElement) => {
    container = newContainer;
  };

  // 计算列数和间隙宽度
  const calc = () => {
    const gapCount = columnCount - 1;

    const leftSpace = container.clientWidth - gapWidth * gapCount;
    const columnWith = leftSpace / columnCount;

    return {
      columnWith,
    };
  };

  // 设置瀑布流元素的位置
  const setPosition = () => {
    if (!container) return;
    itemList = Array.from(container.children) as HTMLElement[];
    container.style.position = 'relative';
    const { columnWith } = calc();

    const columnHeight = new Array(columnCount).fill(0);

    // 遍历每一个元素,使其填充到最短列的下面
    for (let i = 0; i < itemList.length; i += 1) {
      const item: HTMLElement = itemList[i];

      item.style.width = `${columnWith}px`;

      // 确定子分类模块与顶部的距离
      const top = Math.min(...columnHeight);
      item.style.top = `${top}px`;

      // 重新设置当前这一列的高度
      const index = columnHeight.indexOf(top);
      columnHeight[index] += item.clientHeight + gapWidth;

      // 确定子分类模块与左边的距离
      const left = index * gapWidth + index * columnWith;
      item.style.left = `${left}px`;

      item.style.position = 'absolute';
    }

    // 设置子分类模块容器的整体高度
    const height = Math.max(...columnHeight);
    container.style.height = `${height}px`;
  };

  // 根据页面宽度设置展示多少列
  const setColumnCount = () => {
    const screenWidth = window.innerWidth;
    if (screenWidth < 380) {
      columnCount = 1;
    } else if (screenWidth < 768) {
      columnCount = 2;
    } else if (screenWidth < 1200) {
      columnCount = 3;
    } else if (screenWidth < 1600) {
      columnCount = 4;
    } else if (screenWidth < 1920) {
      columnCount = 5;
    } else if (screenWidth < 2560) {
      columnCount = 6;
    } else {
      columnCount = 7;
    }
  };

  // 监听页面缩放
  const handleResize = () => {
    setColumnCount();
    if (timer) {
      clearTimeout(timer);
    }
    setTimeout(setPosition, 300);
  };

  window.addEventListener('resize', handleResize);

  onMounted(() => {
    setColumnCount();
  });

  onBeforeUnmount(() => {
    window.removeEventListener('resize', handleResize);
    clearTimeout(timer);
    timer = null!;
  });

  return {
    setContainer,
    setPosition,
  };
}

export default useWaterfall;
  1. 容器宽度和元素宽度固定,间距自适应

// 分类模块的瀑布流布局
// 容器宽度和元素宽度固定,间距自适应
import { onBeforeUnmount } from 'vue';

/**
 *
 * @param container 瀑布流容器
 * @param width 每一个瀑布流元素的宽度
 */

function useWaterfall(originalContainer: HTMLElement, width: number) {
  let container: HTMLElement = originalContainer;
  let itemList: HTMLElement[];

  let timer: number = null!;

  const setContainer = (newContainer: HTMLElement) => {
    container = newContainer;
  };

  // 计算列数和间隙宽度
  const calc = () => {
    const columns = Math.floor(container.clientWidth / width);

    const gapCount = columns + 1;
    const leftSpace = container.clientWidth % width;
    const gapWidth = leftSpace / gapCount;

    return {
      columns,
      gapWidth,
    };
  };

  // 设置瀑布流元素的位置
  const setPosition = () => {
    console.log('container', container);
    if (!container) return;
    itemList = Array.from(container.children) as HTMLElement[];
    container.style.position = 'relative';
    const { columns, gapWidth } = calc();

    const columnHeight = new Array(columns).fill(0);

    // 遍历每一个元素,使其填充到最短列的下面
    for (let i = 0; i < itemList.length; i += 1) {
      const item: HTMLElement = itemList[i];
      console.log('item', item);

      item.style.width = `${width}px`;

      // 确定子分类模块与顶部的距离
      const top = Math.min(...columnHeight);
      item.style.top = `${top}px`;

      // 重新设置当前这一列的高度
      const index = columnHeight.indexOf(top);
      columnHeight[index] += item.clientHeight + gapWidth;

      // 确定子分类模块与左边的距离
      const left = (index + 1) * gapWidth + index * width;
      item.style.left = `${left}px`;

      item.style.position = 'absolute';
    }

    // 设置子分类模块容器的整体高度
    const height = Math.max(...columnHeight);
    container.style.height = `${height}px`;
  };

  // 监听页面缩放
  const handleResize = () => {
    if (timer) {
      clearTimeout(timer);
    }
    setTimeout(setPosition, 300);
  };

  window.addEventListener('resize', handleResize);

  onBeforeUnmount(() => {
    window.removeEventListener('resize', handleResize);
    clearTimeout(timer);
    timer = null!;
  });

  return {
    setContainer,
    setPosition,
  };
}

export default useWaterfall;

使用

<script lang="ts" setup>

  import useWaterfall from '@/hooks/waterfall';

  const instance = getCurrentInstance();

  const { setPosition, setContainer } = useWaterfall(
    instance?.proxy?.$refs.waterfallContainerRef as HTMLElement,
    20
  );

  // 使用
  setContainer(
        instance?.proxy?.$refs.waterfallContainerRef as HTMLElement
      );
  setPosition();
</script>