Vue

vue继承vant实现支持下拉刷新上拉加载更多的瀑布流组件

Elysian
2021-12-28 / 0 评论 / 293 阅读 / 正在检测是否收录...

组件背景

  1. 需要实现瀑布流效果、使用的前端框架是基于vant的。
  2. 瀑布流内容多种形式,除了图文还有标签形式。
  3. 项目需要SSR。
  4. 片寻网络,找到一个不错的瀑布流组件,基于它修改了部分内容(优化了数据获取方式,传递列表支持ssr,传递promise参数支持下拉刷新和上拉加载更多,修改内容部分,使用slot支持组件外部自定义内容格式)

实现效果

youdu图片20211228102431.png
youdu图片20211228102301.png
youdu图片20211228102449.png

组件源码:

<template>
  <div class="flow-box">
    <div class="type-box" v-if="typeList && typeList.length">
      <ul class="type-list">
        <li
          @click="changeType(index)"
          v-for="(item, index) in typeList"
          :class="['type-item', typeIndex == index ? 'type-item-on' : '']"
          :key="index"
        >
          <div class="text">{{ item.name }}</div>

          <div class="line"></div>
        </li>
      </ul>
    </div>
    <van-pull-refresh
      v-model="isLoading"
      @refresh="onRefresh"
      style="min-height: 100vh"
      :success-duration="800"
      success-text="加载成功"
      loading-text="加载中..."
    >
      <van-list
        v-if="haveData == 2"
        v-model="loading"
        :finished="finished"
        finished-text="没有更多了"
        @load="onBottomLoad"
        :offset="150"
        :immediate-check="false"
      >
        <div class="data-list-box" id="data-list-box">
          <div
            class="data-item"
            v-for="(item, index) in dataList"
            :style="{ width: boxWidth + 'px' }"
            :key="index"
          >
            <slot :item="item" :index="index"></slot>
          </div>
        </div>
      </van-list>
    </van-pull-refresh>
  </div>
