分享一個Vue實現圖片水平瀑布流的外掛

2022-09-30 18:02:06

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

一.需求來源

今天碰到了一個需求,需要在頁面裡,用水平瀑布流的方式,將一些圖片進行載入,這讓我突然想起我很久以前寫的一篇文章《JS兩種方式實現水平瀑布流佈局》

但是有個問題,這個需求是Vue專案的,那沒辦法,這裡給大家分享下我的開發過程,專案主體用的是之前在學習的CRMEB的後端框架來開發,UI使用iView-UI,其餘的場景與其他的vue專案一致

二.邏輯設想

如果不是vue環境,我們的邏輯為

1.獲取所有的div元素
2.獲取盒子的寬度,寬度都是相同,高度不同
3.在浮動佈局中每一行的盒子個數不固定,是根據螢幕寬度和盒子寬度決定
4.獲取螢幕寬度
5.求出列數,螢幕寬度 / 盒子寬度 取整
6.瀑布流最關鍵的是第二行的盒子的排布方式,通過獲取第一行盒子中最矮的一個的下標,絕對定位,top是最矮盒子的高度,left是最矮盒子的下標 * 盒子的寬度
7.迴圈遍歷所有的盒子,通過列數找到第一行所有的盒子,將第一行盒子的高度放入陣列,再取出陣列中最小的一個的下標,就是第6步思路的第一行盒子中最矮盒子的下標。
8.迴圈繼續,第二行第一個盒子,通過絕對定位,放進頁面。
9.關鍵,需要將陣列中最小的值加上放進的盒子的高度
10.繼續迴圈,遍歷所有
11.如果想要載入更多,需要判斷最後一個盒子的高度和頁面捲動的距離,再將資料通過建立元素,追加進頁面,再通過瀑布流佈局展示

但如果是Vue專案,我們可以把邏輯歸結為以下幾步

1.獲取螢幕寬度
2..獲取盒子的寬度,寬度都是相同,高度不同
3.在浮動佈局中每一行的盒子個數不固定,是根據螢幕寬度和盒子寬度決定
4.求出列數,螢幕寬度 / 盒子寬度 取整
5.瀑布流最關鍵的是第二行的盒子的排布方式,通過獲取第一行盒子中最矮的一個的下標,絕對定位,top是最矮盒子的高度,left是最矮盒子的下標 * 盒子的寬度
6.繼續迴圈,遍歷所有
7.如果想要載入更多,需要判斷最後一個盒子的高度和頁面捲動的距離,再將資料通過建立元素,追加進頁面,再通過瀑布流佈局展示

三.最終效果圖片

四.程式碼分析

先看下我的html部分

<template>
  <div class="tab-container" id="tabContainer">
    <div class="tab-item" v-for="(item, index) in pbList" :key="index">
      <img :src="item.url" />
    </div>
  </div>
</template>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
/* 最外層大盒子 */
.tab-container {
  padding-top: 20px;
  position: relative;
}
/* 每個小盒子 */
.tab-container .tab-item {
  position: absolute;
  height: auto;
  border: 1px solid #ccc;
  box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
  background: white;
  /* 元素不能中斷顯示 */
  break-inside: avoid;
  text-align: center;
}
.tab-container .tab-item img {
  width: 100%;
  height: auto;
  display: block;
}
</style>

核心js部分

