# fs-virtual-waterfall

Vue3 虚拟瀑布流组件，支持无限滚动加载。

## 参考

[链接](https://www.bilibili.com/video/BV1qF411f7g7/?spm_id_from=333.1391.0.0&vd_source=e75c0a517670194c110d1fa53894e419)
修复了些 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 悬浮效果，鼠标移动时带来视觉交互
- 支持亮色、暗色和自动主题模式
- 提供圆角和方形卡片样式选项
- 增强容错性和边界条件处理
- 图片加载骨架屏，提供视觉加载反馈
- 底部加载指示器，提示用户正在加载更多内容
- 智能卡片高度计算，确保内容完整显示
- **新增** - 支持父组件传入数据，更好的组件化设计

## 效果预览

图片
![](https://userplugin-1308962059.cos.ap-shanghai.myqcloud.com/page1.png)
![](https://userplugin-1308962059.cos.ap-shanghai.myqcloud.com/page2.png)
![](https://userplugin-1308962059.cos.ap-shanghai.myqcloud.com/page3.png)
![](https://userplugin-1308962059.cos.ap-shanghai.myqcloud.com/page4.png)
[视频](https://userplugin-1308962059.cos.ap-shanghai.myqcloud.com/%E5%B1%95%E7%A4%BA%E8%A7%86%E9%A2%91.mp4)
<video style="width: 100%;   height: 400px;object-fit: cover; " src="https://userplugin-1308962059.cos.ap-shanghai.myqcloud.com/%E5%B1%95%E7%A4%BA%E8%A7%86%E9%A2%91.mp4" controls></video>

## 安装

```bash
npm install fs-virtual-waterfall
# 或
yarn add fs-virtual-waterfall
# 或
pnpm add fs-virtual-waterfall
```

## 使用方法

> ✨ 直接 CV 大法即可

### 全局注册

```js
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')
```

### 新版用法 父组件请求数据

#### 父组件

```vue
<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>
```

#### 子组件

```vue
<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<IDataItem>            | []         | 数据源，由父组件传入 (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 版本已弃用，建议使用 `data`、`loading` 和 `hasMore` 属性代替。

## Events 事件

| 事件名        | 参数                    | 说明                                                           |
| ------------- | ----------------------- | -------------------------------------------------------------- |
| loadMore      | (currentLength: number) | 当需要加载更多数据时触发，参数为当前已加载的数据长度 (v0.1.5+) |
| loadError     | (error: Error)          | 当加载数据出错时触发                                           |
| finishGetList | -                       | 当数据全部加载完成时触发 (v0.1.4 及以下版本)                   |

## Slots 插槽

| 插槽名 | 参数                | 说明           |
| ------ | ------------------- | -------------- |
| item   | { item: IDataItem } | 自定义项内容   |
| footer | -                   | 自定义底部内容 |

## 数据格式

请求数据返回的列表项必须包含以下属性：

```ts
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
