Vue3瀑布流(Waterfall)

简介: 这是一个基于 Vue2 的瀑布流(Waterfall)组件,支持多种自定义属性,如图片数组、列数、间隙、宽度、圆角、背景色及 Spin 加载样式。组件通过计算每张图片的位置实现动态布局,并利用 Vue 的响应式系统自动调整布局。提供了在线预览和详细代码示例,方便集成到项目中。

瀑布流_百度百科

可自定义设置以下属性:

  • 图片数组(images),类型:Array<{name?: string, src: string}>,默认 []

  • 要划分的列数(columnCount),类型:number,默认 3

  • 各列之间的间隙(columnGap),类型:number,单位 px,默认 30

  • 瀑布流区域的总宽度(width),类型:number | string,单位 px,默认 '100%'

  • 瀑布流区域和图片圆角(borderRadius),类型:number,单位 px,默认 8

  • 瀑布流区域背景填充色(backgroundColor),类型:string,默认 '#F2F4F8'

  • Spin 组件属性配置(spinProps),类型:object,默认 {},参考 Spin Props,用于配置图片加载中样式

效果如下图:在线预览

其中引用使用了以下组件和工具函数:

①创建瀑布流组件Waterfall.vue:

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import Spin from '../spin'
import { useResizeObserver } from '../utils'
/*
  宽度固定,图片等比例缩放;使用JS获取每张图片宽度和高度,结合 `relative` 和 `absolute` 定位
  计算每个图片的位置 `top`,`left`,保证每张新的图片都追加在当前高度最小的那列末尾
*/
interface Image {
  name?: string // 图片名称
  src: string // 图片地址
}
interface Props {
  images?: Image[] // 图片数组
  columnCount?: number // 要划分的列数
  columnGap?: number // 各列之间的间隙,单位 px
  width?: string | number // 瀑布流区域的总宽度,单位 px
  borderRadius?: number // 瀑布流区域和图片圆角,单位 px
  backgroundColor?: string // 瀑布流区域背景填充色
  spinProps?: object // Spin 组件属性配置,参考 Spin Props,用于配置图片加载中样式
}
const props = withDefaults(defineProps<Props>(), {
  images: () => [],
  columnCount: 3,
  columnGap: 20,
  width: '100%',
  borderRadius: 8,
  backgroundColor: '#F2F4F8',
  spinProps: () => ({})
})
const waterfallRef = ref()
const waterfallWidth = ref<number>()
const loaded = ref(Array(props.images.length).fill(false)) // 图片是否加载完成
const imageWidth = ref<number>()
const imagesProperty = ref<{ width: number; height: number; top: number; left: number }[]>([])
const preColumnHeight = ref<number[]>(Array(props.columnCount).fill(0)) // 每列的高度
const totalWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  } else {
    return props.width
  }
})
const height = computed(() => {
  return Math.max(...preColumnHeight.value) + props.columnGap
})
const len = computed(() => {
  return props.images.length
})
const flag = ref(0)
watch(
  () => [props.images, props.columnCount, props.columnGap, props.width],
  () => {
    waterfallWidth.value = waterfallRef.value.offsetWidth
    preColumnHeight.value = Array(props.columnCount).fill(0)
    flag.value++
    preloadImages(flag.value)
  },
  {
    deep: true, // 强制转成深层侦听器
    flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
  }
)
onMounted(() => {
  waterfallWidth.value = waterfallRef.value.offsetWidth
  preloadImages(flag.value)
})
function updateWatefall() {
  const currentWidth = waterfallRef.value.offsetWidth
  // 窗口宽度改变时重新计算瀑布流布局
  if (props.images.length && currentWidth !== waterfallWidth.value) {
    waterfallWidth.value = currentWidth
    flag.value++
    preloadImages(flag.value)
  }
}
useResizeObserver(waterfallRef, updateWatefall)
async function preloadImages(symbol: number) {
  // 计算图片宽高和位置(top,left)
  // 计算每列的图片宽度
  imageWidth.value = ((waterfallWidth.value as number) - (props.columnCount + 1) * props.columnGap) / props.columnCount
  imagesProperty.value.splice(0)
  for (let i = 0; i < len.value; i++) {
    if (symbol === flag.value) {
      await loadImage(props.images[i].src, i)
    } else {
      return false
    }
  }
}
function loadImage(url: string, n: number) {
  return new Promise((resolve) => {
    const image = new Image()
    image.src = url
    image.onload = function () {
      // 图片加载完成时执行,此时可通过image.width和image.height获取到图片原始宽高
      const height = image.height / (image.width / (imageWidth.value as number))
      imagesProperty.value[n] = {
        // 存储图片宽高和位置信息
        width: imageWidth.value as number,
        height: height,
        ...getPosition(n, height)
      }
      resolve('load')
    }
  })
}
function getPosition(i: number, height: number) {
  // 获取图片位置信息(top,left)
  if (i < props.columnCount) {
    preColumnHeight.value[i] = props.columnGap + height
    return {
      top: props.columnGap,
      left: ((imageWidth.value as number) + props.columnGap) * i + props.columnGap
    }
  } else {
    const top = Math.min(...preColumnHeight.value)
    let index = 0
    for (let n = 0; n < props.columnCount; n++) {
      if (preColumnHeight.value[n] === top) {
        index = n
        break
      }
    }
    preColumnHeight.value[index] = top + props.columnGap + height
    return {
      top: top + props.columnGap,
      left: ((imageWidth.value as number) + props.columnGap) * index + props.columnGap
    }
  }
}
function onLoaded(index: number) {
  loaded.value[index] = true
}
function getImageName(image: Image) {
  // 从图像地址src中获取图像名称
  if (image) {
    if (image.name) {
      return image.name
    } else {
      const res = image.src.split('?')[0].split('/')
      return res[res.length - 1]
    }
  }
}
</script>
<template>
  <div
    ref="waterfallRef"
    class="m-waterfall"
    :style="`--border-radius: ${borderRadius}px; background-color: ${backgroundColor}; width: ${totalWidth}; height: ${height}px;`"
  >
    <Spin
      class="waterfall-image"
      :style="`width: ${property.width}px; height: ${property.height}px; top: ${property && property.top}px; left: ${property && property.left}px;`"
      :spinning="!loaded[index]"
      size="small"
      indicator="dynamic-circle"
      v-bind="spinProps"
      v-for="(property, index) in imagesProperty"
      :key="index"
    >
      <img class="u-image" :src="images[index].src" :alt="getImageName(images[index])" @load="onLoaded(index)" />
    </Spin>
  </div>
