vue3实现图片瀑布流展示

打印 上一主题 下一主题

主题 1792|帖子 1792|积分 5378

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

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

一、效果

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

二、实现方法

现建立components/waterfall/index.vue组件
  1. <template>
  2.   <div class="waterfall-container" ref="containerRef" @scroll="handleScroll">
  3.     <div class="waterfall-list">
  4.       <div
  5.         class="waterfall-item"
  6.         v-for="(item, index) in resultList"
  7.         :key="index"
  8.         :style="{
  9.           width: `${item.width}px`,
  10.           height: `${item.height}px`,
  11.           transform: `translate3d(${item.x}px, ${item.y}px, 0)`,
  12.         }"
  13.       >
  14.         <slot name="item" v-bind="item"></slot>
  15.       </div>
  16.       <div v-if="isEnd" class="no-more-data">暂无更多数据</div>
  17.     </div>
  18.   </div>
  19. </template>
  20. <script setup>
  21. import { ref, onMounted, computed, onUnmounted, watch } from "vue";
  22. import { throttle, debounce } from "@/utils/waterfall/utils.js";
  23. const props = defineProps({
  24.   gap: {
  25.     type: Number,
  26.     default: 10,
  27.   },
  28.   columns: {
  29.     type: Number,
  30.     default: 3,
  31.   },
  32.   bottom: {
  33.     type: Number,
  34.     default: 0,
  35.   },
  36.   images: {
  37.     type: Array,
  38.     default: () => [],
  39.   },
  40.   fetchMoreImages: {
  41.     type: Function,
  42.     required: true,
  43.   },
  44.   isEnd: {
  45.     type: Boolean,
  46.     default: false,
  47.   },
  48. });
  49. const containerRef = ref(null);
  50. const cardWidth = ref(0);
  51. const columnHeight = ref(new Array(props.columns).fill(0));
  52. const resultList = ref([]);
  53. const loading = ref(false);
  54. const minColumn = computed(() => {
  55.   let minIndex = -1,
  56.     minHeight = Infinity;
  57.   columnHeight.value.forEach((item, index) => {
  58.     if (item < minHeight) {
  59.       minHeight = item;
  60.       minIndex = index;
  61.     }
  62.   });
  63.   return {
  64.     minIndex,
  65.     minHeight,
  66.   };
  67. });
  68. const handleScroll = throttle(() => {
  69.   const { scrollTop, clientHeight, scrollHeight } = containerRef.value;
  70.   const bottom = scrollHeight - clientHeight - scrollTop;
  71.   if (bottom <= props.bottom && !props.isEnd) {
  72.     !loading.value && props.fetchMoreImages();
  73.   }
  74. });
  75. const getList = (list) => {
  76.   return list.map((x, index) => {
  77.     const cardHeight = Math.floor((x.height * cardWidth.value) / x.width);
  78.     const { minIndex, minHeight } = minColumn.value;
  79.     const isInit = index < props.columns && resultList.value.length < props.columns;
  80.     if (isInit) {
  81.       columnHeight.value[index] = cardHeight + props.gap;
  82.     } else {
  83.       columnHeight.value[minIndex] += cardHeight + props.gap;
  84.     }
  85.     return {
  86.       width: cardWidth.value,
  87.       height: cardHeight,
  88.       x: isInit
  89.         ? index % props.columns !== 0
  90.           ? index * (cardWidth.value + props.gap)
  91.           : 0
  92.         : minIndex % props.columns !== 0
  93.         ? minIndex * (cardWidth.value + props.gap)
  94.         : 0,
  95.       y: isInit ? 0 : minHeight,
  96.       image: x,
  97.     };
  98.   });
  99. };
  100. const resizeObserver = new ResizeObserver(() => {
  101.   handleResize();
  102. });
  103. const handleResize = debounce(() => {
  104.   const containerWidth = containerRef.value.clientWidth;
  105.   cardWidth.value =
  106.     (containerWidth - props.gap * (props.columns - 1)) / props.columns;
  107.   columnHeight.value = new Array(props.columns).fill(0);
  108.   resultList.value = getList(resultList.value);
  109. });
  110. const init = () => {
  111.   if (containerRef.value) {
  112.     const containerWidth = containerRef.value.clientWidth;
  113.     cardWidth.value =
  114.       (containerWidth - props.gap * (props.columns - 1)) / props.columns;
  115.     resultList.value = getList(props.images);
  116.     resizeObserver.observe(containerRef.value);
  117.   }
  118. };
  119. watch(() => props.images, (newImages) => {
  120.   const newList = getList(newImages);
  121.   resultList.value = [...resultList.value, ...newList];
  122. });
  123. onMounted(() => {
  124.   init();
  125. });
  126. onUnmounted(() => {
  127.   containerRef.value && resizeObserver.unobserve(containerRef.value);
  128. });
  129. </script>
  130. <style lang="scss">
  131. .waterfall {
  132.   &-container {
  133.     width: 100%;
  134.     height: 100%;
  135.     overflow-y: scroll;
  136.     overflow-x: hidden;
  137.   }
  138.   &-list {
  139.     width: 100%;
  140.     position: relative;
  141.   }
  142.   &-item {
  143.     position: absolute;
  144.     left: 0;
  145.     top: 0;
  146.     box-sizing: border-box;
  147.     transition: all 0.3s;
  148.   }
  149.   .no-more-data {
  150.     text-align: center;
  151.     padding: 20px;
  152.     color: #999;
  153.     font-size: 14px;
  154.   }
  155. }
  156. </style>
