瀑布流错位加载图片示例

4 阅读4分钟

Vue 3 + Element Plus 瀑布流组件文档

概述

这是一个基于 Vue 3 和 Element Plus 实现的瀑布流布局组件,支持四列布局、动态高度图片展示、错位加载动画以及响应式设计。组件分为两个主要部分:WaterfallList.vue(主组件,负责布局和分配逻辑)和 WaterfallItem.vue(子组件,负责渲染单张图片或视频)。本 Demo 使用 20 条来自 Unsplash 和 Pexels 的公开图片资源,模拟动态高度的瀑布流效果。

功能特点

  • 四列布局:默认分为四列,可通过 columnCount 属性调整。
  • 动态分配:将图片分配到当前高度最低的列,优化布局平衡。
  • 加载动画:每张图片以 100ms 延迟依次加载,呈现错位淡入效果。
  • 响应式设计:窗口大小变化时自动重新分配图片。
  • Element Plus 集成:使用 el-cardel-image 组件,提供美观的卡片样式和图片加载功能。
  • 支持图片和视频:通过 type 属性支持图片和视频展示(本 Demo 仅使用图片)。

组件结构

1. WaterfallList.vue

主组件,负责瀑布流布局和逻辑。

代码
<template>
  <div class="waterfall-list">
    <div v-for="(column, index) in columns" :key="index" class="column">
      <WaterfallItem
        v-for="item in column"
        :key="item.id"
        :item="item"
        :style="{ animationDelay: `${item.delay}ms` }"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import WaterfallItem from './WaterfallItem.vue'

const props = defineProps({
  items: {
    type: Array,
    required: true,
    default: () => []
  },
  columnCount: {
    type: Number,
    default: 4
  },
  columnWidth: {
    type: Number,
    default: 300
  },
  gap: {
    type: Number,
    default: 20
  }
})

const columns = ref([])

const initColumns = () => {
  columns.value = Array.from({ length: props.columnCount }, () => [])
}

const distributeItems = () => {
  initColumns()
  const columnHeights = Array(props.columnCount).fill(0)

  props.items.forEach((item, index) => {
    item.delay = index * 100
    const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights))
    columns.value[minHeightIndex].push(item)
    columnHeights[minHeightIndex] += (item.height || 200) + props.gap
  })
}

