vue3实现瀑布流布局组件

news2025/1/15 13:21:46

先看效果图
在这里插入图片描述

直接上代码
utils.js

// 用于模拟接口请求
export const getRemoteData = (data = '获取数据', time = 2000) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`模拟获取接口数据`, data)
            resolve(data)
        }, time)
    })
}

// 获取数组随机项
export const getRandomElement = (arr) => {
    var randomIndex = Math.floor(Math.random() * arr.length);
    return arr[randomIndex];
}

// 指定范围随机数
export const getRandomNumber = (min, max) => {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

// 节流
export const throttle = (fn, time) => {
    let timer = null
    return (...args) => {
        if (!timer) {
            timer = setTimeout(() => {
                timer = null
                fn.apply(this, args)
            }, time)
        }
    }
}
// 防抖
export const debounce = (fn, time) => {
    let timer = null
    return (...args) => {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, time)
    }
}

data.js 模拟后台返回的数据

import { getRandomElement, getRandomNumber } from "./utils.js"

const colorList = ['red', 'blue', 'green', 'pink', 'yellow', 'orange', 'purple', 'brown', 'gray', 'skyblue']

export const createList = (pageSize) => {
    let list = Array.from({ length: pageSize }, (v, i) => i)
    return list.map(x => {
        return {
            background: getRandomElement(colorList),
            width: getRandomNumber(200, 600),
            height: getRandomNumber(400, 700),
            x: 0,
            y: 0
        }
    })
}

瀑布流布局组件waterfall.vue

<template>
  <div class="waterfall-container" ref="containerRef" @scroll="handleScroll">
    <div class="waterfall-list">
      <div
        class="waterfall-item"
        v-for="(item, index) in resultList"
        :key="index"
        :style="{
          width: `${item.width}px`,
          height: `${item.height}px`,
          transform: `translate3d(${item.x}px, ${item.y}px, 0)`,
        }"
      >
        <slot name="item" v-bind="item"></slot>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, computed, nextTick, onUnmounted } from "vue";
import { createList } from "@/common/data.js";
import { getRemoteData, throttle, debounce } from "@/common/utils.js";
const props = defineProps({
  // 间距
  gap: {
    type: Number,
    default: 10,
  },
  // 列数
  columns: {
    type: Number,
    default: 3,
  },
  // 距离底部
  bottom: {
    type: Number,
    default: 0,
  },
  // 分页大小
  pageSize: {
    type: Number,
    default: 10,
  },
});

// 容器ref
const containerRef = ref(null);

// 卡片宽度
const cardWidth = ref(0);

// 列高度
const columnHeight = ref(new Array(props.columns).fill(0));

// 数据list
const resultList = ref([]);

// 当前页码
const pageNum = ref(1);

// 加载状态
const loading = ref(false);

// 计算最小列高度及其下标
const minColumn = computed(() => {
  let minIndex = -1,
    minHeight = Infinity;

  columnHeight.value.forEach((item, index) => {
    if (item < minHeight) {
      minHeight = item;
      minIndex = index;
    }
  });

  return {
    minIndex,
    minHeight,
  };
});

// 获取接口数据
const getData = async () => {
  loading.value = true;
  const list = createList(props.pageSize);
  const resList = await getRemoteData(list, 300).finally(
    () => (loading.value = false)
  );
  pageNum.value++;
  resultList.value = [...resultList.value, ...getList(resList)];
};

// 滚动到底部获取新一页数据-节流
const handleScroll = throttle(() => {
  const { scrollTop, clientHeight, scrollHeight } = containerRef.value;
  const bottom = scrollHeight - clientHeight - scrollTop;
  if (bottom <= props.bottom) {
    !loading.value && getData();
  }
});

// 拼装数据结构
const getList = (list) => {
  return list.map((x, index) => {
    const cardHeight = Math.floor((x.height * cardWidth.value) / x.width);
    const { minIndex, minHeight } = minColumn.value;
    const isInit = index < props.columns && resultList.length <= props.pageSize;
    if (isInit) {
      columnHeight.value[index] = cardHeight + props.gap;
    } else {
      columnHeight.value[minIndex] += cardHeight + props.gap;
    }

    return {
      width: cardWidth.value,
      height: cardHeight,
      x: isInit
        ? index % props.columns !== 0
          ? index * (cardWidth.value + props.gap)
          : 0
        : minIndex % props.columns !== 0
        ? minIndex * (cardWidth.value + props.gap)
        : 0,
      y: isInit ? 0 : minHeight,
      background: x.background,
    };
  });
};

// 监听元素
const resizeObserver = new ResizeObserver(() => {
  handleResize();
});

// 重置计算宽度以及位置
const handleResize = debounce(() => {
  const containerWidth = containerRef.value.clientWidth;
  cardWidth.value =
    (containerWidth - props.gap * (props.columns - 1)) / props.columns;
  columnHeight.value = new Array(props.columns).fill(0);
  resultList.value = getList(resultList.value);
});