<script>
export default {
  name:'compList',
  props:{
    pbList:{
      type:Array,
      default:()=>{return []}
    }
  },
  data() {
    return {
    };
  },
  mounted() {
    this.$nextTick(()=>{
      this.waterFall("#tabContainer", ".tab-item"); //實現瀑布流
    })
  },
  methods: {
    waterFall(
        wrapIdName,
        contentIdName,
        columns = 5,
        columnGap = 20,
        rowGap = 20
    ) {
      // 獲得內容可用寬度(去除卷軸寬度)
      const wrapContentWidth =
          document.querySelector(wrapIdName).offsetWidth;

      // 間隔空白區域
      const whiteArea = (columns - 1) * columnGap;

      // 得到每列寬度(也即每項內容寬度)
      const contentWidth = parseInt((wrapContentWidth - whiteArea) / columns);

      // 得到內容項集合
      const contentList = document.querySelectorAll(contentIdName);

      // 成行內容項高度集合
      const lineConentHeightList = [];

      for (let i = 0; i < contentList.length; i++) {
        // 動態設定內容項寬度
        contentList[i].style.width = contentWidth + "px";

        // 獲取內容項高度
        const height = contentList[i].clientHeight;

        if (i < columns) {
          // 第一行按序佈局
          contentList[i].style.top = "0px";
          contentList[i].style.left = contentWidth * i + columnGap * i + "px";

          // 將行高push到陣列
          lineConentHeightList.push(height);
        } else {
          // 其他行
          // 獲取陣列最小的高度 和 對應索引
          let minHeight = Math.min(...lineConentHeightList);
          let index = lineConentHeightList.findIndex(
              (listH) => listH === minHeight
          );

          contentList[i].style.top = minHeight + rowGap +"px";
          contentList[i].style.left = (contentWidth + columnGap) * index + "px";

          // 修改最小列的高度 最小列的高度 = 當前自己的高度 + 拼接過來的高度 + 行間距
          lineConentHeightList[index] += height + rowGap;
        }
      }
    },
  },
};
</script>

這裡要給大家提個醒,在當外掛使用的時候,我們需要用this.$nextTick()來進行頁面初始化,因為方法成功的前提是要等頁面初始化載入完畢後才能進行獲取和計算

總體外掛程式碼為:

<template>
  <div class="tab-container" id="tabContainer">
    <div class="tab-item" v-for="(item, index) in pbList" :key="index">
      <img :src="item.url" />
    </div>
  </div>
</template>

<script>
export default {
  name:'compList',
  props:{
    pbList:{
      type:Array,
      default:()=>{return []}
    }
  },
  data() {
    return {
    };
  },
  mounted() {
    this.$nextTick(()=>{
      this.waterFall("#tabContainer", ".tab-item"); //實現瀑布流
    })
  },
  methods: {
    waterFall(
        wrapIdName,
        contentIdName,
        columns = 5,
        columnGap = 20,
        rowGap = 20
    ) {
      // 獲得內容可用寬度(去除卷軸寬度)
      const wrapContentWidth =
          document.querySelector(wrapIdName).offsetWidth;

      // 間隔空白區域
      const whiteArea = (columns - 1) * columnGap;

      // 得到每列寬度(也即每項內容寬度)
      const contentWidth = parseInt((wrapContentWidth - whiteArea) / columns);

      // 得到內容項集合
      const contentList = document.querySelectorAll(contentIdName);

      // 成行內容項高度集合
      const lineConentHeightList = [];

      for (let i = 0; i < contentList.length; i++) {
        // 動態設定內容項寬度
        contentList[i].style.width = contentWidth + "px";

        // 獲取內容項高度
        const height = contentList[i].clientHeight;

        if (i < columns) {
          // 第一行按序佈局
          contentList[i].style.top = "0px";
          contentList[i].style.left = contentWidth * i + columnGap * i + "px";

          // 將行高push到陣列
          lineConentHeightList.push(height);
        } else {
          // 其他行
          // 獲取陣列最小的高度 和 對應索引
          let minHeight = Math.min(...lineConentHeightList);
          let index = lineConentHeightList.findIndex(
              (listH) => listH === minHeight
          );

          contentList[i].style.top = minHeight + rowGap +"px";
          contentList[i].style.left = (contentWidth + columnGap) * index + "px";

          // 修改最小列的高度 最小列的高度 = 當前自己的高度 + 拼接過來的高度 + 行間距
          lineConentHeightList[index] += height + rowGap;
        }
      }
    },
  },
};
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
/* 最外層大盒子 */
.tab-container {
  padding-top: 20px;
  position: relative;
}
/* 每個小盒子 */
.tab-container .tab-item {
  position: absolute;
  height: auto;
  border: 1px solid #ccc;
  box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
  background: white;
  /* 元素不能中斷顯示 */
  break-inside: avoid;
  text-align: center;
}
.tab-container .tab-item img {
  width: 100%;
  height: auto;
  display: block;
}
</style>

