vue2 无限滚动

360 阅读1分钟

介绍

瀑布流滚动加载,用于展示长列表,当列表即将滚动到底部时,会触发事件并加载更多列表项。

scroll 组件通过 loading 和 finished 两个变量控制加载状态,当组件滚动到底部时,会触发 load 事件并将 loading 设置成 true。此时可以发起异步操作并更新数据,数据更新完毕后,将 loading 设置成 false 即可。若数据已全部加载完毕,则直接将 finished 设置成 true 即可。

<template>
  <div class="infinite_scroll">
    <slot></slot>
    <div ref="placeholder" class="list_placeholder"></div>
    <div v-if="loading" class="list_loading">
      <div class="loading">
        <span class="loading_icon"></span>
        <span class="loading_text">{{ loadingText }}</span>
      </div>
    </div>
    <div v-if="finished" class="list_finished">{{ finishedText }}</div>
    <div v-if="error" class="list_error" @click="clickErrorText">
      {{ errorText }}
    </div>
  </div>
</template>

<script>
const overflowScrollReg = /scroll|auto|overlay/i;
function getScroller(el, root = window) {
  let node = el;

  while (
    node &&
    node.tagName !== "HTML" &&
    node.tagName !== "BODY" &&
    node.nodeType === 1 &&
    node !== root
  ) {
    const { overflowY } = window.getComputedStyle(node);
    if (overflowScrollReg.test(overflowY)) {
      return node;
    }
    // 从 infinite_scroll 一层一层网上找
    node = node.parentNode;
  }
  return root;
}

export default {
  props: {
    loading: Boolean,
    finished: Boolean,
    error: Boolean,
    immediateCheck: {
      type: Boolean,
      default: true,
    },
    offset: {
      type: [Number, String],
      default: 300,
    },
  },
  data() {
    return {
      direction: "down",

      scroller: "",

      loadingText: "加载中",

      finishedText: "没有更多了",

      errorText: "请求失败,点击重新加载",
    };
  },
  mounted() {
    this.scroller = getScroller(this.$el);
    // 添加滚动事件监听器
    this.scroller.addEventListener("scroll", this.check);
    if (this.immediateCheck) {
      this.check();
    }
  },
  beforeUnmount() {
    // 移除滚动事件监听器
    this.scroller.removeEventListener("scroll", this.check);
  },
  watch: {
    loading: "check",
    finished: "check",
  },
  methods: {
    check() {
      this.$nextTick(() => {
        if (this.loading || this.finished || this.error) {
          return;
        }

        const { scroller, offset, direction } = this;

        let scrollerRect;
        if (scroller.getBoundingClientRect) {
          scrollerRect = this.scroller.getBoundingClientRect();
        } else {
          scrollerRect = {
            top: 0,
            bottom: scroller.innerHeight,
          };
        }
        let isReachEdge = false;
        const placeholderRect = this.$refs.placeholder.getBoundingClientRect();
        isReachEdge = placeholderRect.bottom - scrollerRect.bottom <= offset;
        if (isReachEdge) {
          this.$emit("update:loading", true);
          this.$emit("load");
        }
      });
    },

    genLoading() {
      if (this.loading && !this.finished) {
        return "加载中";
      }
    },

    genFinishedText() {
      if (this.finished) {
        return "加载完毕";
      }
    },

    genErrorText() {
      if (this.error) {
        return "获取失败";
      }
    },

    clickErrorText() {
      this.$emit("update:error", false);
      this.$emit("update:loading", false);
      this.check();
    },
  },
};
</script>

<style scoped>
.list_placeholder {
  height: 0;
  visibility: hidden;
}

.list_loading,
.list_finished,
.list_error {
  padding: 10px 16px;
  font-size: 14px;
  text-align: center;
  color: #999;
  background-color: #fff;
}

.list_loading .loading {
  display: flex;
  align-items: center;
  justify-content: center;
}

.list_loading .loading_icon {
  /* 设置 loading 图标样式 */
}

.list_loading .loading_text {
  line-height: 18px;
}

.list_finished,
.list_error {
  /* pointer-events: none; */
}

.list_error {
  cursor: pointer;
}
</style>

使用

<template>
  <div class="list">
    <scroll
      @load="handleLoad"
      :loading.sync="loading"
      :finished.sync="finished"
      :error.sync="error"
    >
      <div v-for="(item, index) in list" :key="index" class="item">
        {{ index }}
      </div>
    </scroll>
  </div>
</template>

<script>
import scroll from "./scroll.vue";
export default {
  components: {
    scroll,
  },
  data() {
    return {
      list: [],
      loading: false,
      finished: false,
      error: false,
      pageNum: 1,
    };
  },
  methods: {
    handleLoad() {
      if (this.list.length === 80) {
        this.list.push("1");
        this.error = true;
      } else {
        console.log("执行");
        setTimeout(() => {
          for (let i = 0; i < 20; i++) {
            this.list.push(i);
          }
          this.pageNum++;
          this.loading = false;
          if (this.list.length === 161) {
            this.finished = true;
          }
        }, 2000);
      }
    },
  },
};
</script>

<style>
* {
  margin: 0;
  padding: 0;
}
html,
body,
#app {
  height: 100%;
}
.item {
  padding: 10px 0;
  border-bottom: 1px solid blue;
}
.item:last-child {
  border: none;
}
.list {
  height: 100%;
  overflow-y: auto;
}
</style>