</template>
<script>
export default {
  props: ["dataSource", "promisefunc"],
  data() {
    return {
      typeList: [
        //分类列表
        // { name: "全部" },
        // { name: "手机数码" },
        // { name: "家用电器" },
        // { name: "酒水饮料" },
        // { name: "钟表珠宝" },
        // { name: "美妆护肤" },
        // { name: "运动户外" },
        // { name: "汽车生活" },
      ],
      typeIndex: 0, //分类索引
      dataList: this.dataSource || [], //列表数据
      haveData: 0, //是否有数据,1=无,2=有,0=页面还未初始化
      pageIndex: 1, //页码
      pageSize: 8, //每页加载数据数量
      isLoading: false, //下拉刷新进行中,请求开始true, 请求完成false,用于下拉刷新组件van-pull-refresh
      loading: false, //上拉加载更多中,上拉触底时自动变成true, 请求完成设置为false, 用于列表组件van-list
      finished: false, //上拉加载是否加载完最后一页数,用于组件van-list
      itemCount: 0, //上一次加载完成后的瀑布流item个数
      lastRowHeights: [0, 0], //最后一行标签的顶部间距+高度,2列

      boxMargin: 15, //每个item之间的边距
      boxWidth: 165, //每个item宽度
    };
  },
  created() {
    if (process.server) {
      return;
    }
    this.$toast.loading({
      message: "加载中...",
      duration: 0,
    });
    //当前瀑布流设置为两列,计算瀑布流每个item和图片的宽度
    //console.log("load", window.innerWidth);
    let screenWidth = window.innerWidth || 375; //屏幕宽度
    this.boxWidth = (screenWidth - this.boxMargin * 3) / 2; //每个item的宽度
    if (this.dataSource == null || !this.dataSource.length) {
      this.onRefresh(); //刷新数据
    } else {
      this.pageIndex = 1; //重置第一页
      this.finished = false; //上拉加载"所有数据已经完成"标识 重置为false
      this.dataList = [];
      this.loadImagesHeight(this.dataSource);
    }
  },
  methods: {
    changeType(index) {
      //切换类型
      if (this.typeIndex == index) return;

      this.$toast.loading({
        message: "加载中...",
        duration: 0,
      });
      this.typeIndex = index;
      this.onRefresh();
    },
    onRefresh() {
      //下拉刷新
      // if (this.isLoading) return; //还在请求中,返回

      this.pageIndex = 1; //重置第一页
      this.finished = false; //上拉加载"所有数据已经完成"标识 重置为false
      //接口请求
      this.getDataList();
    },
    onBottomLoad() {
      //上拉加载更多
      if (this.finished) return; //说明所有数据已经加载完毕,返回
      this.getDataList(); //下一页数据请求中
    },
    //数据请求
    getDataList() {
      this.promisefunc({ page: this.pageIndex })
        .then((res) => {
          let list = res ? res : [];
          if (list.length > 0) {
            //从list中取pageSize条数据出来
            var tempList = [];
            for (let i = 0; i < this.pageSize; i++) {
              if (list.length > 0) {
                let tempIndex = parseInt(Math.random() * 1000) % list.length;
                tempList.push(list[tempIndex]);
                list.splice(tempIndex, 1);
              }
            }
            this.loadImagesHeight(tempList); //模拟预加载图片,获取图片高度
          } else {
            this.loadImagesHeight(list); //处理数据
          }
        })
        .catch((res) => {
          //console.log("..fail: ", res);
          this.$toast.clear();
          this.isLoading = false; //下拉刷新请求完成
          this.loading = false; //上拉加载更多请求完成
        });
    },
    loadImagesHeight(list) {
      var count = 0; //用来计数,表示是否所有图片高度已经获取
      list.forEach((item, index) => {
        //创建图片对象,加载图片,计算图片高度
        var img = new Image();
        img.src = item.cover;
        img.onload = img.onerror = (e) => {
          count++;
          if (e.type == "load") {
            //图片加载成功
            //计算图片缩放后的高度:图片原高度/原宽度 = 缩放后高度/缩放后宽度
            list[index].imgHeight = Math.round(
              (img.height * this.boxWidth) / img.width
            );
            // console.log('index: ', index, ', load suc, imgHeiht: ', list[index].imgHeight);
          } else {
            //图片加载失败,给一个默认高度50
            list[index].imgHeight = 50;
            //console.log("index: ", index, ", 加载报错:", e);
          }

          //加载完成最后一个图片高度,开始下一步数据处理
          if (count == list.length) {
            this.resolveDataList(list);
          }
        };
      });
    },
    resolveDataList(list) {
      //处理数据
      //下拉刷新,清空原数据
      if (this.pageIndex <= 1) {
        this.itemCount = 0;
        this.dataList = [];
        this.lastRowHeights = [0, 0]; //存储每列的最后一行高度清0
      }

      if (list.length >= this.pageSize) {
        this.pageIndex++; //还有下一页
      } else {
        this.finished = true; //当前tab类型下所有数据已经加载完成
      }

      //合并新老两个数组数据
      this.dataList = [...this.dataList, ...list];
      //判断页面是否有数据
      this.haveData = this.dataList.length > 0 ? 2 : 1;
      //   this.isLoading = false; //下拉刷新请求完成
      //   this.loading = false; //上拉加载更多请求完成

      //console.log("...datalist: ", this.dataList);
      //console.log("...this.isLoading: ", this.isLoading);

      this.$nextTick(() => {
        setTimeout(() => {
          //渲染完成,计算每个item宽高,设置标签坐标定位
          this.setItemElementPosition();
          this.isLoading = false; //下拉刷新请求完成
          this.loading = false; //上拉加载更多请求完成
        }, 100);
      });
    },
    //获取每个item标签高度,设置item的定位
    setItemElementPosition() {
      if (process.server) {
        return;
      }
      let parentEle = document.getElementById("data-list-box");
      let boxEles = parentEle.getElementsByClassName("data-item");

      for (let i = this.itemCount; i < boxEles.length; i++) {
        let tempEle = boxEles[i];
        //上一个标签最小高度的列索引
        let curColIndex = this.getMinHeightIndex(this.lastRowHeights);
        let boxTop = this.lastRowHeights[curColIndex] + this.boxMargin;
        let boxLeft =
          curColIndex * (this.boxWidth + this.boxMargin) + this.boxMargin;
        tempEle.style.left = boxLeft + "px";
        tempEle.style.top = boxTop + "px";
        this.lastRowHeights[curColIndex] = boxTop + tempEle.offsetHeight;

        // console.log('i = ', i, ', boxTop: ', boxTop, ', eleHeight: ', tempEle.offsetHeight);
      }

      this.itemCount = boxEles.length;

      //修改父级标签的高度
      let maxHeight = Math.max.apply(null, this.lastRowHeights);
      parentEle.style.height = maxHeight + "px";

      this.$toast.clear();
      //console.log("...boxEles: ", boxEles.length, ", maxH: ", maxHeight);
    },
    //获取数组中最小值的索引
    getMinHeightIndex(arr) {
      var minHeight = Math.min.apply(null, arr);
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] == minHeight) {
          return i;
        }
      }
    },
  },
};
</script>
<style lang="scss" scoped>
.flow-box {
  background-color: #ffffff;
  width: 100vw;
  height: 100vh;
}
.flow-box .type-box {
  width: 100%;
  height: 40px;
  position: fixed;
  top: 0;
  font-size: 14px;
  background: #fff;
  color: rgba(0, 0, 0, 0.4);
  z-index: 99;
  border-bottom: 0.5px solid #dddddd;
  padding: 0 5px;
}
.type-box .type-list {
  white-space: nowrap;
  overflow-x: scroll;
}
.type-list .type-item {
  display: inline-block;
  padding: 0 12.5px;
  height: 40px;
  text-align: center;
}
.type-list .type-item .text {
  line-height: 37.5px;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.4);
  margin: 0;
}
.type-list .type-item .line {
  background-color: #ffffff;
}
.type-list .type-item-on .text {
  font-size: 16px;
  color: #f0142d;
}
.type-list .type-item-on .line {
  width: 19px;
  height: 2px;
  margin: 0 auto;
  background-color: #f0142d;
  border-radius: 2px;
}
/* 隐藏滚动条 */
.type-list::webkit-scrollbar {
  display: none;
}

