首页 前端知识 Vue3 结合vueUse实现图片瀑布流布局以及响应式,懒加载

Vue3 结合vueUse实现图片瀑布流布局以及响应式,懒加载

2024-05-08 10:05:49 前端知识 前端哥 741 384 我要收藏

前言

        最近在用vue3及相关生态做一个自己的小网站项目学习时,做到个人中心模块时觉得内容很单一空洞,在短视频上看到瀑布流实现,以前只听说过瀑布流这种概念布局,没有深究其实现原理。想着可以搞个照片墙菜单用vue3实操一波。有很多不足之处,希望大家多多指正。

基本效果

        

瀑布流概念

        瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据并附加至当前尾部。最后页面布局趋向于平铺,所以在视觉上看起来很舒服。

实现思路

        了解瀑布流概念后,图片的宽带应该是固定的,这样我们就能得到页面布局的列数,最后就是要将页面不同元素排的合理使其页面达到尽可能平铺,这就要我们将图片放到合理位置,想到可通过绝对定位来实现。将第一行铺满后,需对后面的图片进行定位操作,需将该图片放到第一行图片各列占用高度最低的元素下,依次类推,最后页面将实现平铺效果。

代码实现

       1.确定列数

        因为页面宽度固定,图片宽度应固定,不考虑各列之间的间隔则列数为页面宽度/图片宽度向下取整,保证图片在横向平铺不产生进度条。在vue3的生态vueUse中,可通过useElementSize()函数获得。

<template>
  <div class="img-wall" ref="imgWall">
  </div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, h, render, onMounted, computed } from "vue";
// 图片显示宽度
const imgWidth = 300;
// 容器元素
const imgWall = ref<HTMLElement>(null);
// 容器盒子宽度
const { width } = useElementSize(imgWall);
//列数
const column = computed(() => Math.floor(width.value / imgWidth));
<script>

useElement 会返回当前传入元素的宽度,返回值为Ref,即如果该元素宽度改变,我们的width是实时改变的,column通过计算属性也及时改变,如列数改变,页面将重新布局。

2.确定位置(核心)

const setPosition = async (startIndex: number) => {
  for (let index = startIndex; index < imgList.value.length; index++) {
    let top;
    let left;
    let ele = imgList.value[index];
    console.log(ele.imgSrc);
    let src = `http://localhost:8080${ele.imgSrc}`;
    let { execute } = useImage({ src: src });
    // useImage 返回的img元素 包含该img的属性值
    let loadImg = await execute();
    if (loadImg == undefined) {
      loadImg = new Image(300, 400);
    }
    // 获取根据缩放比例得到的高度
    // ts类型校验提示loadImg.height可能会为undefined,使用!确认height不可能为空值
    let scaleHeight = Math.ceil(loadImg?.height! * (imgWidth / loadImg?.width));
    // 首先将第一行铺满,得到基准高度
    if (index < column.value) {
      top = gap
      left = imgWidth * index + gap * (index + 1);
      cHeight.value[index] = scaleHeight+gap;
    } else {
      top = cHeight.value[minIndex.value]+ gap;
      left = imgWidth * minIndex.value + gap * (minIndex.value + 1);
      cHeight.value[minIndex.value] =
        cHeight.value[minIndex.value] + scaleHeight+gap;
    }
    loadImg.width = imgWidth;
    loadImg.style.position = "absolute";
    loadImg.style.left = left + "px";
    loadImg.style.top = top + "px";
    imgWall.value.appendChild(loadImg);
  }
};

 对该方法返回的imgList进行循环后对生成图片并进行绝对定位,startIndex是循环的起始位置,因为我这里涉及到懒加载,每次循环的起始位置不一定为0。useImage是vueUse的函数,传入图片路径后,会返回相关加载状态和img。excute是其返回的一个promise,resolve的是加载成功的img元素。loadImg里会有该img的相关属性值。scaleHeight为图片经过缩放比后在页面的显示高度。在循环中,if里首先将第一行铺满,并将每列的初始高度传入cHeight数组(ref<number[]>[]),当循环的元素不在第一列时。通过cHeight数组里最小高度,找到对应的下标及列数,该元素即可确定位置。top为保存的高度数组里的最小高度加上图片顶部间隔(可根据实际设置),left为对应下标与图片宽度的乘积加上对应图片间隔。最后设置img的style,然后通过dom操作,将img挂载到容器下。

3.响应式

 如对应容器的宽度发生变化造成列数变化则需重新对图片设置定位。

watch(width, (nowValue, preValue) => {
  // 初始渲染不调用,因为在首次获取图片时调用
  if (preValue == 0) {
    return;
  }
  // 如果宽度变化小于图片的宽度,则不重新布局
  if (Math.abs(nowValue - preValue) < imgWidth) {
    return;
  }
  // 清除容器下所有已设置的img
  while (imgWall.value.firstChild) {
    imgWall.value.removeChild(imgWall.value.firstChild);
  }
  // 重新设置高度数组
  cHeight.value = [];
  // 重新设置图片位置,因为imgList是响应式,在多次懒加载后,无需再次请求,直接从0开始循环
  setPosition(0);
});

 4.懒加载

 当图片数量很多时,我们不是一次返回所有图片,而是当我们滑倒页面底部时再重新请求下一批数据,从而更新渲染。vueUse给我们提供了该函数useInfiniteScroll()

