npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fs-virtual-waterfall

v1.2.0

Published

Vue3 虚拟瀑布流组件

Downloads

40

Readme

fs-virtual-waterfall

Vue3 虚拟瀑布流组件,支持无限滚动加载。

参考

链接 修复了些 bug

版本

  • v1.2.0 - 更新视频文件
  • v1.1.9 - 更新文档
  • v1.1.8 - 修复 bug
  • v1.1.7 - 添加示例视频
  • v1.1.6 - 更新 示例代码
  • v1.1.5 - 更新 示例代码
  • v1.1.4 - 更新 md 文件
  • v1.1.3 - 修复 bug,美化细节
  • v1.1.2 - 添加声明
  • v1.1.1 - 优化界面
  • v1.1.0 - 修复 bug
  • v0.1.5 - 改为接收父组件传入数据,不再自行请求数据,更符合组件化设计
  • v0.1.4 - 新增图片骨架屏加载效果、底部加载指示器、优化内容区域显示和修复卡片底部数据丢失问题
  • v0.1.3 - 新增主题模式切换、卡片样式选择、增强响应式布局、性能优化和错误处理
  • v0.1.2 - 添加 3D 悬浮效果,利用 GSAP 实现鼠标移动时的动态倾斜和缩放
  • v0.1.1 - 优化滚动性能,修复滚动交互问题,提高组件稳定性
  • v0.1.0 - 初始版本

特性

  • 基于 Vue3 和 TypeScript 开发
  • 支持虚拟滚动,高效处理大量数据
  • 自适应列数和间距
  • 支持自定义内容渲染
  • 支持懒加载和无限滚动
  • 错误处理和容器自适应
  • 支持 3D 悬浮效果,鼠标移动时带来视觉交互
  • 支持亮色、暗色和自动主题模式
  • 提供圆角和方形卡片样式选项
  • 增强容错性和边界条件处理
  • 图片加载骨架屏,提供视觉加载反馈
  • 底部加载指示器,提示用户正在加载更多内容
  • 智能卡片高度计算,确保内容完整显示
  • 新增 - 支持父组件传入数据,更好的组件化设计

效果预览

图片 视频

安装

npm install fs-virtual-waterfall
# 或
yarn add fs-virtual-waterfall
# 或
pnpm add fs-virtual-waterfall

使用方法

✨ 直接 CV 大法即可

全局注册

import { createApp } from 'vue'
import App from './App.vue'
import FsVirtualWaterfall from 'fs-virtual-waterfall'
import 'fs-virtual-waterfall/dist/index.css'

const app = createApp(App)
app.use(FsVirtualWaterfall)
app.mount('#app')

新版用法 父组件请求数据

父组件

<template>
  <div class="app">
    <header class="header">
      <div class="logo">
        <svg viewBox="0 0 24 24" width="24" height="24" stroke="#4299e1" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
          <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
          <line x1="3" y1="9" x2="21" y2="9"></line>
          <line x1="9" y1="21" x2="9" y2="9"></line>
        </svg>
        <h1>虚拟瀑布流示例</h1>
      </div>
      <div class="theme-toggle" @click="toggleTheme">
        <svg v-if="theme === 'light'" viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
          <circle cx="12" cy="12" r="5"></circle>
          <line x1="12" y1="1" x2="12" y2="3"></line>
          <line x1="12" y1="21" x2="12" y2="23"></line>
          <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
          <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
          <line x1="1" y1="12" x2="3" y2="12"></line>
          <line x1="21" y1="12" x2="23" y2="12"></line>
          <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
          <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
        </svg>
        <svg v-else viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
        </svg>
      </div>
    </header>
    <main class="workspace">
      <div class="waterfall-container">
        <virtual-waterfall-parent :waterfall-data="waterfallData" :is-loading="isLoading" :has-more-data="hasMoreData" @load-more="handleLoadMore" />
      </div>
    </main>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import VirtualWaterfallParent from './views/virtual-waterfall-parent.vue'
import type { IDataItem } from 'fs-virtual-waterfall'

// 数据相关状态
const waterfallData = ref<IDataItem[]>([])
const isLoading = ref<boolean>(false)
const hasMoreData = ref<boolean>(true)
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)

// 主题切换
const theme = ref(localStorage.getItem('theme') || 'light')

const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
  localStorage.setItem('theme', theme.value)
  document.documentElement.setAttribute('data-theme', theme.value)
}

// 初始化主题
document.documentElement.setAttribute('data-theme', theme.value)

// 处理加载更多数据
const handleLoadMore = async () => {
  await fetchData()
}

