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

二、實現(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">暫無更多數(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如下
// 用于模擬接口請求
export const getRemoteData = (data = '獲取數(shù)據(jù)', time = 2000) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`模擬獲取接口數(shù)據(jù)`, data)
resolve(data)
}, time)
})
}
// 獲取數(shù)組隨機項
export const getRandomElement = (arr) => {
var randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}
// 指定范圍隨機數(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)
}
}調用組件
<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="請在此輸入繪畫提示詞,系統(tǒ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);
// 請求參數(shù)
const dalleParams = reactive({
size:"1024*1024",
prompt: ""
});
// 創(chuàng)建繪圖任務
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("請輸入繪畫提示詞!");
}
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ù)組的開頭
// 將新圖片插入到 images 數(shù)組的開頭
images.value = [newImage, ...images.value];
// 將 WaterFall 組件的滾動條滾動到頂部
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);
// 請求參數(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; // 如果已經沒有更多數(shù)據(jù)了,直接返回
}
const newImages = await fetchImages();
images.value = [...newImages];
};
// 列數(shù)設置
const columns = ref(4); // 你可以在這里修改列數(shù)
//放大預覽
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>總結
到此這篇關于vue3實現(xiàn)圖片瀑布流展示效果的文章就介紹到這了,更多相關vue3圖片瀑布流展示內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue實現(xiàn)contenteditable元素雙向綁定的方法詳解
contenteditable是所有HTML元素都有的枚舉屬性,表示元素是否可以被用戶編輯。本文將詳細介紹如何實現(xiàn)contenteditable元素的雙向綁定,需要的可以參考一下2022-05-05
詳解vue axios用post提交的數(shù)據(jù)格式
這篇文章主要介紹了詳解vue axios用post提交的數(shù)據(jù)格式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
關于Vue中echarts響應式頁面變化resize()的用法介紹
Vue項目中開發(fā)數(shù)據(jù)大屏,使用echarts圖表根據(jù)不同尺寸的屏幕進行適配,resize()可以調用echarts中內置的resize函數(shù)進行自適應縮放,本文將給大家詳細介紹resize()的用法,需要的朋友可以參考下2023-06-06

