vue3实现图片瀑布流展示

小码哥(xmgcode88) 2024-09-30 14:33:01 阅读 51

最近在研发AI副业项目平台,然后自己设计了一个瀑布流组件,可以随意调整展示的列数、懒加载、每页滚动数量、高度、点击效果等。

一、效果

先看看效果如何,如何随意调整4列、5列、6列、N列展示。

二、实现方法

现建立components/waterfall/index.vue组件

<code><template>

<div class="waterfall-container" ref="containerRef" @scroll="handleScroll">code>

<div class="waterfall-list">code>

<div

class="waterfall-item"code>

v-for="(item, index) in resultList"code>

:key="index"code>

:style="{

width: `${item.width}px`,

height: `${item.height}px`,

transform: `translate3d(${item.x}px, ${item.y}px, 0)`,

}"code>

>

<slot name="item" v-bind="item"></slot>code>

</div>

<div v-if="isEnd" class="no-more-data">暂无更多数据</div>code>

</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">code>

.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 = '获取数据', time = 2000) => {

return new Promise((resolve) => {

setTimeout(() => {

console.log(`模拟获取接口数据`, data)

resolve(data)

}, time)

})

}

// 获取数组随机项

export const getRandomElement = (arr) => {

var randomIndex = Math.floor(Math.random() * arr.length);

return arr[randomIndex];

}

// 指定范围随机数

export const getRandomNumber = (min, max) => {

return Math.floor(Math.random() * (max - min + 1) + min);

}

// 节流

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">code>

<el-row>

<el-col :span="6">code>

<div class="inner">code>

<div class="sd-box">code>

<h2>DALL-E 创作中心</h2>

<div>

<el-form label-position="left">code>

<div style="padding-top: 10px">code>

<el-form-item :label-style="{ color: 'white' }" label="图片尺寸">code>

<template #default>

<div>

<el-select v-model="selectedValue" @change="updateSize" style="width:176px">code>

<el-option label="1024*1024" value="1024*1024"/>code>

<el-option label="1972*1024" value="1972*1024"/>code>

<el-option label="1024*1972" value="1024*1972"/>code>

</el-select>

</div>

</template>

</el-form-item>

</div>

<div style="padding-top: 10px">code>

<div class="param-line">code>

<el-input

v-model="dalleParams.prompt"code>

:autosize="{ minRows: 4, maxRows: 6 }"code>

type="textarea"code>

ref="promptRef"code>

placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"code>

/>

</div>

</div>

</el-form>

</div>

<div class="submit-btn">code>

<el-button color="#ffffff" :loading="loading" :dark="false" round @click="generate">code>

立即生成

</el-button>

</div>

</div>

</div>

</el-col>

<el-col :span="18">code>

<div class="inner">code>

<div class="right-box">code>

<h2>创作记录</h2>

<div>

<el-form label-position="left">code>

<div class="container">code>

<WaterFall :columns="columns" :gap="10" :images="images" :fetchMoreImages="fetchMoreImages" :isEnd="isEnd">code>

<template #item="{ image }">code>

<div class="card-box">code>

<el-image :src="image.url" @click="previewImg(image)" alt="waterfall image" fit="cover" style="width: 100%; height: 100%;cursor:pointer;" loading="lazy"></el-image>code>

</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]"/>code>

</div>

</template>

<script lang="ts" setup>code>

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);

// 请求参数

const dalleParams = reactive({

size:"1024*1024",

prompt: ""

});

// 创建绘图任务

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 数组的开头

// 将新图片插入到 images 数组的开头

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);

// 请求参数

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; // 如果已经没有更多数据了,直接返回

}

const newImages = await fetchImages();

images.value = [...newImages];

};

// 列数设置

const columns = ref(4); // 你可以在这里修改列数

//放大预览

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>

项目源码和问题交流,可以通过文末名片找到我。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。