五.外層使用和懶載入

在使用這個外掛的時候,有兩個問題,就是因為內層是position: absolute;定位,不會撐開外部的div,會導致外層盒模型不好佈局,還有就是頁面下拉懶載入,那要怎麼辦呢?

這裡我給出我的處理方法

 整體程式碼如下:

<template>
  <div>
    <div class="list-box" @scroll="scrollFun">
      <compList :pbList="pbList" ref="compList"></compList>
    </div>
  </div>
</template>

<script>
import compList from "@/pages/test/components/compList";
export default {
  name:'testList',
  components:{
    compList
  },
  data() {
    return {
      //瀑布流資料
      pbList: [
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        }
      ],
      addList:[
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fm%2Fpublic%2Fp2650049201.jpg&refer=http%3A%2F%2Fimg3.doubanio.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664935370&t=d4bf3e4d352c277a1bdebfcc8fda959f",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        },
        {
          url: "https://img1.baidu.com/it/u=2911909188,130959360&fm=253&fmt=auto&app=138&f=JPEG?w=440&h=641",
        }
      ],
      bottomMain:true
    };
  },
  methods:{
    scrollFun(e) {
      const  offsetHeight= e.target.offsetHeight
      const  scrollHeight= e.target.scrollHeight
      const  scrollTop= e.target.scrollTop
      if((scrollHeight - (offsetHeight+scrollTop)) < 10){
        if(this.bottomMain){
          this.bottomMain = false
          this.addListDataFun()
        }
      }
    },
    addListDataFun(){
      this.$Spin.show({
        render: (h) => {
          return h('div', [
            h('Icon', {
              'class': 'demo-spin-icon-load',
              props: {
                type: 'ios-loading',
                size: 18
              }
            }),
            h('div', '資料更新中...')
          ])
        }
      });
      setTimeout(() => {
        this.pbList = this.pbList.concat(this.addList)
        this.bottomMain = true
        this.$nextTick(()=>{
          this.$refs.compList.waterFall("#tabContainer", ".tab-item")
          this.$Spin.hide()
        })
      },1000)
    }
  }
};
</script>

<style scoped>
.list-box{
  position: relative;
  width: 100%;
  height: calc(100vh - 240px);
  background: white;
  padding: 20px 30px 20px 20px;
  margin-top: 20px;
  box-sizing: border-box;
  overflow: auto;
}
</style>

下拉的核心程式碼為:

    scrollFun(e) {
      const  offsetHeight= e.target.offsetHeight
      const  scrollHeight= e.target.scrollHeight
      const  scrollTop= e.target.scrollTop
      if((scrollHeight - (offsetHeight+scrollTop)) < 10){
        if(this.bottomMain){
          this.bottomMain = false
          this.addListDataFun()
        }
      }
    },
    addListDataFun(){
      this.$Spin.show({
        render: (h) => {
          return h('div', [
            h('Icon', {
              'class': 'demo-spin-icon-load',
              props: {
                type: 'ios-loading',
                size: 18
              }
            }),
            h('div', '資料更新中...')
          ])
        }
      });
      setTimeout(() => {
        this.pbList = this.pbList.concat(this.addList)
        this.bottomMain = true
        this.$nextTick(()=>{
          this.$refs.compList.waterFall("#tabContainer", ".tab-item")
          this.$Spin.hide()
        })
      },1000)
    }

這裡使用的是iView-UI的全域性載入事件,如果你要用其他的UI框架,也可以自行修改

到這裡,所有的思路就結束了

如果對您有所幫助,歡迎您點個關注,我會定時更新技術檔案,大家一起討論學習,一起進步。