vue瀑布流

发布于 2024-07-22  647 次阅读


<template>
  <div ref="main" class="main" id="main" :style="{ height: maxH + 'px' }">
    <!-- <div v-if="loading" class="loading">Loading...</div> -->
    <div
      class="item"
      :class="[moveMode, styleArr[i] && styleArr[i].showClass]"
      v-for="(item, i) in list"
      :key="i"
      :style="styleArr[i]"
      :ref="'item' + i"
    >
      <slot
        :state="(styleArr[i] && styleArr[i].state) || 'loading'"
        :data="item"
        :index="i"
      ></slot>
    </div>
    <iframe v-if="autoResize" ref="autoExpand" class="autoExpand"></iframe>
  </div>
</template>
<script>
let loaderCache = {};
let loaderImg = new Map();
let time = null;
export default {
  props: {
    list: {
      type: Array,
      default: () => [],
    },
    imgKey: {
      type: String,
      default: "src",
    },
    // 列数
    col: {
      type: Number,
      default: 0,
    },
    // 列宽,和列数只能生效其一,列数优先
    colWidth: {
      type: Number,
      default: 0,
    },
    // 是否根据容器宽度变化重新计算
    autoResize: {
      type: Boolean,
      default: true,
    },
    // 是否填充满容器
    fillBox: {
      type: Boolean,
      default: false,
    },
    // 移动模式transform、convention
    moveMode: {
      type: String,
      default: "transform",
    },
    // 自动重绘时的过渡时间
    moveTransitionDuration: {
      type: Number,
      default: 0.4,
    },
    // 元素之间的间隔
    gap: {
      type: Number,
      default: 10,
    },
  },
  data() {
    return {
      styleArr: [], //数据对应的样式
      colW: 0, //列宽
      maxH: 599, //最高的列
      mainW: 0, //容器宽度
      _col: 0, //列缓存
      __col: 0, //内部维护列数
      batchCB: null, //批处理Promise
      onRender: null, //渲染完毕回调函数
      loading: false,
    };
  },
  computed: {
    //窗口改变节流函数
    resizeDebounce() {
      return this.isTransition ? this.debounce(this.resize, 100) : this.resize;
    },
    //默认过渡的条件规则
    isTransition() {
      return this.autoResize && this.col < 1;
    },
  },
  mounted() {
    this.mainW = this.getWidth();
    this.init();
    this.polling();
  },
  watch: {
    ["list.length"]: {
      deep: false,
      async handler(newV, oldV) {
        if (newV > oldV) {
          this.loading = true; // 设置loading状态为true
          await this.nextTick();
          this.batchCB = this.initItem(oldV);
        }
      },
    },
    autoResize: {
      handler(newV) {
        newV &&
          setTimeout(() => {
            this.refs.autoExpand.contentWindow.onresize = (e) => {
              this.resizeDebounce();
            };
          });
      },
      immediate: true,
    },
  },
  methods: {
    async repaints(start = 0, duration) {
      await this.nextTick();
      this.mainW = this.getWidth();
      this.calcCol();
      this.calcXY(start, "repaints", duration);
    },
    init(start = 0) {
      this.calcCol();
      this.batchCB = this.initItem(start);
    },
    getWidth() {
      return (this.refs.main.getBoundingClientRect() || {}).width || 0;
    },
    async resize(start = 0) {
      if (!this.refs.main) return;
      let width = this.getWidth();
      if (width == this.mainW) return;
      this.mainW = width;
      this.calcCol();
      if (this.autoResize) {
        if (this.fillBox || this.col || this.__col != this._col) {
          this.calcXY(start, "resize");
        }
      }
    },
    calcCol() {
      let col = 3;
      if (this.col) {
        col = this.col;
        this.colW = (this.mainW - this.gap * (col - 1)) / col; // 修改列宽计算,考虑间隔
      } else if (this.colWidth) {
        col = parseInt(this.mainW / (this.colWidth + this.gap)) || 1;
        if (this.mainW % (this.colWidth + this.gap) && this.fillBox) {
          this.colW = (this.mainW - this.gap * (col - 1)) / col;
        } else {
          this.colW = this.colWidth;
        }
      } else {
        this.colW = (this.mainW - this.gap * (col - 1)) / col;
      }
      this.__col = col;
      return col;
    },
    polling() {
      clearInterval(time);
      time = setInterval(() => {
        for (let item of loaderImg) {
          let key = item[0];
          item = item[1];
          if (item.img.height>0 || item.img.complete) {
            item.cb(item.img);
            loaderImg.delete(key);
          } else {
            return;
          }
        }
      }, 300);
    },
    initItem(start = 0) {
      let list = this.list.slice(start);
      let loadNum = 0;
      list.forEach((e, i) => {
        let _i = i + start;
        if (!this.styleArr[_i]) {
          this.styleArr[_i] = {};
        }
        this.styleArr[_i].width = this.colW + "px";
        this.loader(
          e[this.imgKey],
          () => {
            loadNum++;
            this.styleArr[_i].complete = true;
            this.styleArr[_i].state = "complete";
            this.set(this.styleArr, _i, this.styleArr[_i]);
            if (loadNum != list.length) return;
            this.waitRender(start);
          },
          this.getColDom(_i).getElementsByClassName("waterfall-img")[0],
          i
        );
      });
    },
    waitRender(start) {
      for (var i = 0; i < this.styleArr.length; i++) {
        if (i < start) {
          if (!this.styleArr[i] || !this.styleArr[i].complete) return;
        }
      }
      this.calcXY(start);
    },
    async calcXY(index = 0, cause = "data", duration) {
      duration = isNaN(duration) ? this.moveTransitionDuration : duration;
      let idx = index;
      this._col = this.__col;
      for (let i = index; i < this.styleArr.length; i++) {
        if (!this.styleArr[i] || !this.styleArr[i].complete) continue;
        this.styleArr[i].width = this.colW + "px";
        if (this.styleArr[i].showClass) {
          this.styleArr[i]["transition-duration"] = `{duration}s`;
        }
      }
      await this.nextTick();
      for (let i = idx; i < this.styleArr.length; i++) {
        if (!this.styleArr[i] || !this.styleArr[i].complete) return;
        const e = this.getColDom(i);
        if (!e) return;
        // 获取当前元素高度
        this.styleArr[i].height = e.offsetHeight;
        let xy = this.getMinCol(i);
        const curTop = xy.curTop + (index < this.__col ? 0 : this.gap),
          curCol = xy.curCol,
          curBT = curTop + this.styleArr[i].height + this.gap,
          maxH = xy.maxH > curBT ? xy.maxH : curBT;
        if (this.moveMode == "convention") {
          this.styleArr[i].left = `{curCol * (this.colW + this.gap)}px`;
          this.styleArr[i].top = `{curTop}px`;
        } else {
          this.styleArr[i].transform = `translate3d({
            curCol * (this.colW + this.gap)
          }px,{curTop}px ,0)`;
        }
        this.maxH = maxH;
        this.styleArr[i].bottomTop = curBT;
        this.styleArr[i].col = curCol;
        this.styleArr[i].showClass = "show";
        this.styleArr[i].state = "show";
      }
      this.forceUpdate();
      this.loading = false; // 设置loading状态为false
      await this.nextTick();
      this.onRender &&
        this.onRender({
          cause: cause,
          start: index,
        });
    },
    getMinCol(curIndex) {
      if (!curIndex) {
        return {
          curCol: 0,
          curTop: 0,
          maxH: 0,
        };
      }
      let curCol = 0,
        curTop = 0;
      let set = {};
      for (let index = curIndex - 1; index >= 0; index--) {
        const item = this.styleArr[index];
        if (item && !set[item.col]) {
          set[item.col] = item;
        }
        if (Object.keys(set).length == this.__col) {
          break;
        }
      }
      let order = Object.values(set).sort((a, b) => a.bottomTop - b.bottomTop);
      if (curIndex < this.__col) {
        curCol = curIndex;
        curTop = 0;
      } else {
        curCol = order[0].col;
        curTop = order[0].bottomTop;
      }
      return {
        curCol,
        curTop,
        maxH: order[order.length - 1].bottomTop,
      };
    },
    loader(src, cb, imgDom = {}, i) {
      if (imgDom.height > 0) {
        return cb(imgDom);
      }
      if (!src && !imgDom.src) return cb();
      if (imgDom.src) {
        src = imgDom.src;
      }
      let img = loaderCache[src] && loaderCache[src].img;
      if (img) {
        if (img.complete || img.height > 0) return cb(img);
      } else {
        if (imgDom.src) {
          img = imgDom;
        } else {
          img = new Image();
          img.src = src;
        }
        if (img.complete || img.height > 0) return cb(img);
        loaderCache[src] = {
          img: img,
          cbs: [],
          i,
        };
        loaderImg.set(img.src, {
          img,
          cb: () => {
            loaderCache[src].cbs.forEach((cb) => cb());
            loaderCache[src].cbs.length = 0;
          },
        });
      }
      loaderCache[src].cbs.push(cb);
    },
    getColDom(i) {
      return this.$refs["item" + i][0];
    },
    debounce(func, wait) {
      let timer;
      return function () {
        let context = this;
        let args = arguments;
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
          func.apply(this, args);
        }, wait);
      };
    },
  },
};
</script>

