<template>
<div class="container">
<h1 class="title">Vue3 Waterfall Demo</h1>
<div class="controls">
<button @click="addItems" :disabled="loading" class="load-more-btn">
<span v-if="!loading">加载更多</span>
<span v-else class="loading-spinner"></span>
</button>
<button class="clear-reload-btn" @click="forceUpdate">强制更新</button>
<button @click="clearAndReload" class="clear-reload-btn">
清空并重新加载
</button>
</div>
<div v-infinite-scroll="loadMore">
<Waterfall
ref="waterfall"
:list="imageList"
:width="300"
:gutter="16"
:columns="4"
img-selector="url"
animation-effect="fadeIn"
:animation-duration="1000"
:animation-delay="300"
background-color="#f5f5f5"
>
<template #item="{ item }">
<div class="card">
<div class="image-wrapper">
<LazyImg
:url="item.url"
:previewSrcList="[item.url]"
:previewIcon="previewIcon"
:hideOnClickModal="true"
/>
<!-- <div class="overlay">
<span class="zoom-icon">🔍</span>
</div> -->
</div>
<div class="card-content">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<div class="card-footer">
<span class="likes">❤️ {{ item.likes }}</span>
<span class="views">👁️ {{ item.views }}</span>
</div>
</div>
</div>
</template>
</Waterfall>
</div>
</div>
</template>
<script setup lang="ts">
import _ from "lodash";
import { ref } from "vue";
import { Waterfall, LazyImg } from "../../lib/index";
import previewIcon from "../assets/previewIcon.png";
declare module "vue" {
interface HTMLAttributes {
"v-infinite-scroll"?: () => void;
}
}
interface ImageItem {
id: number;
url: string;
title: string;
description: string;
likes: number;
views: number;
}
const baseImages: ImageItem[] = [
{
id: 1,
url: new URL(`../assets/image.png`, import.meta.url).href,
title: "工作室风景",
description: "工作室的风景,充满着创意与灵感",
likes: 128,
views: 356,
},
{
id: 2,
url: new URL(`../assets/01.jpg`, import.meta.url).href,
title: "飞行员兔子",
description: "一只可爱的飞行员兔子,喜欢吗?",
likes: 245,
views: 589,
},
{
id: 3,
url: new URL(`../assets/book.jpg`, import.meta.url).href,
title: "书籍",
description: "HarmonyOS NEXT 原生开发之旅",
likes: 167,
views: 423,
},
{
id: 4,
url: new URL(`../assets/str.png`, import.meta.url).href,
title: "树莓,莓",
description: "据说树莓含有大量的营养成分是真的么??",
likes: 198,
views: 467,
},
{
id: 5,
url: new URL(`../assets/animate.png`, import.meta.url).href,
title: "动物世界",
description: "可爱有趣的动物们,带来欢乐与温暖",
likes: 312,
views: 678,
},
];
const imageList = ref<ImageItem[]>(baseImages);
const loading = ref(false);
const waterfall = ref<any>(null);
/**
* 强制更新瀑布流布局
* 通过调用瀑布流组件的renderer方法来重新计算和更新布局
* 在图片加载完成或需要手动更新布局时调用
*/
const forceUpdate = () => {
waterfall.value.renderer();
};
/**
* 清空并重新加载数据
* 通过调用瀑布流组件的clearAndReload方法来清空当前数据并重新加载
* 适用于需要完全重置瀑布流状态的场景,如切换数据源或重新排序
*/
const clearAndReload = () => {
waterfall.value.clearAndReload();
};
const addItems = async () => {
loading.value = true;
const newItems = baseImages.map((item, index) => ({
...item,
id: imageList.value.length + index + 1,
url: `${item.url}?random=${Date.now()}-${index}`,
likes: Math.floor(Math.random() * 500),
views: Math.floor(Math.random() * 1000),
}));
imageList.value.push(...newItems);
loading.value = false;
};
const loadMore = _.debounce(
() => {
addItems();
},
1000,
{
leading: true,
trailing: false,
}
);
</script>
<style scoped>
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.title {
text-align: center;
margin-bottom: 20px;
color: #333;
font-size: 2.5em;
font-weight: bold;
}
.controls {
text-align: center;
margin-bottom: 20px;
}
.load-more-btn {
padding: 12px 24px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
position: relative;
min-width: 120px;
}
.load-more-btn:hover {
background-color: #45a049;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.load-more-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.clear-reload-btn {
margin-left: 10px;
padding: 12px 24px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.clear-reload-btn:hover {
background-color: #ff6b81;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.card {
border-radius: 12px;
overflow: hidden;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.image-wrapper {
position: relative;
overflow: hidden;
}
.image-wrapper img {
width: 100%;
display: block;
transition: transform 0.3s ease;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.card:hover .overlay {
opacity: 1;
}
.card:hover .image-wrapper img {
transform: scale(1.1);
}
.zoom-icon {
font-size: 2em;
color: white;
}
.card-content {
padding: 16px;
}
.card-content h3 {
margin: 0 0 8px;
color: #333;
font-size: 1.2em;
font-weight: bold;
}
.card-content p {
margin: 0 0 12px;
color: #666;
font-size: 0.9em;
line-height: 1.5;
}
.card-footer {
display: flex;
justify-content: space-between;
color: #888;
font-size: 0.9em;
}
.likes,
.views {
display: flex;
align-items: center;
gap: 4px;
}
</style>