</template>
<style lang="less" scoped>
.m-waterfall {
  position: relative;
  border-radius: var(--border-radius);
  .waterfall-image {
    position: absolute;
    .u-image {
      width: 100%;
      height: 100%;
      border-radius: var(--border-radius);
      display: inline-block;
      vertical-align: bottom;
    }
  }
}
</style>
AI 代码解读

②在要使用的页面引入:

<script setup lang="ts">
import Waterfall from './Waterfall.vue'
import { ref, onBeforeMount, reactive } from 'vue'

const images = ref<any[]>([])
const state = reactive({
  columnCount: 3,
  columnGap: 20,
  backgroundColor: '#e1faeb',
  borderRadius: 12
})
function loadImages() {
  for (let i = 1; i <= 10; i++) {
    images.value.push({
      title: `image-${i}`,
      link: '',
      src: `https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.5/${i}.jpg`
    })
  }
}
onBeforeMount(() => {
  // 组件已完成响应式状态设置,但未创建DOM节点
  loadImages()
})
</script>
<template>
  <div>
    <h1>{
  
  { $route.name }} {
  
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Waterfall :images="images" />
    <h2 class="mt30 mb10">瀑布流配置器</h2>
    <Row :gutter="24">
      <Col :span="6">
        <Flex vertical gap="middle">
          columnCount:
          <Slider :min="1" :max="6" v-model:value="state.columnCount" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical gap="middle">
          columnGap:
          <Slider :min="10" :max="100" v-model:value="state.columnGap" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical gap="middle">
          borderRadius:
          <Slider :min="0" :max="100" v-model:value="state.borderRadius" />
        </Flex>
      </Col>
      <Col :span="6">
        <Flex vertical>
          backgroundColor:
          <Input v-model:value="state.backgroundColor" placeholder="backgroundColor" />
        </Flex>
      </Col>
    </Row>
    <Waterfall
      class="mt30"
      :images="images"
      :column-count="state.columnCount"
      :column-gap="state.columnGap"
      :background-color="state.backgroundColor"
      :border-radius="state.borderRadius"
    />
  </div>
</template>
AI 代码解读
目录
打赏
0
1
1
0
23
分享
相关文章
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
405 5
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
177 17
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
190 6
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
467 0
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
240 2
高效工作流:用Mermaid绘制你的专属流程图;如何在Vue3中导入mermaid绘制流程图
mermaid是一款非常优秀的基于 JavaScript 的图表绘制工具,可渲染 Markdown 启发的文本定义以动态创建和修改图表。非常适合新手学习或者做一些弱交互且自定义要求不高的图表 除了流程图以外,mermaid还支持序列图、类图、状态图、实体关系图等图表可供探索。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解
onMounted作为vue3中最常用的钩子函数之一,能够灵活、随心应手的使用是每个Vue开发者的必修课,同时根据其不同写法的特性,来选择最合适最有利于维护的写法。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Pinia 如何在 Vue 3 项目中进行安装和配置?
Pinia 如何在 Vue 3 项目中进行安装和配置?
313 4
管理数据必备;侦听器watch用法详解,vue2与vue3中watch的变化与差异
一篇文章同时搞定Vue2和Vue3的侦听器,是不是很棒?不要忘了Vue3中多了一个可选项watchEffect噢。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问