fs-virtual-waterfall
v1.2.0
Published
Vue3 虚拟瀑布流组件
Downloads
40
Maintainers
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 版本已弃用,建议使用 data、loading 和 hasMore 属性代替。
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- 方形卡片
加载状态和用户体验
组件提供了两种加载状态的视觉反馈:
- 图片骨架屏:图片加载过程中显示的骨架屏动画效果,提供加载中的视觉反馈
- 底部加载指示器:在底部显示加载动画,当用户滚动到底部加载更多数据时提供反馈
这些加载状态提升了用户体验,可以:
- 减少用户等待的焦虑感
- 提供明确的视觉反馈
- 防止在加载过程中出现布局抖动
- 确保用户理解应用正在响应他们的滚动操作
性能优化和最佳实践
- 确保容器有明确的高度,例如:
height: 600px - 设置适当的列数(column)以适应不同屏幕尺寸
- 对于大型应用,建议使用
cardStyle="square"来提高渲染性能 - 在移动设备上推荐使用较小的
loadMoreThreshold值以节省资源 - 使用组件提供的图片加载状态管理,提供更好的用户体验
- 为图片添加
loading="lazy"属性以利用浏览器原生懒加载能力 - 3D 效果对性能有一定影响,在低性能设备上可以设置
enable3DEffect="false" - 为长列表设置合理的
requestSize,避免一次加载过多数据 - 合理设置内容区域最小高度,确保信息完整显示
License
MIT