// 数据请求函数
const fetchData = async () => {
  try {
    if (isLoading.value || !hasMoreData.value) return

    isLoading.value = true

    // 请求,并传入分页参数
    const rep = await fetch(`https://www.vilipix.com/api/v1/picture/public?limit=${pageSize.value}&sort=hot&offset=${(currentPage.value - 1) * pageSize.value}`)

    if (!rep.ok) {
      throw new Error(`API request failed with status ${rep.status}`)
    }

    // 数据处理
    const data = await rep.json()

    if (!data || !data.data || !Array.isArray(data.data.rows)) {
      console.warn('API response format unexpected:', data)
      hasMoreData.value = false
      return
    }

    let { rows, count } = data.data
    count = 100
    // 强制宽高比例不为0,防止布局错误
    const processedRows = rows.map((item: any) => {
      // 确保宽高都为有效的正数
      const width = Math.max(parseInt(item.width) || 1, 1)
      const height = Math.max(parseInt(item.height) || 1, 1)
      // 更智能地处理宽高比
      const aspectRatio = width / height

      // 对于宽高比超出正常范围的图片进行修正
      let normalizedHeight = height

      // 减小最小卡片高度
      const minCardHeight = 180
      const baseWidth = 240

      // 计算图片在实际显示宽度下的映射高度
      const scaledHeight = (baseWidth * height) / width

      // 更严格控制宽高比例
      if (aspectRatio > 2) {
        normalizedHeight = width / 2
      } else if (aspectRatio < 0.3) {
        normalizedHeight = width / 0.3
      }

      // 确保图片能完整显示,不要裁剪
      const contentAreaHeight = 0 // 移除内容区域高度限制
      if (scaledHeight < minCardHeight - contentAreaHeight) {
        normalizedHeight = Math.max(normalizedHeight, ((minCardHeight - contentAreaHeight) * width) / baseWidth)
      }

      // 不限制最大高度,确保图片完整显示
      // normalizedHeight = Math.min(normalizedHeight, 900)
      return {
        id: item.picture_id,
        picture_id: item.picture_id,
        width: width,
        height: normalizedHeight,
        src: item.regular_url + '?x-oss-process=image/resize,w_240/format,jpg',
      }
    })

    // 更新数据和状态
    waterfallData.value = [...waterfallData.value, ...processedRows]
    currentPage.value++

    // 检查是否还有更多数据
    if (processedRows.length < pageSize.value || (count && waterfallData.value.length >= count)) {
      hasMoreData.value = false
    }
  } catch (error) {
    console.error('Failed to fetch data:', error)
    hasMoreData.value = false
  } finally {
    isLoading.value = false
  }
}

// 初始化加载第一页数据
onMounted(() => {
  fetchData()
})
</script>

<style>
:root {
  --bg-color: #f6f8fa;
  --text-color: #24292e;
  --border-color: #e1e4e8;
  --header-bg: #ffffff;
  --header-shadow: rgba(0, 0, 0, 0.1);
}

[data-theme='dark'] {
  --bg-color: #0d1117;
  --text-color: #c9d1d9;
  --border-color: #30363d;
  --header-bg: #161b22;
  --header-shadow: rgba(0, 0, 0, 0.3);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s, color 0.3s;
}
</style>

<style scoped lang="scss">
.app {
  width: 100vw;
  height: 98vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  margin: 20px;
  box-sizing: border-box;
}

.header {
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  background-color: var(--header-bg);
  border-bottom: 1px solid var(--border-color);
  box-shadow: 0 1px 3px var(--header-shadow);
  z-index: 10;

  .logo {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--text-color);

    svg {
      stroke: #4299e1;
    }

    h1 {
      font-size: 18px;
      font-weight: 600;
    }
  }

  .theme-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    cursor: pointer;
    color: var(--text-color);

    &:hover {
      background-color: var(--bg-color);
    }

    svg {
      stroke: currentColor;
    }
  }
}

.workspace {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  overflow: hidden;
}

.waterfall-container {
  width: 100%;
  max-width: 1200px;
  height: 100%;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
  // 确保容器不会有额外的内边距
  padding: 0;
  margin: 0;
}
</style>

子组件