const init = () => {
  if (containerRef.value) {
    const containerWidth = containerRef.value.clientWidth;
    cardWidth.value =
      (containerWidth - props.gap * (props.columns - 1)) / props.columns;
    getData();
    resizeObserver.observe(containerRef.value);
  }
};

onMounted(() => {
  init();
});
// 取消监听
onUnmounted(() => {
  containerRef.value && resizeObserver.unobserve(containerRef.value);
});
</script>

<style lang="scss">
.waterfall {
  &-container {
    width: 100%;
    height: 100%;
    overflow-y: scroll;
    overflow-x: hidden;
  }

  &-list {
    width: 100%;
    position: relative;
  }
  &-item {
    position: absolute;
    left: 0;
    top: 0;
    box-sizing: border-box;
    transition: all 0.3s;
  }
}
</style>

使用该组件(这里columns写死了3列)

<template>
  <div class="container">
    <WaterFall :columns="3" :gap="10">
      <template #item="{ background }">
        <div class="card-box" :style="{ background }"></div>
      </template>
    </WaterFall>
  </div>
</template>

<script setup>
import WaterFall from "@/components/waterfall.vue";
</script>

<style scoped lang="scss">
.container {
  width: 700px;  /* 一般业务场景不是固定宽度 */
  height: 800px;
  border: 2px solid #000;
  margin-top: 10px;
  margin-left: auto;
}
.card-box {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 4px;
}
</style>

若要响应式调整列数,可参考以下代码


const fContainerRef = ref(null);
const columns = ref(3);
const fContainerObserver = new ResizeObserver((entries) => {
  changeColumn(entries[0].target.clientWidth);
});

// 根据宽度,改变columns列数
const changeColumn = (width) => {
  if (width > 1200) {
    columns.value = 5;
  } else if (width >= 768 && width < 1200) {
    columns.value = 4;
  } else if (width >= 520 && width < 768) {
    columns.value = 3;
  } else {
    columns.value = 2;
  }
};

onMounted(() => {
  fContainerRef.value && fContainerObserver.observe(fContainerRef.value);
});

onUnmounted(() => {
  fContainerRef.value && fContainerObserver.unobserve(fContainerRef.value);
});

瀑布流布局组件监听columns变化

watch(
  () => props.columns,
  () => {
    handleResize();
  }
);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1459083.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

npm ERR! code CERT_HAS_EXPIRED:解决证书过期问题

转载&#xff1a;npm ERR! code CERT_HAS_EXPIRED&#xff1a;解决证书过期问题_npm err! code cert_has_expired npm err! errno cert-CSDN博客 npm config set registry http://registry.cnpmjs.org npm config set registry http://registry.npm.taobao.org

【洛谷题解】P1303 A*B Problem

题目链接&#xff1a;A*B Problem - 洛谷 题目难度&#xff1a;普及- 涉及知识点&#xff1a;高精度 题意&#xff1a; 分析&#xff1a;直接套用模版即可 AC代码&#xff1a; #include<bits/stdc.h> using namespace std; char n[1000000],m[1000000]; int a[1000…

《C++ Primer Plus》《4、复合类型》

文章目录 前言&#xff1a;1 数组1.1数组的初始化规则1.2 C11的数组初始化方法 2 字符串2.1 拼接字符串常量2.2在数组中使用字符串2.3 字符串输入2.4 每次读取一行字符串输入2.5 混合输入字符串和数字 3 string类简介3.1 C11字符串初始化3.2 赋值、拼接、附加3.3 string类的其他…

神经网络基础——激活函数的选择、参数初始化

一、神经网络 1、神经网络 人工神经网络&#xff08;Artificial Neural Network&#xff0c;即ANN&#xff09;也简称为神经网络&#xff08;NN&#xff09;是一种模仿生物神经网络结构 和功能的计算模型。 2、基本部分 输入层&#xff1a;输入 x 输出层&#xff1a;输出 y 隐…

计算机组成原理----计算机系统

目录 1.计算机的硬件和软件 2.硬件的发展 3.软件的发展 4.计算机硬件的基本组成 &#xff08;1&#xff09;早期冯诺依曼机的结构 &#xff08;2&#xff09;现代计算机的结构 5.各硬件的工作原理 &#xff08;1&#xff09;主存储器 &#xff08;2&#xff09;运算器…

01 Qt自定义风格控件的基本原则

目录 1.继承原生控件 2.组合原生控件 3.仿写原生控件 PS:后续将继续分享开发实践中各类自定义控件的方法、思路以及组件库 1.继承原生控件 关键字&#xff1a;继承、paintEvent 这里想说的是&#xff0c;Qt的Gui框架在封装原生控件的同时&#xff0c; 也为开发者提供了各…

opencv鼠标操作与响应

//鼠标事件 Point sp(-1, -1); Point ep(-1, -1); Mat temp; static void on_draw(int event, int x, int y, int flags, void *userdata) {Mat image *((Mat*)userdata);if (event EVENT_LBUTTONDOWN) {sp.x x;sp.y y;std::cout << "start point:"<<…

【面试题】谈谈MySQL的索引