复制代码
此中@/utils/waterfall/utils.js如下
  1. // 用于模拟接口请求
  2. export const getRemoteData = (data = '获取数据', time = 2000) => {
  3.     return new Promise((resolve) => {
  4.         setTimeout(() => {
  5.             console.log(`模拟获取接口数据`, data)
  6.             resolve(data)
  7.         }, time)
  8.     })
  9. }
  10. // 获取数组随机项
  11. export const getRandomElement = (arr) => {
  12.     var randomIndex = Math.floor(Math.random() * arr.length);
  13.     return arr[randomIndex];
  14. }
  15. // 指定范围随机数
  16. export const getRandomNumber = (min, max) => {
  17.     return Math.floor(Math.random() * (max - min + 1) + min);
  18. }
  19. // 节流
  20. export const throttle = (fn, time) => {
  21.     let timer = null
  22.     return (...args) => {
  23.         if (!timer) {
  24.             timer = setTimeout(() => {
  25.                 timer = null
  26.                 fn.apply(this, args)
  27.             }, time)
  28.         }
  29.     }
  30. }
  31. // 防抖
  32. export const debounce = (fn, time) => {
  33.     let timer = null
  34.     return (...args) => {
  35.         clearTimeout(timer)
  36.         timer = setTimeout(() => {
  37.             fn.apply(this, args)
  38.         }, time)
  39.     }
  40. }