<template>
  <div class="virtual-waterfall-container">
    <div class="controls">
      <div class="control-group">
        <label>列数:</label>
        <select v-model="columnCount">
          <option :value="2">2列</option>
          <option :value="3">3列</option>
          <option :value="4">4列</option>
          <option :value="5">5列</option>
        </select>
      </div>
      <div class="control-group">
        <label>样式:</label>
        <select v-model="cardStyle">
          <option value="rounded">圆角</option>
          <option value="square">方形</option>
        </select>
      </div>
      <div class="control-group">
        <label>主题:</label>
        <select v-model="themeMode">
          <option value="light">亮色</option>
          <option value="dark">暗色</option>
          <option value="auto">自动</option>
        </select>
      </div>
      <div class="control-group">
        <label>调试:</label>
        <input type="checkbox" v-model="debugMode" />
      </div>
    </div>

    <fs-virtual-water-fall :data="waterfallData" :loading="isLoading" :has-more="hasMoreData" :gap="15" :column="columnCount" :theme-mode="themeMode" :card-style="cardStyle" :debug="debugMode" :initial-height="800" :load-more-threshold="300" @load-more="handleLoadMore" class="waterfall-wrapper">
      <template #item="{ item }">
        <div class="waterfall-card">
          <div class="waterfall-image-container">
            <div class="waterfall-image-skeleton" v-if="!imageLoadStatus[item.id]"></div>
            <img class="waterfall-image" :src="item.src" loading="lazy" @error="handleImageError" @load="handleImageLoad(item.id)" :class="{ 'image-loaded': imageLoadStatus[item.id] }" />
            <div class="waterfall-image-overlay">
              <span class="waterfall-image-icon">
                <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
                  <circle cx="12" cy="12" r="10"></circle>
                  <path d="M12 8v8"></path>
                  <path d="M8 12h8"></path>
                </svg>
              </span>
            </div>
          </div>
          <div class="waterfall-content">
            <h3 class="waterfall-title">id: {{ item.picture_id }}</h3>
            <!-- <div class="waterfall-description">
              <button>Add</button>
              <button>Edit</button>
              <button>View</button>
              <button>Delete</button>
            </div> -->
            <div class="waterfall-info">
              <span>{{ item.width }} × {{ item.height }}</span>
              <span class="waterfall-view-count">
                <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
                  <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
                  <circle cx="12" cy="12" r="3"></circle>
                </svg>
                <!-- <span>{{ Math.floor(Math.random() * 1000) + 100 }}</span> -->
              </span>
            </div>
          </div>
        </div>
      </template>

      <template #footer>
        <div v-if="isLoading" class="waterfall-bottom-loading">
          <div class="loading-spinner"></div>
          <div class="loading-text">加载中...</div>
        </div>
      </template>
    </fs-virtual-water-fall>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { FsVirtualWaterFall } from 'fs-virtual-waterfall'
import type { IDataItem } from 'fs-virtual-waterfall'

// 接收来自App.vue的props
const props = defineProps<{
  waterfallData: IDataItem[]
  isLoading: boolean
  hasMoreData: boolean
}>()

// 发射事件
const emit = defineEmits<{
  loadMore: []
}>()

// 瀑布流配置
const columnCount = ref<number>(4)
const cardStyle = ref<'rounded' | 'square'>('rounded')
const themeMode = ref<'light' | 'dark' | 'auto'>('auto')
const debugMode = ref<boolean>(false)
const imageLoadStatus = ref<Record<string | number, boolean>>({})

// 处理图片加载错误
const handleImageError = (e: Event) => {
  const imgElement = e.target as HTMLImageElement
  if (imgElement) {
    imgElement.src = 'https://via.placeholder.com/240x320?text=Image+Error'
  }
}

// 处理图片加载完成
// 修改图片加载处理函数
const handleImageLoad = (id: string | number) => {
  setTimeout(() => {
    imageLoadStatus.value[id] = true
  }, 100)
}

// 处理加载更多 - 现在只需要发送事件给App
const handleLoadMore = (currentLength: number) => {
  emit('loadMore')
}
</script>