<style scoped>
.main {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  transition-property: height;
}
.main > .item {
  position: absolute;
  z-index: 1;
  opacity: 0;
  box-sizing: border-box;
  transform: translate3d(0, 0, 0);
}
.convention {
  transition-property: top, left;
}
.transform {
  transition-property: transform;
}
.show {
  opacity: 1 !important;
}
.main > .col {
  float: left;
}
.col > .item {
  width: 100%;
}
.autoExpand {
  opacity: 0;
  position: absolute;
  left: -100%;
  top: -100%;
  width: 100%;
  height: 100%;
  visibility: hidden;
  pointer-events: none;
}
</style>

使用

// 参数
//是否根据容器尺寸自动计算重绘
      autoResize: true,
      //是否始终填满容器
      fillBox: true,
      //列宽-有指定列数则此属性失效
      colWidth: window.innerWidth / 5,
      //列数
      col: 6,

<div class="tab-container" id="tabContainer" @scroll="handleScroll">
          <waterfall
            :col="col"
            :autoResize="autoResize"
            :moveTransitionDuration="0.4"
            :fillBox="fillBox"
            :col-width="colWidth"
            :list="meterialArray"
            ref="waterfall"
            imgKey="src"
          >
            <!-- 两种图片绑定模式
        1.指定图片的Key( imgKey="src")
        2.在img标签上加class( class="waterfall-img") -->
            <!-- img标签如果设置宽高会渲染的更快 -->
            <div
              class="waterfall-item"
              :class="{ bounceIn: item.state == 'show' }"
              slot-scope="item"
              @click="detailsClick(item.data, item.index)"
            >
              <div class="waterfall_box">
                <img
                  v-if="item.data.src"
                  style="width: 100%"
                  class="waterfall-img"
                  :src="item.data.src"
                />
                <div class="details_info" v-if="item.data.copywritingMaterial">
                  <div class="details_text">
                    {{ item.data.copywritingMaterial }}
                  </div>
                </div>
              </div>

              <div class="list_box" v-if="item.data.src">
                <img
                  v-if="item.data.type == 1"
                  src="@/assets/index/picture_icon.png"
                  alt=""
                />
                <img v-else src="@/assets/index/video_icon.png" alt="" />
                <div class="list_info">
                  <template v-if="item.data.type == 1">
                    {{ item.data.width }}*{{ item.data.height }}
                  </template>
                  <template v-else>
                    {{ item.data.fileSizeStr }} {{ item.data.durationStr }}
                  </template>
                </div>
              </div>
              <div class="material_list">
                <div class="title_box">
                  <div class="title_left">
                    {{ item.data.name }}
                    . {{ getFileExtension(item.data.baseMaterial) }}
                  </div>
                  <div class="title_right">
                    <img src="@/assets/index/more_icon.png" alt="" />
                  </div>
                </div>
                <div class="tip_box">
                  {{ item.data.applicationTypeName }}-{{
                    item.data.advertisingPositionTypeName
                  }}
                </div>
              </div>
            </div>
          </waterfall>
        </div>

原地址:https://github.com/1977474741/vue-waterfall-rapid


只会写bug的bugming