vue3實(shí)現(xiàn)圖片瀑布流展示效果實(shí)例代碼
最近在研發(fā)AI副業(yè)項(xiàng)目平臺(tái),然后自己設(shè)計(jì)了一個(gè)瀑布流組件,可以隨意調(diào)整展示的列數(shù)、懶加載、每頁(yè)滾動(dòng)數(shù)量、高度、點(diǎn)擊效果等。
一、效果
先看看效果如何,如何隨意調(diào)整4列、5列、6列、N列展示。

二、實(shí)現(xiàn)方法
現(xiàn)建立components/waterfall/index.vue組件
<template>
<div class="waterfall-container" ref="containerRef" @scroll="handleScroll">
<div class="waterfall-list">
<div
class="waterfall-item"
v-for="(item, index) in resultList"
:key="index"
:style="{
width: `${item.width}px`,
height: `${item.height}px`,
transform: `translate3d(${item.x}px, ${item.y}px, 0)`,
}"
>
<slot name="item" v-bind="item"></slot>
</div>
<div v-if="isEnd" class="no-more-data">暫無(wú)更多數(shù)據(jù)</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, onUnmounted, watch } from "vue";
import { throttle, debounce } from "@/utils/waterfall/utils.js";
const props = defineProps({
gap: {
type: Number,
default: 10,
},
columns: {
type: Number,
default: 3,
},
bottom: {
type: Number,
default: 0,
},
images: {
type: Array,
default: () => [],
},
fetchMoreImages: {
type: Function,
required: true,
},
isEnd: {
type: Boolean,
default: false,
},
});
const containerRef = ref(null);
const cardWidth = ref(0);
const columnHeight = ref(new Array(props.columns).fill(0));
const resultList = ref([]);
const loading = ref(false);
const minColumn = computed(() => {
let minIndex = -1,
minHeight = Infinity;
columnHeight.value.forEach((item, index) => {
if (item < minHeight) {
minHeight = item;
minIndex = index;
}
});
return {
minIndex,
minHeight,
};
});
const handleScroll = throttle(() => {
const { scrollTop, clientHeight, scrollHeight } = containerRef.value;
const bottom = scrollHeight - clientHeight - scrollTop;
if (bottom <= props.bottom && !props.isEnd) {
!loading.value && props.fetchMoreImages();
}
});
const getList = (list) => {
return list.map((x, index) => {
const cardHeight = Math.floor((x.height * cardWidth.value) / x.width);
const { minIndex, minHeight } = minColumn.value;
const isInit = index < props.columns && resultList.value.length < props.columns;
if (isInit) {
columnHeight.value[index] = cardHeight + props.gap;
} else {
columnHeight.value[minIndex] += cardHeight + props.gap;
}
return {
width: cardWidth.value,
height: cardHeight,
x: isInit
? index % props.columns !== 0
? index * (cardWidth.value + props.gap)
: 0
: minIndex % props.columns !== 0
? minIndex * (cardWidth.value + props.gap)
: 0,
y: isInit ? 0 : minHeight,
image: x,
};
});
};
const resizeObserver = new ResizeObserver(() => {
handleResize();
});
const handleResize = debounce(() => {
const containerWidth = containerRef.value.clientWidth;
cardWidth.value =
(containerWidth - props.gap * (props.columns - 1)) / props.columns;
columnHeight.value = new Array(props.columns).fill(0);
resultList.value = getList(resultList.value);
});
const init = () => {
if (containerRef.value) {
const containerWidth = containerRef.value.clientWidth;
cardWidth.value =
(containerWidth - props.gap * (props.columns - 1)) / props.columns;
resultList.value = getList(props.images);
resizeObserver.observe(containerRef.value);
}
};
watch(() => props.images, (newImages) => {
const newList = getList(newImages);
resultList.value = [...resultList.value, ...newList];
});
onMounted(() => {
init();
});
onUnmounted(() => {
containerRef.value && resizeObserver.unobserve(containerRef.value);
});
</script>
<style lang="scss">
.waterfall {
&-container {
width: 100%;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
}
&-list {
width: 100%;
position: relative;
}
&-item {
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
transition: all 0.3s;
}
.no-more-data {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
}
</style>其中@/utils/waterfall/utils.js如下
// 用于模擬接口請(qǐng)求
export const getRemoteData = (data = '獲取數(shù)據(jù)', time = 2000) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`模擬獲取接口數(shù)據(jù)`, data)
resolve(data)
}, time)
})
}
// 獲取數(shù)組隨機(jī)項(xiàng)
export const getRandomElement = (arr) => {
var randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}
// 指定范圍隨機(jī)數(shù)
export const getRandomNumber = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
// 節(jié)流
export const throttle = (fn, time) => {
let timer = null
return (...args) => {
if (!timer) {
timer = setTimeout(() => {
timer = null
fn.apply(this, args)
}, time)
}
}
}
// 防抖
export const debounce = (fn, time) => {
let timer = null
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, time)
}
}調(diào)用組件
<template>
<div>
<div class="page-dall">
<el-row>
<el-col :span="6">
<div class="inner">
<div class="sd-box">
<h2>DALL-E 創(chuàng)作中心</h2>
<div>
<el-form label-position="left">
<div style="padding-top: 10px">
<el-form-item :label-style="{ color: 'white' }" label="圖片尺寸">
<template #default>
<div>
<el-select v-model="selectedValue" @change="updateSize" style="width:176px">
<el-option label="1024*1024" value="1024*1024"/>
<el-option label="1972*1024" value="1972*1024"/>
<el-option label="1024*1972" value="1024*1972"/>
</el-select>
</div>
</template>
</el-form-item>
</div>
<div style="padding-top: 10px">
<div class="param-line">
<el-input
v-model="dalleParams.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
placeholder="請(qǐng)?jiān)诖溯斎肜L畫提示詞,系統(tǒng)會(huì)自動(dòng)翻譯中文提示詞,高手請(qǐng)直接輸入英文提示詞"
/>
</div>
</div>
</el-form>
</div>
<div class="submit-btn">
<el-button color="#ffffff" :loading="loading" :dark="false" round @click="generate">
立即生成
</el-button>
</div>
</div>
</div>
</el-col>
<el-col :span="18">
<div class="inner">
<div class="right-box">
<h2>創(chuàng)作記錄</h2>
<div>
<el-form label-position="left">
<div class="container">
<WaterFall :columns="columns" :gap="10" :images="images" :fetchMoreImages="fetchMoreImages" :isEnd="isEnd">
<template #item="{ image }">
<div class="card-box">
<el-image :src="image.url" @click="previewImg(image)" alt="waterfall image" fit="cover" style="width: 100%; height: 100%;cursor:pointer;" loading="lazy"></el-image>
</div>
</template>
</WaterFall>
</div>
</el-form>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<el-image-viewer @close="() => { previewURL = '' }" v-if="previewURL !== ''" :url-list="[previewURL]"/>
</div>
</template>
<script lang="ts" setup>
import { ElUpload, ElImage, ElDialog, ElRow, ElCol, ElButton, ElIcon, ElTag, ElInput, ElSelect, ElTooltip, ElForm, ElFormItem, ElOption ,ElImageViewer} from "element-plus";
import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
import feedback from "~~/utils/feedback";
import { useUserStore } from '@/stores/user';
import WaterFall from '@/components/waterfall/index.vue';
import * as xmgai from "~~/api/ai";
// 獲取圖片前綴
const config = useRuntimeConfig();
const filePrefix = config.public.filePrefix;
const router = useRouter();
const selectedValue = ref('1024*1024');
const previewURL = ref("")
const loading = ref(false);
// 請(qǐng)求參數(shù)
const dalleParams = reactive({
size:"1024*1024",
prompt: ""
});
// 創(chuàng)建繪圖任務(wù)
const promptRef = ref(null);
const updateSize = () => {
dalleParams.size = selectedValue.value;
};
const generate = async () => {
loading.value = true;
if (dalleParams.prompt === '') {
promptRef.value.focus();
loading.value = false;
return feedback.msgError("請(qǐng)輸入繪畫提示詞!");
}
const ctdata = await xmgai.dalle3(dalleParams);
console.info("ctdata",ctdata);
if (ctdata.code === 0) {
feedback.msgError(ctdata.msg);
loading.value = false;
return [];
}
if (ctdata.code === 1) {
// 獲取新生成的圖片地址
const newImage = {
url: filePrefix + ctdata.data,
width: 300 + Math.random() * 300,
height: 400 + Math.random() * 300,
};
// 將新圖片插入到 images 數(shù)組的開(kāi)頭
// 將新圖片插入到 images 數(shù)組的開(kāi)頭
images.value = [newImage, ...images.value];
// 將 WaterFall 組件的滾動(dòng)條滾動(dòng)到頂部
nextTick(() => {
const waterfallContainer = document.querySelector('.waterfall-container');
if (waterfallContainer) {
waterfallContainer.scrollTop = 0;
}
});
feedback.msgSuccess(ctdata.msg);
loading.value = false;
}
};
const images = ref([]);
const pageNo = ref(1);
const pageSize = ref(10);
const isEnd = ref(false);
// 請(qǐng)求參數(shù)
const paramsCreate = reactive({
aiType: "dalle3",
pageNo: pageNo.value,
pageSize: pageSize.value,
});
const fetchImages = async () => {
const ctdata = await xmgai.aiList(paramsCreate);
if (ctdata.code === 0) {
feedback.msgError(ctdata.msg);
return [];
}
if (ctdata.code === 1) {
const data = ctdata.data.lists;
if (data.length === 0) {
isEnd.value = true;
return [];
}
paramsCreate.pageNo++;
return data.map(item => ({
...item, // 保留所有原始字段
url: filePrefix + item.localUrls,
width: 300 + Math.random() * 300,
height: 400 + Math.random() * 300,
}));
}
};
const fetchMoreImages = async () => {
if (isEnd.value) {
return; // 如果已經(jīng)沒(méi)有更多數(shù)據(jù)了,直接返回
}
const newImages = await fetchImages();
images.value = [...newImages];
};
// 列數(shù)設(shè)置
const columns = ref(4); // 你可以在這里修改列數(shù)
//放大預(yù)覽
const previewImg = (item) => {
console.info("item",item.url);
previewURL.value = item.url
}
onMounted(async () => {
const initialImages = await fetchImages();
images.value = initialImages;
});
</script>
<style scoped>
.page-dall {
background-color: #0c1c9181;
border-radius: 10px; /* 所有角的圓角大小相同 */
border: 1px solid #3399FF;
}
.page-dall .inner {
display: flex;
}
.page-dall .inner .sd-box {
margin: 10px;
background-color: #222542b4;
width: 100%;
padding: 10px;
border-radius: 10px;
color: #ffffff;
font-size: 14px;
}
.page-dall .inner .sd-box h2 {
font-weight: bold;
font-size: 20px;
text-align: center;
color: #ffffff;
}
.page-dall .inner .right-box {
margin: 10px;
background-color: #222542b4;
width: 100%;
padding: 10px;
border-radius: 10px;
color: #ffffff;
font-size: 14px;
}
.page-dall .inner .right-box h2 {
font-weight: bold;
font-size: 20px;
text-align: center;
color: #ffffff;
}
.submit-btn {
padding: 10px 15px 0 15px;
text-align: center;
}
::v-deep(.el-form-item__label) {
color: white !important;
}
.container {
height: 600px;
border: 2px solid #000;
margin-top: 10px;
margin-left: auto;
margin-right: auto; /* 添加居中處理 */
}
.card-box {
position: relative;
width: 100%;
height: 100%;
border-radius: 4px;
overflow: hidden;
}
.card-box img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-box .remove {
display: none;
position: absolute;
right: 10px;
top: 10px;
}
.card-box:hover .remove {
display: block;
}
</style>總結(jié)
到此這篇關(guān)于vue3實(shí)現(xiàn)圖片瀑布流展示效果的文章就介紹到這了,更多相關(guān)vue3圖片瀑布流展示內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue3-treeselect數(shù)據(jù)綁定失敗的解決方案
這篇文章主要介紹了vue3-treeselect數(shù)據(jù)綁定失敗的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
vue2?使用echarts實(shí)現(xiàn)地圖點(diǎn)擊進(jìn)入下一層級(jí)+點(diǎn)擊空白處回退功能
這篇文章主要介紹了vue2?使用echarts實(shí)現(xiàn)地圖點(diǎn)擊進(jìn)入下一層級(jí)+點(diǎn)擊空白處回退,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-05-05
vite.config.ts與vite.config.js的區(qū)別小結(jié)
在 Vite 項(xiàng)目中,vite.config.ts?和?vite.config.js?都是 Vite 的配置文件,主要區(qū)別在于使用的語(yǔ)言和類型支持,下面就來(lái)介紹一下2025-07-07
vue使用swiper插件實(shí)現(xiàn)輪播圖的示例
這篇文章主要介紹了vue使用swiper插件實(shí)現(xiàn)輪播圖的示例,幫助大家更好的理解和學(xué)習(xí)使用vue框架,感興趣的朋友可以了解下2021-05-05
vue中vxe-table虛擬滾動(dòng)列表的使用詳解
vxe-table 是一個(gè)功能強(qiáng)大的 Vue 表格組件,它支持虛擬滾動(dòng)列表作為其核心功能之一,本文主要介紹一下vxe-table的虛擬滾動(dòng)列表功能的使用場(chǎng)景和優(yōu)勢(shì),感興趣的可以了解下2023-12-12
Vue中@click.stop和@click.prevent實(shí)例詳解
當(dāng)我們使用Vue.js開(kāi)發(fā)前端應(yīng)用時(shí),經(jīng)常會(huì)在模版中使用@click指令來(lái)響應(yīng)用戶的點(diǎn)擊事件,這篇文章主要給大家介紹了關(guān)于Vue中@click.stop和@click.prevent的相關(guān)資料,需要的朋友可以參考下2024-04-04
在vant中使用時(shí)間選擇器和popup彈出層的操作
這篇文章主要介紹了在vant中使用時(shí)間選擇器和popup彈出層的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
vue3中頁(yè)面跳轉(zhuǎn)兩種實(shí)現(xiàn)方式
在Vue3中Vue?Router是一個(gè)常用的路由管理庫(kù),它提供了一種簡(jiǎn)單而強(qiáng)大的方式來(lái)實(shí)現(xiàn)路由跳轉(zhuǎn)和導(dǎo)航,這篇文章主要給大家介紹了關(guān)于vue3中頁(yè)面跳轉(zhuǎn)的兩種實(shí)現(xiàn)方式,需要的朋友可以參考下2024-09-09

