water-fall.vue 7.87 KB
<template>
  <div class="wf-list" :style="{'height': listHeight + 'px'}">
    <div
      v-for="i in viewList"
      :key="`${i._temporary ? '_' : ''}${i.articleId}`"
      class="wf-item"
      :class="{'wf-item-default': i._default, 'wf-item-temp': i._temporary}"
      :style="`width: ${100 / cols}%;transform: translate(${i.left}px, ${i.top}px)`">
      <div class="wf-item-mid">
        <router-link :to="`/article/${i.articleId}`">
          <div class="layer-image" :style="{'height': i.coverHeight + 'px'}">
            <ImageFormat v-if="!i._temporary" :src="i[srcKey]" :width="coverImageWidth" :height="i.coverHeight"></ImageFormat>
          </div>
          <div class="description">{{i.content}}</div>
        </router-link>

        <div class="attribution">
          <router-link :to="`/article/${i.articleId}`" class="auther">
            <span class="avatar">
              <WidgetAvatar v-if="!i._temporary" :src="i.authorHeadIco" :width="70" :height="70"></WidgetAvatar>
            </span>
            <span class="name">{{i.authorName}}</span>
          </router-link>

          <div class="fav">
            <WidgetFav :articleId="i.articleId" :num="i.praiseCount" :option="favOption"></WidgetFav>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import {assign, get} from 'lodash';