复制代码
调用组件
  1. <template>
  2.   <div>
  3.     <div class="page-dall">
  4.       <el-row>
  5.         <el-col :span="6">
  6.           <div class="inner">
  7.             <div class="sd-box">
  8.               <h2>DALL-E 创作中心</h2>
  9.               <div>
  10.                 <el-form label-position="left">
  11.                   <div style="padding-top: 10px">
  12.                     <el-form-item :label-style="{ color: 'white' }" label="图片尺寸">
  13.                       <template #default>
  14.                         <div>
  15.                           <el-select v-model="selectedValue" @change="updateSize" style="width:176px">
  16.                             <el-option label="1024*1024" value="1024*1024"/>
  17.                             <el-option label="1972*1024" value="1972*1024"/>
  18.                             <el-option label="1024*1972" value="1024*1972"/>
  19.                           </el-select>
  20.                         </div>
  21.                       </template>
  22.                     </el-form-item>
  23.                   </div>
  24.                   <div style="padding-top: 10px">
  25.                     <div class="param-line">
  26.                         <el-input
  27.                             v-model="dalleParams.prompt"
  28.                             :autosize="{ minRows: 4, maxRows: 6 }"
  29.                             type="textarea"
  30.                             ref="promptRef"
  31.                             placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
  32.                         />
  33.                       </div>
  34.                   </div>
  35.                 </el-form>
  36.               </div>
  37.               <div class="submit-btn">
  38.                 <el-button color="#ffffff" :loading="loading" :dark="false" round @click="generate">
  39.                   立即生成
  40.                 </el-button>
  41.               </div>
  42.             </div>
  43.           </div>
  44.         </el-col>
  45.         <el-col :span="18">
  46.           <div class="inner">
  47.             <div class="right-box">
  48.               <h2>创作记录</h2>
  49.               <div>
  50.                 <el-form label-position="left">
  51.                    <div class="container">
  52.                     <WaterFall :columns="columns" :gap="10" :images="images" :fetchMoreImages="fetchMoreImages" :isEnd="isEnd">
  53.                       <template #item="{ image }">
  54.                         <div class="card-box">
  55.                           <el-image :src="image.url" @click="previewImg(image)" alt="waterfall image" fit="cover" style="width: 100%; height: 100%;cursor:pointer;" loading="lazy"></el-image>
  56.                           
  57.                         
  58.                         </div>
  59.                       </template>
  60.                     </WaterFall>
  61.                   </div>
  62.                 </el-form>
  63.               </div>
  64.             </div>
  65.           </div>
  66.         </el-col>
  67.       </el-row>
  68.     </div>
  69.     <el-image-viewer @close="() => { previewURL = '' }" v-if="previewURL !== ''"  :url-list="[previewURL]"/>
  70.   </div>
  71. </template>
  72. <script lang="ts" setup>
  73. import { ElUpload, ElImage, ElDialog, ElRow, ElCol, ElButton, ElIcon, ElTag, ElInput, ElSelect, ElTooltip, ElForm, ElFormItem, ElOption ,ElImageViewer} from "element-plus";
  74. import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
  75. import feedback from "~~/utils/feedback";
  76. import { useUserStore } from '@/stores/user';
  77. import WaterFall from '@/components/waterfall/index.vue';
  78. import * as xmgai from "~~/api/ai";
  79. // 获取图片前缀
  80. const config = useRuntimeConfig();
  81. const filePrefix = config.public.filePrefix;
  82. const router = useRouter();
  83. const selectedValue = ref('1024*1024');
  84. const previewURL = ref("")
  85. const loading = ref(false);
  86. // 请求参数
  87. const dalleParams = reactive({
  88.   size:"1024*1024",
  89.   prompt: ""
  90. });
  91. // 创建绘图任务
  92. const promptRef = ref(null);
  93. const updateSize = () => {
  94.   dalleParams.size = selectedValue.value;
  95. };
  96. const generate = async () => {
  97.   loading.value = true;
  98.   if (dalleParams.prompt === '') {
  99.     promptRef.value.focus();
  100.     loading.value = false;
  101.     return feedback.msgError("请输入绘画提示词!");
  102.    
  103.   }
  104.   const ctdata = await xmgai.dalle3(dalleParams);
  105.   console.info("ctdata",ctdata);
  106.   if (ctdata.code === 0) {
  107.     feedback.msgError(ctdata.msg);
  108.     loading.value = false;
  109.     return [];
  110.   }
  111.   if (ctdata.code === 1) {
  112.     // 获取新生成的图片地址
  113.     const newImage = {
  114.       url: filePrefix +  ctdata.data,
  115.       width: 300 + Math.random() * 300,
  116.       height: 400 + Math.random() * 300,
  117.     };
  118.     // 将新图片插入到 images 数组的开头
  119.      // 将新图片插入到 images 数组的开头
  120.     images.value = [newImage, ...images.value];
  121.     // 将 WaterFall 组件的滚动条滚动到顶部
  122.     nextTick(() => {
  123.       const waterfallContainer = document.querySelector('.waterfall-container');
  124.       if (waterfallContainer) {
  125.         waterfallContainer.scrollTop = 0;
  126.       }
  127.     });
  128.     feedback.msgSuccess(ctdata.msg);
  129.     loading.value = false;
  130.   }
  131. };
  132. const images = ref([]);
  133. const pageNo = ref(1);
  134. const pageSize = ref(10);
  135. const isEnd = ref(false);
  136. // 请求参数
  137. const paramsCreate = reactive({
  138.   aiType: "dalle3",
  139.   pageNo: pageNo.value,
  140.   pageSize: pageSize.value,
  141. });
  142. const fetchImages = async () => {
  143.   const ctdata = await xmgai.aiList(paramsCreate);
  144.   if (ctdata.code === 0) {
  145.     feedback.msgError(ctdata.msg);
  146.     return [];
  147.   }
  148.   if (ctdata.code === 1) {
  149.     const data = ctdata.data.lists;
  150.     if (data.length === 0) {
  151.       isEnd.value = true;
  152.       return [];
  153.     }
  154.     paramsCreate.pageNo++;
  155.     return data.map(item => ({
  156.       ...item, // 保留所有原始字段
  157.       url: filePrefix + item.localUrls,
  158.       width: 300 + Math.random() * 300,
  159.       height: 400 + Math.random() * 300,
  160.     }));
  161.   }
  162. };
  163. const fetchMoreImages = async () => {
  164.   if (isEnd.value) {
  165.     return; // 如果已经没有更多数据了,直接返回
  166.   }
  167.   const newImages = await fetchImages();
  168.   images.value = [...newImages];
  169. };
  170. // 列数设置
  171. const columns = ref(4); // 你可以在这里修改列数
  172. //放大预览
  173. const previewImg = (item) => {
  174.   console.info("item",item.url);
  175.   previewURL.value = item.url
  176. }
  177. onMounted(async () => {
  178.   const initialImages = await fetchImages();
  179.   images.value = initialImages;
  180. });
  181. </script>
  182. <style scoped>
  183. .page-dall {
  184.   background-color: #0c1c9181;
  185.   border-radius: 10px; /* 所有角的圆角大小相同 */
  186.   border: 1px solid #3399FF;
  187. }
  188. .page-dall .inner {
  189.   display: flex;
  190. }
  191. .page-dall .inner .sd-box {
  192.   margin: 10px;
  193.   background-color: #222542b4;
  194.   width: 100%;
  195.   padding: 10px;
  196.   border-radius: 10px;
  197.   color: #ffffff;
  198.   font-size: 14px;
  199. }
  200. .page-dall .inner .sd-box h2 {
  201.   font-weight: bold;
  202.   font-size: 20px;
  203.   text-align: center;
  204.   color: #ffffff;
  205. }
  206. .page-dall .inner .right-box {
  207.   margin: 10px;
  208.   background-color: #222542b4;
  209.   width: 100%;
  210.   padding: 10px;
  211.   border-radius: 10px;
  212.   color: #ffffff;
  213.   font-size: 14px;
  214. }
  215. .page-dall .inner .right-box h2 {
  216.   font-weight: bold;
  217.   font-size: 20px;
  218.   text-align: center;
  219.   color: #ffffff;
  220. }
  221. .submit-btn {
  222.   padding: 10px 15px 0 15px;
  223.   text-align: center;
  224. }
  225. ::v-deep(.el-form-item__label) {
  226.   color: white !important;
  227. }
  228. .container {
  229.   height: 600px;
  230.   border: 2px solid #000;
  231.   margin-top: 10px;
  232.   margin-left: auto;
  233.   margin-right: auto; /* 添加居中处理 */
  234. }
  235. .card-box {
  236.   position: relative;
  237.   width: 100%;
  238.   height: 100%;
  239.   border-radius: 4px;
  240.   overflow: hidden;
  241. }
  242. .card-box img {
  243.   width: 100%;
  244.   height: 100%;
  245.   object-fit: cover;
  246. }
  247. .card-box .remove {
  248.   display: none;
  249.   position: absolute;
  250.   right: 10px;
  251.   top: 10px;
  252. }
  253. .card-box:hover .remove {
  254.   display: block;
  255. }
  256. </style>
复制代码
项目源码和题目交流,可以通过文末名片找到我。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

拉不拉稀肚拉稀

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表