/* 列表数据样式 */
@keyframes data-item-ani {
  0% {
    transform: scale(0.5);
  }
  100% {
    transform: scale(1);
  }
}
.flow-box .data-list-box {
  position: relative;
  height: 100vh;
  /* margin-top: 40px; */
}
.data-list-box .data-item {
  height: auto;
  position: absolute;
  background-color: #ffffff;
  /* box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.5); */
  left: -1000px;
  animation: data-item-ani 0.4s;
  transition: left 0.6s, top 0.6s;
  transition-delay: 0.1s;
}
</style>

使用方法

<template>
  <Waterfall
    :dataSource="anlilist"
    :promisefunc="this.requestFunc || this.$api.requestZhuangxiuCase"
  >
    <template slot-scope="child">
      <div
        class="hot-list"
        v-if="child.index === 5 && hotlist && hotlist.length"
      >
        <lookall :list="hotlist" />
      </div>
      <div class="item-box">
        <a href="/shop/" class="a">
          <img
            :src="child.item.cover"
            class="data-cover"
            :alt="child.item.title"
            :style="{ width: '100%', height: child.item.imgHeight + 'px' }"
          />
          <p class="p">{{ child.item.sign }}</p>
          <h3 class="h3">{{ child.item.title }}</h3>
          <i class="icon icon-an"></i>
        </a>
        <div class="news-list-info cf">
          <a href="/shop/" class="source"
            ><img :src="child.item.iconimg" alt="" />{{
              child.item.icontitle
            }}</a
          ><a href="" class="btn-zan">{{ child.item.count }}赞</a>
        </div>
      </div>
    </template>
  </Waterfall>
</template>

<script>
import lookall from "~/components/jiajuhao/lookall.vue";

export default {
  components: {
    lookall,
  },
  props: ["dataSource", "requestFunc", "hotlist"],
  data() {
    return {
      anlilist: this.dataSource || [],
    };
  },
};
</script>

<style lang="scss">
/* 全局样式 */
.hot-list {
  border-radius: 0.1rem;
  border: 1px solid #e5e5e5;
  overflow: hidden;
  background: #fff;
  padding: 0.35rem 0.2rem;
  margin-bottom: 0.25rem;
  .news-block {
    padding: 0;
    border: 0;
    h2 {
      border: 0;
      margin: 0;
    }
    ul {
      li {
        width: 100%;
      }
    }
  }
}
</style>
<style lang="scss" scoped>
.item-box {
  border-radius: 0.1rem;
  border: 1px solid #e5e5e5;
  overflow: hidden;
  background: #fff;
  .a {
    position: relative;
    display: block;
    height: auto;
    padding-bottom: 0.2rem;
  }
  .data-cover {
    margin-bottom: 0.1rem;
    display: block;
  }
  .p {
    padding: 0.05rem 0.2rem 0.1rem;
    font-size: 0.2rem;
    color: #999;
  }
  .h3 {
    padding: 0 0 0 0.2rem;
    font-size: 0.26rem;
  }
  .icon-an {
    width: 0.59rem;
    height: 0.33rem;
    background-position: 0 -2.5rem;
    position: absolute;
    top: 0;
    left: 0;
  }
  .data-name {
    font-size: 14px;
    padding: 5px 10px;
    text-align: left;
  }
  .news-list-info {
    margin: 0 0.1rem 0.1rem;
  }
}
</style>

组件原地址转至:https://www.cnblogs.com/tandaxia/p/12189301.html

0

评论

博主关闭了所有页面的评论