useInfiniteScroll(
  imgWall,
  (state) => {
    if (!endFlag.value) {
      pageNum.value++;
      queryImgs();
    } else {
      ElMessage.info("无更多内容");
    }
  },
  { distance: 10 }
);

传入对应的元素,和写入自己的业务函数即可

实现代码

<template>
  <div class="img-wall" ref="imgWall">
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, watch, h, render, onMounted, computed } from "vue";
import {
  useArrayFindIndex,
  useElementSize,
  useImage,
  useInfiniteScroll,
} from "@vueuse/core";
import { QueryLifeImg } from "@/api";
import { ElMessage } from "element-plus";
const imgList = ref<string[]>([]);
const total = ref(0);
const endFlag = ref(false);
const pageNum = ref(1);
const imgWidth = 300;
const gap = 10
interface NumberArray {
    [index: number]: number;  // readonly 只读readonly [index: number]: string
}
const imgWall = ref<HTMLElement>(null);
const { width } = useElementSize(imgWall);
const column = computed(() => Math.floor(width.value / imgWidth));
const cHeight = ref<number[]>([]);

// 最小高度对应的下标
const minIndex = useArrayFindIndex(
  cHeight,
  (ele) => ele == Math.min(...cHeight.value)
);
onMounted(() => {
  queryImgs();
});
const queryImgs = async () => {
  let param = {
    appHead: {
      pageNum: pageNum.value,
      pageSize: 20,
    },
    imgTag: "",
  };
  const res = await QueryLifeImg(param);
  if (res.returnCode === "000000") {
    total.value = res.data.total;
    imgList.value = imgList.value.concat(res.data.resultList);
    setPosition((pageNum.value - 1)*20);
    if (imgList.value.length === total.value) {
      endFlag.value = true;
    }
  } else {
    ElMessage.info("无更多内容");
  }
};
const setPosition = async (startIndex: number) => {
  for (let index = startIndex; index < imgList.value.length; index++) {
    let top;
    let left;
    let ele = imgList.value[index];
    console.log(ele.imgSrc);
    let src = `http://localhost:8080${ele.imgSrc}`;
    let { execute } = useImage({ src: src });
    let loadImg = await execute();
    if (loadImg == undefined) {
      loadImg = new Image(300, 400);
    }
    // 获取根据缩放比例得到的高度
    // ts类型校验提示loadImg.height可能会为undefined,使用!确认height不可能为空值
    let scaleHeight = Math.ceil(loadImg?.height! * (imgWidth / loadImg?.width));
    // 首先将第一行铺满,得到基准高度
    if (index < column.value) {
      top = gap
      left = imgWidth * index + gap * (index + 1);
      cHeight.value[index] = scaleHeight+gap;
    } else {
      top = cHeight.value[minIndex.value]+ gap;
      left = imgWidth * minIndex.value + gap * (minIndex.value + 1);
      cHeight.value[minIndex.value] =
        cHeight.value[minIndex.value] + scaleHeight+gap;
    }
    loadImg.width = imgWidth;
    loadImg.style.position = "absolute";
    loadImg.style.left = left + "px";
    loadImg.style.top = top + "px";
    imgWall.value.appendChild(loadImg);
  }
};
/** 图片无限加载 */
useInfiniteScroll(
  imgWall,
  (state) => {
    if (!endFlag.value) {
      pageNum.value++;
      queryImgs();
    } else {
        ElMessage.info("无更多内容");
    }
  },
  { distance: 10 }
);
watch(width, (nowValue, preValue) => {
  if (preValue == 0) {
    return;
  }
  if (Math.abs(nowValue - preValue) < imgWidth) {
    return;
  }
  while (imgWall.value.firstChild) {
    imgWall.value.removeChild(imgWall.value.firstChild);
  }

  cHeight.value = [];
  setPosition(0);
});
</script>
<style lang="scss" scoped>
.img-wall {
  width: 100%;
  position: relative;
  height: 100%;
  overflow-y: auto;
}
</style>

总结

         基于vue3及vueUse实现的瀑布流布局实现了,但也有很多不足之处。一开始我是想通过render函数渲染,但是图片的请求是异步的,在render时一直报错(no parentNode)。后面使用原生dom的append操作。后面我将再试试render函数,以及将该抽离成hook函数以及实现图片的懒加载,相关方法以及定义变量时未定义类型,后续我将完善。各处逻辑还有许多不足之处,请大家多多指教,谢谢。

      

转载请注明出处或者链接地址:https://www.qianduange.cn//article/7547.html
标签
评论
发布的文章

Unity读取Json的几种方法

2024-05-12 17:05:57

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!