索引是啥 可以把Mysql的索引看做是一本书的目录&#xff0c;当你需要快速查找某个章节在哪的时候&#xff0c;就可以利用目录&#xff0c;快速的得到某个章节的具体的页码。Mysql的索引就是为了提高查询的速度&#xff0c;但是降低了增删改的操作效率&#xff0c;也提高了空间…

HarmonyOS—@Observed装饰器和@ObjectLink嵌套类对象属性变化

Observed装饰器和ObjectLink装饰器&#xff1a;嵌套类对象属性变化 概述 ObjectLink和Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步&#xff1a; 被Observed装饰的类&#xff0c;可以被观察到属性的变化&#xff1b;子组件中ObjectLink装饰器装饰的状…

两分钟秒懂Android控件设置

导言 本文从整体到局部,从常用布局(layout)到控件的设置,层层递进,一定要从前往后依次看 目录 1.常用布局方式 1.1 ConstraintLayout 约束布局 1.2 LinearLayout 线性布局 1.3 TableLayout 表格布局 1.4 GridLayout 瀑布布局 2.关于控件属性整合 2.1 放置位置 2.2 如…

三相逆变器拓扑结构的分析及应用场景

三相逆变器是电力电子领域中广泛应用的一种设备&#xff0c;用于将直流电转换为交流电。不同的拓扑结构在性能、成本、复杂度和应用领域上存在差异。小编将在本文中将进一步拓展对三相逆变器拓扑结构的分析&#xff0c;并探讨其适用的应用场景。 1. 单桥逆变器 优点&#xff…

CAN FD一致性测试:便捷、高效的自动化测试系统

后起之秀——CAN FD&#xff1a;随着各个行业的快速发展&#xff0c;消费者对汽车电子智能化的诉求越来越强烈&#xff0c;这使得整车厂将越来越多的电子控制系统加入到了汽车控制中&#xff0c;且在传统汽车、新能源汽车、ADAS和自动驾驶等汽车领域中也无不催生着更高的需求&a…

【硬核】Log4j2 与 Logback 当初的选型以及在当前云原生环境下的反思与展望

个人创作公约&#xff1a;本人声明创作的所有文章皆为自己原创&#xff0c;如果有参考任何文章的地方&#xff0c;会标注出来&#xff0c;如果有疏漏&#xff0c;欢迎大家批判。如果大家发现网上有抄袭本文章的&#xff0c;欢迎举报&#xff0c;并且积极向这个 github 仓库 提交…

革命性进展!OpenAI推出全新视频生成模型Sora,开启视频创作新纪元!

OpenAI发布文生视频模型Sora——视频模型的奇点或许来临&#xff01;&#xff01; 初七啦&#xff0c;得开始工作了&#xff0c;没想到第一天就这么劲爆&#xff01; 今天OpenAI迎来重大更新——发布视频模型Sora&#xff01;&#xff01; 官网Sora (openai.com) 说实话有点…

Laravel Reverb 强势降临:全新风潮,引领未来开发潮流!

终于有官方 Websocket package 了&#xff01;&#xff01;&#xff01; Laravel Reverb &#xff01;&#xff01;&#xff01; reverb.laravel.com/ Laravel Reverb的引入使得Laravel框架在实时通信方面更加强大和灵活&#xff0c;为开发者提供了更多的选择和可能性。通过利…

C++结合Lambda表达式在函数内部实现递归

529. 扫雷游戏 已解答 中等 相关标签 相关企业 让我们一起来玩扫雷游戏&#xff01; 给你一个大小为 m x n 二维字符矩阵 board &#xff0c;表示扫雷游戏的盘面&#xff0c;其中&#xff1a; M 代表一个 未挖出的 地雷&#xff0c;E 代表一个 未挖出的 空方块&#xff…

01_02_mysql06_视图-存储过程-函数

视图 使用 视图一方面可以帮我们使用表的一部分而不是所有的表&#xff0c;另一方面也可以针对不同的用户制定不同的查询视图。比如&#xff0c;针对一个公司的销售人员&#xff0c;我们只想给他看部分数据&#xff0c;而某些特殊的数据&#xff0c;比如采购的价格&#xff0…

Rabbitmq入门与应用(三)-RabbitMQ开发流程

RabbitMQ开发流程 引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId> </dependency>配置MQ 最简配置 spring:rabbitmq:host: mq的安装机器ipport: 5672username: ad…

手撕C语言习题

定义一个表示公交线路的结构体&#xff0c;要求有线路名称(例如 616)&#xff0c;起始站&#xff0c;终点站&#xff0c;里程等成员&#xff0c; 定义结构体数组&#xff0c;用来存储多条条公交线路信息&#xff0c;要求能够输出从指定起始站发车的所以公交线路信息。 2、定义…

C++11---(3)

目录 一、可变参数模板 1.1、可变参数模板的概念 1.2、可变参数模板的定义方式 1.3、如何获取可变参数 二、lambda表达式 2.1、Lamabda表达式定义 2.2、为什么有Lambda 2.3、Lambda表达式的用法 2.4、函数对象与lambda表达式 三、包装器 3.1、function 3.2、bind …