<style scoped lang="scss">
.virtual-waterfall-container {
  width: 100%;
  height: 100%;
  min-height: 100vh;
  background-color: var(--bg-color, #f6f8fa);
  padding: 10px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.waterfall-wrapper {
  flex: 1;
  overflow: auto;
  position: relative;
  // 确保瀑布流容器不会有额外的内边距
  padding: 0;
  margin: 0;
}

// 修改底部加载区域样式
:deep(.fs-virtual-waterfall-footer) {
  height: auto !important;
  min-height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
}

// 确保卡片底部没有多余空间
:deep(.fs-virtual-waterfall-item-inner) {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.waterfall-card {
  height: 100%;
  display: flex;
  flex-direction: column;
  border-color: pink;
  overflow: hidden;
  position: relative;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  min-height: 200px;
  opacity: 0; /* 添加此行 */
  animation: fadeIn 0.3s ease forwards; /* 添加此行 */
}

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

.waterfall-image {
  width: 100%;
  height: 100% !important; /* 修改此行 */
  object-fit: cover;
  min-height: 160px;
  max-height: 600px;
  display: block;
  transition: transform 0.4s ease, opacity 0.3s ease; /* 修改此行 */
  opacity: 0;
  position: relative;
  z-index: 2;

  &.image-loaded {
    opacity: 1;
  }
}

.controls {
  display: flex;
  gap: 20px;
  margin-bottom: 15px;
  padding: 10px;
  background-color: rgba(0, 0, 0, 0.03);
  border-radius: 8px;
  flex-wrap: wrap;
}

.control-group {
  display: flex;
  align-items: center;
  gap: 8px;

  label {
    font-size: 14px;
    color: var(--text-color, #333);
  }

  select {
    padding: 5px 10px;
    border-radius: 4px;
    border: 1px solid var(--border-color, #ddd);
    background-color: var(--card-bg, #fff);
    color: var(--text-color, #333);
    cursor: pointer;

    &:focus {
      outline: none;
      border-color: #4299e1;
    }
  }
}

.waterfall-card {
  height: 100%;
  display: flex;
  flex-direction: column;
  // background-color: var(--card-bg, #fff);
  border-color: pink;
  overflow: hidden;
  position: relative;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  min-height: 200px; /* 确保卡片最小高度 */
}

.waterfall-image-container {
  position: relative;
  overflow: hidden;
  width: 100%;
  flex: 1; /* 让图片容器根据内容高度灵活伸缩 */
  min-height: 160px; /* 图片容器最小高度,确保足够空间 */
  background-color: #f0f0f0; /* 图片加载前的背景色 */
  display: flex; /* 添加此行 */
  align-items: center; /* 添加此行 */
  justify-content: center; /* 添加此行 */

  &:hover {
    .waterfall-image-overlay {
      opacity: 1;
    }

    .waterfall-image {
      transform: scale(1.05);
    }
  }
}

.waterfall-image-skeleton {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, var(--card-bg, #f0f0f0) 25%, rgba(240, 240, 240, 0.5) 50%, var(--card-bg, #f0f0f0) 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.5s infinite;
  z-index: 1;
}

.waterfall-image {
  width: 100%;
  height: 100% !important; // ✨
  object-fit: cover; /* 保持图片比例并填充容器 */
  min-height: 160px; /* 确保图片最小高度 */
  max-height: 600px; /* 限制图片最大高度,避免过长 */
  display: block;
  transition: transform 0.4s ease;
  opacity: 0;
  position: relative;
  z-index: 2;

  &.image-loaded {
    opacity: 1;
  }
}

.waterfall-image-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.3s ease;
  z-index: 3;
}

.waterfall-image-icon {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #333;
  transform: translateY(10px);
  transition: transform 0.3s ease;

  .waterfall-image-overlay:hover & {
    transform: translateY(0);
  }

  svg {
    width: 20px;
    height: 20px;
  }
}

.waterfall-content {
  padding: 12px 15px;
  // height: 80% !important;
  min-height: 70px; /* 增加内容区域最小高度,确保显示完整内容 */
  display: flex;
  flex-direction: column;
  justify-content: space-between; /* 确保标题和信息区域分布合理 */
  flex-shrink: 0; /* 防止内容区被压缩 */
}

.waterfall-title {
  margin: 10px 0 8px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary, #fff);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.waterfall-description {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  button {
    padding: 4px 12px; /* 舒适的内边距[2,6](@ref) */
    margin: 8px; /* 按钮间距 */
    font-family: 'Segoe UI', system-ui; /* 现代字体 */
    font-size: 14px; /* 适中字号[8](@ref) */
    border: none; /* 移除默认边框[6](@ref) */
    border-radius: 6px; /* 柔和圆角[8](@ref) */
    cursor: pointer; /* 手型光标 */
    transition: all 0.3s ease; /* 平滑过渡效果[6,8](@ref) */
    text-transform: uppercase; /* 字母大写增强识别性[6](@ref) */
    letter-spacing: 1px; /* 字符间距 */
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 基础阴影[6](@ref) */
  }
}
.waterfall-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0;
  font-size: 12px;
  color: var(--text-secondary, #6e7687);
  line-height: 1.5; /* 确保文本有足够行高 */
  flex-shrink: 0; /* 防止信息区被压缩 */
}

.waterfall-view-count {
  display: flex;
  align-items: center;
  gap: 4px;

  svg {
    stroke: currentColor;
  }

  p {
    margin: 0; /* 确保p标签没有默认边距 */
  }
}

.waterfall-bottom-loading {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20px 0;
  color: var(--text-secondary, #6e7687);

  .loading-spinner {
    width: 30px;
    height: 30px;
    border: 3px solid rgba(0, 0, 0, 0.1);
    border-radius: 50%;
    border-top-color: var(--text-primary, #333);
    animation: spin 1s linear infinite;
    margin-bottom: 10px;
  }

  .loading-text {
    font-size: 14px;
  }

  @keyframes spin {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
}

@keyframes skeleton-loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

@media (prefers-color-scheme: dark) {
  .virtual-waterfall-container {
    --bg-color: #1a1a1a;
    --card-bg: #242424;
    --text-color: #e0e0e0;
    --text-primary: #e0e0e0;
    --text-secondary: #a0a0a0;
    --border-color: #333;
  }

  .waterfall-image-container {
    background-color: #333;
  }
}
</style>

Props 参数

| 参数 | 类型 | 默认值 | 说明 | | ----------------- | --------------------------- | ---------- | ------------------------------------------ | | data | Array | [] | 数据源,由父组件传入 (v0.1.5+) | | loading | boolean | false | 是否正在加载中 (v0.1.5+) | | hasMore | boolean | true | 是否还有更多数据 (v0.1.5+) | | column | number | 4 | 瀑布流列数 | | gap | number | 20 | 间距 | | columnItemCount | number | 6 | 每列初始加载的项数 | | themeMode | 'light' | 'dark' | 'auto' | 'auto' | 主题模式,支持亮色、暗色和自动(跟随系统) | | cardStyle | 'rounded' | 'square' | 'rounded' | 卡片样式,圆角或方形 | | scrollBehavior | 'smooth' | 'auto' | 'smooth' | 滚动行为 | | initialHeight | number | 1000 | 初始高度(px) | | loadMoreThreshold | number | 200 | 加载更多阈值(px) | | hoverScale | number | 1.02 | 悬停时的缩放比例 | | debug | boolean | false | 是否启用调试模式,显示列边界 | | emptyText | string | '暂无数据' | 空状态文本 |

注意: request 属性在 v0.1.5 版本已弃用,建议使用 dataloadinghasMore 属性代替。

Events 事件

| 事件名 | 参数 | 说明 | | ------------- | ----------------------- | -------------------------------------------------------------- | | loadMore | (currentLength: number) | 当需要加载更多数据时触发,参数为当前已加载的数据长度 (v0.1.5+) | | loadError | (error: Error) | 当加载数据出错时触发 | | finishGetList | - | 当数据全部加载完成时触发 (v0.1.4 及以下版本) |

Slots 插槽

| 插槽名 | 参数 | 说明 | | ------ | ------------------- | -------------- | | item | { item: IDataItem } | 自定义项内容 | | footer | - | 自定义底部内容 |

数据格式

请求数据返回的列表项必须包含以下属性:

interface IDataItem {
  id: number | string // 唯一标识
  width: number // 原始宽度
  height: number // 原始高度
  [key: string]: any // 其他自定义属性
}

主题和样式

组件提供了三种主题模式:

  • light - 亮色主题
  • dark - 暗色主题
  • auto - 自动主题(根据用户系统设置自动切换)

卡片样式支持:

  • rounded - 圆角卡片
  • square - 方形卡片

加载状态和用户体验

组件提供了两种加载状态的视觉反馈:

  1. 图片骨架屏:图片加载过程中显示的骨架屏动画效果,提供加载中的视觉反馈
  2. 底部加载指示器:在底部显示加载动画,当用户滚动到底部加载更多数据时提供反馈

这些加载状态提升了用户体验,可以:

  • 减少用户等待的焦虑感
  • 提供明确的视觉反馈
  • 防止在加载过程中出现布局抖动
  • 确保用户理解应用正在响应他们的滚动操作

性能优化和最佳实践

  • 确保容器有明确的高度,例如:height: 600px
  • 设置适当的列数(column)以适应不同屏幕尺寸
  • 对于大型应用,建议使用 cardStyle="square" 来提高渲染性能
  • 在移动设备上推荐使用较小的 loadMoreThreshold 值以节省资源
  • 使用组件提供的图片加载状态管理,提供更好的用户体验
  • 为图片添加 loading="lazy" 属性以利用浏览器原生懒加载能力
  • 3D 效果对性能有一定影响,在低性能设备上可以设置 enable3DEffect="false"
  • 为长列表设置合理的 requestSize,避免一次加载过多数据
  • 合理设置内容区域最小高度,确保信息完整显示

License

MIT