vue3實(shí)現(xiàn)圖片瀑布流展示效果實(shí)例代碼
最近在研發(fā)AI副業(yè)項(xiàng)目平臺(tái),然后自己設(shè)計(jì)了一個(gè)瀑布流組件,可以隨意調(diào)整展示的列數(shù)、懶加載、每頁滾動(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">暫無更多數(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ù)組的開頭 // 將新圖片插入到 images 數(shù)組的開頭 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)沒有更多數(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)文章
Vue實(shí)現(xiàn)contenteditable元素雙向綁定的方法詳解
contenteditable是所有HTML元素都有的枚舉屬性,表示元素是否可以被用戶編輯。本文將詳細(xì)介紹如何實(shí)現(xiàn)contenteditable元素的雙向綁定,需要的可以參考一下2022-05-05詳解vue axios用post提交的數(shù)據(jù)格式
這篇文章主要介紹了詳解vue axios用post提交的數(shù)據(jù)格式,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08vue響應(yīng)式原理與雙向數(shù)據(jù)的深入解析
Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)。下面這篇文章主要給大家介紹了關(guān)于vue響應(yīng)式原理與雙向數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2021-06-06Vue3.2單文件組件setup的語法糖與新特性總結(jié)
ue3上線已經(jīng)很久了,許多小伙伴應(yīng)該都已經(jīng)使用過vue3了,下面這篇文章主要給大家介紹了關(guān)于Vue3.2單文件組件setup的語法糖與新特性總結(jié)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07關(guān)于Vue中echarts響應(yīng)式頁面變化resize()的用法介紹
Vue項(xiàng)目中開發(fā)數(shù)據(jù)大屏,使用echarts圖表根據(jù)不同尺寸的屏幕進(jìn)行適配,resize()可以調(diào)用echarts中內(nèi)置的resize函數(shù)進(jìn)行自適應(yīng)縮放,本文將給大家詳細(xì)介紹resize()的用法,需要的朋友可以參考下2023-06-06