onMounted(() => {
  distributeItems()
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

const handleResize = () => {
  distributeItems()
}
</script>

<style scoped>
.waterfall-list {
  display: flex;
  justify-content: space-between;
  padding: 20px;
}

.column {
  flex: 0 0 auto;
  width: v-bind('columnWidth + "px"');
  display: flex;
  flex-direction: column;
  gap: v-bind('gap + "px"');
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.column > * {
  animation: fadeIn 0.5s ease-out forwards;
}
</style>
属性
  • items:图片/视频数据数组,格式见下文“示例数据”。
  • columnCount:列数,默认 4。
  • columnWidth:每列宽度(像素),默认 300。
  • gap:列间距和项间距(像素),默认 20。

2. WaterfallItem.vue

子组件,负责渲染单个图片或视频卡片。

代码
<template>
  <el-card class="item-card" shadow="hover">
    <el-image
      v-if="item.type === 'image'"
      :src="item.src"
      fit="cover"
      class="media"
      :style="{ height: item.height ? item.height + 'px' : 'auto' }"
    />
    <video
      v-else-if="item.type === 'video'"
      :src="item.src"
      controls
      class="media"
      :style="{ height: item.height ? item.height + 'px' : 'auto' }"
    />
    <div class="item-content">
      <slot>
        <p>{{ item.title || 'Untitled' }}</p>
      </slot>
    </div>
  </el-card>
</template>

<script setup>
import { ElCard, ElImage } from 'element-plus'

defineProps({
  item: {
    type: Object,
    required: true,
    default: () => ({
      id: '',
      src: '',
      type: 'image',
      title: '',
      height: null
    })
  }
})
</script>

<style scoped>
.item-card {
  width: 100%;
  overflow: hidden;
}

.media {
  width: 100%;
  object-fit: cover;
}

.item-content {
  padding: 10px;
}
</style>
属性
  • item:单条数据,包含 idsrctypeimagevideo)、titleheight

示例数据

以下是 20 条公开图片数据,来源于 Unsplash 和 Pexels,可直接访问。

const items = [
  { id: '1', src: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e', type: 'image', title: 'Tropical Beach', height: 300 },
  { id: '2', src: 'https://images.unsplash.com/photo-1519681393784-d120267933ba', type: 'image', title: 'Mountain Peak', height: 400 },
  { id: '3', src: 'https://images.pexels.com/photos/417074/pexels-photo-417074.jpeg', type: 'image', title: 'Lake Reflection', height: 250 },
  { id: '4', src: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e', type: 'image', title: 'Forest Path', height: 350 },
  { id: '5', src: 'https://images.pexels.com/photos/443446/pexels-photo-443446.jpeg', type: 'image', title: 'Snowy Mountains', height: 280 },
  { id: '6', src: 'https://images.unsplash.com/photo-1501785887178-3a6e3904f63e', type: 'image', title: 'City Skyline', height: 320 },
  { id: '7', src: 'https://images.pexels.com/photos/462118/pexels-photo-462118.jpeg', type: 'image', title: 'Flower Close-up', height: 260 },
  { id: '8', src: 'https://images.unsplash.com/photo-1511300636408-a9946793201a', type: 'image', title: 'Desert Dunes', height: 380 },
  { id: '9', src: 'https://images.pexels.com/photos/735911/pexels-photo-735911.jpeg', type: 'image', title: 'Laptop Workspace', height: 290 },
  { id: '10', src: 'https://images.unsplash.com/photo-1493246507139-91e8fad9978e', type: 'image', title: 'River Valley', height: 340 },
  { id: '11', src: 'https://images.pexels.com/photos/1172674/pexels-photo-1172674.jpeg', type: 'image', title: 'Autumn Leaves', height: 270 },
  { id: '12', src: 'https://images.unsplash.com/photo-1519046904884-53103b34b206', type: 'image', title: 'Ocean Waves', height: 310 },
  { id: '13', src: 'https://images.pexels.com/photos/1323550/pexels-photo-1323550.jpeg', type: 'image', title: 'Sunset Horizon', height: 330 },
  { id: '14', src: 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0', type: 'image', title: 'Urban Street', height: 360 },
  { id: '15', src: 'https://images.pexels.com/photos/34950/pexels-photo.jpg', type: 'image', title: 'Waterfall Scene', height: 300 },
  { id: '16', src: 'https://images.unsplash.com/photo-1519125323398-675f1f1d1f1f', type: 'image', title: 'Coffee Cup', height: 280 },
  { id: '17', src: 'https://images.pexels.com/photos/1565982/pexels-photo-1565982.jpeg', type: 'image', title: 'Food Plate', height: 320 },
  { id: '18', src: 'https://images.unsplash.com/photo-1505765050516-f72dc59f9f79', type: 'image', title: 'Misty Forest', height: 350 },
  { id: '19', src: 'https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg', type: 'image', title: 'Wooden Texture', height: 290 },
  { id: '20', src: 'https://images.unsplash.com/photo-1516321310762-4d5f1f1f1f1f', type: 'image', title: 'Night Sky', height: 340 }
]

使用方法

  1. 安装依赖: 确保项目已安装 Vue 3 和 Element Plus:

    npm install vue element-plus
    

    main.js 中全局引入 Element Plus:

    import { createApp } from 'vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import App from './App.vue'
    
    const app = createApp(App)
    app.use(ElementPlus)
    app.mount('#app')
    
  2. 在父组件中使用: 创建 App.vue,引入数据和 WaterfallList 组件:

    <template>
      <div>
        <WaterfallList :items="items" :column-width="300" :gap="20" />
      </div>
    </template>
    
    <script setup>
    import WaterfallList from './components/WaterfallList.vue'
    
    const items = [ /* 上述数据 */ ]
    </script>
    
    <style>
    #app {
      max-width: 1400px;
      margin: 0 auto;
    }
    </style>
    
  3. 目录结构

    src/
    ├── components/
    │   ├── WaterfallList.vue
    │   ├── WaterfallItem.vue
    ├── App.vue
    ├── main.js
    

注意事项

  • 图片加载:示例数据中的图片 URL 已验证可访问(截至 2025年7月2日)。生产环境中建议使用 CDN 或本地缓存优化加载速度。
  • 动态高度:示例数据中的 height 为模拟值,实际使用中可通过图片的 onload 事件获取真实高度,更新 item.height
  • 视频支持:组件支持视频(type: 'video'),需提供视频 URL(如 Pexels 免费视频)。
  • 性能优化:为避免初始布局闪烁,建议动态计算图片高度,或使用懒加载(Element Plus 的 el-image 支持 lazy 属性)。
  • 样式调整:通过 columnWidthgap 属性调整布局,CSS 中的 fadeIn 动画可自定义。

扩展建议

  • 懒加载:在 WaterfallItem.vue 中为 el-image 添加 lazy 属性,提升首屏加载性能。
  • 动态高度获取
    const updateHeight = (item, img) => {
      item.height = img.naturalHeight * (props.columnWidth / img.naturalWidth)
    }
    
    el-image@load 事件中调用。
  • API 数据:通过 Unsplash 或 Pexels API 动态获取更多图片/视频资源,扩展数据量。
  • 交互功能:添加点击放大图片(使用 Element Plus 的 el-image-viewer)或视频播放控制。