export default {
  data() {
    return {
      viewList: [{
        _default: true
      }],
      coverImageWidth: 0,
      colWidthPer: 0,
      calcIndex: 0,
      loadedIndex: 0,
      colsHeight: [],
      favOption: {
        iconFontSize: 26,
        textAlign: 'normal'
      }
    }
  },
  props: {
    pos: {
      type: Number,
      default: 0
    },
    list: {
      type: Array,
      default: []
    },
    srcKey: {
      type: String,
      default: 'coverImage'
    },
    cols: {
      type: Number,
      default: 2
    },
    space: {
      type: Number,
      default: 14
    }
  },
  mounted() {
    this.$on('calced', (nlist) => {
      this.viewList = this.viewList.concat(nlist);
      this.$nextTick(() => {
        this.calcLayout();
      })
    });

    this.reset();
    this.clacCoverSize();
  },
  watch: {
    pos() {
      this.timer && clearTimeout(this.timer);
      this.timer = setTimeout(this.resetViewList, 0);
    },
    list(newList, oldList) {
      if (oldList.length > newList.length ||
        get(oldList, '[0].articleId') !== get(newList, '[0].articleId')) {
        this.reset();
      }

      this.clacCoverSize();
    }
  },
  computed: {
    listHeight() {
      return Math.max.apply(null, this.colsHeight);
    },
    colWidth() {
      return this.$el.offsetWidth / this.cols;
    }
  },
  methods: {
    clacCoverSize() {
      let nlist = [];

      for (let i = this.calcIndex; i < this.list.length; i++) {
        let item = this.list[i];

        item.coverHeight = Math.floor(item.imageHeight / item.imageWidth * this.coverImageWidth);

        nlist.push(assign({_temporary: true}, item));
      };

      this.$emit('calced', nlist);
    },
    calcLayout() {
      let $item = this.$el.getElementsByClassName('wf-item-temp');

      if (!$item || !$item.length) {
        return;
      }

      if (!this.loadedIndex || !this.colsHeight) {
        this.colsHeight = [];
      }

      for (let i = this.loadedIndex; i < this.list.length; i++) {
        let $elem = $item[i - this.loadedIndex];

        if (!$elem) {
          return;
        }

        let height = $elem.offsetHeight;
        let top, left, leftPer;
        let item = this.list[i];

        item.height = height;

        if (i < this.cols) {
          this.colsHeight[i] = height;
          top = 0
          left = i * this.colWidth;
          leftPer = i * this.colWidthPer;
        } else {
          let minHeight = Math.min.apply(null, this.colsHeight);
          let minIndex = this.colsHeight.indexOf(minHeight);

          top = minHeight;
          left = minIndex * this.colWidth;
          leftPer = minIndex * this.colWidthPer;

          this.colsHeight[minIndex] = minHeight + height;
        }

        item.left = left;
        item.leftPer = leftPer;
        item.top = top;
        item.bottom = top + height;
      }

      this.loadedIndex = this.list.length;

      this.resetViewList();
    },
    setImgWidth() {
      let imgWidth = this.$el.offsetWidth / this.cols;
      let $item = this.$el.getElementsByClassName('wf-item');

      if ($item && $item.length) {
        let _w = $item[0].offsetWidth;
        let $img = $item[0].getElementsByClassName('layer-image');

        (_w > 0) && (imgWidth = _w);

        if ($img && $img.length) {
          _w = $img[0].offsetWidth;
          (_w > 0) && (imgWidth = _w);
        }
      }

      this.coverImageWidth = Math.floor(imgWidth);
    },
    reset() {
      this.offsetTop = this.$el.offsetTop;
      this.clientHeight = document.body.clientHeight;
      this.colWidthPer = 100 / this.cols;
      this.loadedIndex = 0;
      this.calcIndex = 0;

      this.viewList = [];
      this.lastPos = 0;
      this.viewIndex = 0;

      this.setImgWidth();
    },
    resetViewList() {
      this.viewIndex = this.viewIndex || {};

      let i, step;
      let startPos = this.pos - this.clientHeight * 2;
      let endPos = this.pos + this.clientHeight * 2;
      let list = [];
      let indexArr = [];

      if (this.pos < this.lastPos) {
        i = Math.min.apply(null, [(this.viewIndex['end'] || 0), this.list.length - 1]);
        step = -1;
      } else {
        i = Math.max.apply(null, [(this.viewIndex['start'] || 0), 0]);
        step = 1;
      }

      let loop = true;

      while (loop)
      {
        if (i < 0 || i >= this.list.length) {
          loop = false;
          continue;
        }

        let item = this.list[i];

        if (item) {
          if (item.top > endPos - this.offsetTop) {
            if (step > 0) {
              loop = false;
            }
          } else if (item.bottom < startPos - this.offsetTop) {
            if (step < 0) {
              loop = false;
            }
          } else {
            indexArr.push(i);
            list.push(item);
          }
        }

        i += step;
      }

      if (!indexArr.length) {
        return;
      }

      indexArr = indexArr.sort(function(a, b) {
        return a - b;
      });

      let viewIndex = {
        start: indexArr[0],
        end: indexArr[indexArr.length - 1]
      };

      if (this.viewIndex.start !== viewIndex.start || this.viewIndex.end !== viewIndex.end) {
        this.viewList = list;
        this.lastPos = this.pos;
        this.viewIndex = viewIndex;
      }
    }
  }
};
</script>

<style lang="css">
  .wf-list {
    margin: 0 20px;
    font-size: 0;
    position: relative;
  }

  .wf-item {
    padding: 10px;
    font-size: 24px;
    overflow: hidden;
    position: absolute;

    .wf-item-mid {
      border-radius: 4px;
      box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
      overflow: hidden;
    }

    .layer-image {
      background-color: #f4f4f4;
      min-height: 100px;

      > img {
        width: 100%;
        height: 100%;
        display: block;
      }
    }

    .description {
      line-height: 1.5;
      padding: 10px 20px;
      word-break: break-all;
      text-overflow: -o-ellipsis-lastline;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      -webkit-box-orient: vertical;
    }

    .attribution {
      display: flex;
      justify-content: space-between;
      padding: 20px;
    }

    .avatar {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      display: inline-block;
      vertical-align: middle;
      overflow: hidden;

      > img {
        width: 100%;
        height: 100%;
      }
    }

    .name {
      display: inline-block;
      vertical-align: middle;
    }

    .fav {
      line-height: 60px;
    }
  }

  .wf-item-default,
  .wf-item-temp {
    opacity: 0;
    margin-left: -100%;
  }
</style>