|
|
<template>
|
|
|
<div class="recycle-scroll-reveal" v-bind="$attrs" v-on="$listeners">
|
|
|
<div class="recycle-scroll-reveal-main">
|
|
|
<div ref="eternal" class="eternal-top">
|
|
|
<slot name="eternalTop"></slot>
|
|
|
</div>
|
|
|
<div class="scroll-reveal-list">
|
|
|
<div
|
|
|
v-for="(items, col) in visibleItems"
|
|
|
:key="col"
|
|
|
class="scroll-reveal-col"
|
|
|
:ref="'col'+ col"
|
|
|
:style="colStyle">
|
|
|
<div
|
|
|
v-for="item in items"
|
|
|
:key="item.index"
|
|
|
class="scroll-reveal-item"
|
|
|
:ref="'items' + item.index"
|
|
|
:style="getItemStyle(item)">
|
|
|
<div v-if="!item.placeholder">
|
|
|
<slot name="item" :data="item"></slot>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="loading">
|
|
|
<p v-if="noMore" class="load-text">没有更多了</p>
|
|
|
<Loading v-else :size="20"></Loading>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import {throttle, get} from 'lodash';
|
|
|
import {Loading} from 'cube-ui';
|
|
|
|
|
|
const EVENT_SCROLL = 'scroll';
|
|
|
|
|
|
export default {
|
|
|
name: 'RecycleScrollReveal',
|
|
|
data() {
|
|
|
let data = {
|
|
|
colPrefix: 'sr_col',
|
|
|
visibleItems: [],
|
|
|
colsHeight: [],
|
|
|
items: [],
|
|
|
itemWidth: 0,
|
|
|
currentItems: [],
|
|
|
noMore: false
|
|
|
};
|
|
|
|
|
|
for (let i = 0; i < this.cols; i++) {
|
|
|
data[data.colPrefix + i] = [];
|
|
|
data.visibleItems[i] = data[data.colPrefix + i];
|
|
|
data.colsHeight[i] = 0;
|
|
|
}
|
|
|
|
|
|
return data;
|
|
|
},
|
|
|
props: {
|
|
|
infinite: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
},
|
|
|
size: {
|
|
|
type: Number,
|
|
|
default: 20
|
|
|
},
|
|
|
offset: {
|
|
|
type: Number,
|
|
|
default: 100
|
|
|
},
|
|
|
onFetch: {
|
|
|
type: Function,
|
|
|
required: true
|
|
|
},
|
|
|
thumbs: {
|
|
|
type: Array,
|
|
|
default() {
|
|
|
return [];
|
|
|
}
|
|
|
},
|
|
|
cols: {
|
|
|
type: Number,
|
|
|
default: 2
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
colStyle() {
|
|
|
return {
|
|
|
width: `${100 / this.cols}%`
|
|
|
};
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
items(newList, oldList) {
|
|
|
let list = newList.slice(oldList.length, newList.length);
|
|
|
|
|
|
this.loadItems(list, oldList.length);
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
this.updateList = throttle(this._updateList.bind(this), 100);
|
|
|
let supportsPassive = false;
|
|
|
|
|
|
try {
|
|
|
const opts = Object.defineProperty({}, 'passive', {
|
|
|
get() {
|
|
|
supportsPassive = true;
|
|
|
return true;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
window.addEventListener('test', null, opts);
|
|
|
} catch (e) {} //eslint-disable-line
|
|
|
this.$el.addEventListener(EVENT_SCROLL, this.onScroll, supportsPassive ? { passive: true } : false);
|
|
|
|
|
|
this.init();
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
this.$el.removeEventListener(EVENT_SCROLL, this.onScroll);
|
|
|
},
|
|
|
methods: {
|
|
|
resize(index) {
|
|
|
this.$refs.scroll.resize(index);
|
|
|
},
|
|
|
init() {
|
|
|
this.currentItems = [];
|
|
|
this.colsHeight = [];
|
|
|
this.itemWidth = Math.floor(this.$el.offsetWidth / this.cols);
|
|
|
|
|
|
for (let i = 0; i < this.cols; i++) {
|
|
|
this[this.colPrefix + i].length = 0;
|
|
|
this.visibleItems[i] = this[this.colPrefix + i];
|
|
|
this.colsHeight[i] = 0;
|
|
|
}
|
|
|
|
|
|
this.load();
|
|
|
},
|
|
|
load(reload) {
|
|
|
if (!reload && (this.loading || this.noMore)) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
this.onFetch().then(res => {
|
|
|
this.loading = false;
|
|
|
|
|
|
if (!res) {
|
|
|
this.noMore = true;
|
|
|
} else {
|
|
|
this.items = this.items.concat(res);
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
async loadItems(list, start = 0) {
|
|
|
if (!list.length) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
let lastIndex = list.length;
|
|
|
if (this.cols > 1) {
|
|
|
lastIndex = Math.max.apply(null, [1, list.length - Math.floor(this.cols * 2)]);
|
|
|
}
|
|
|
|
|
|
let startCol = this.getMinHeightCol();
|
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
await this.loadItem({
|
|
|
data: list[i],
|
|
|
index: i + start,
|
|
|
width: this.itemWidth,
|
|
|
isThumb: true,
|
|
|
placeholder: false
|
|
|
}, i <= lastIndex ? (startCol + i) % this.cols : -1);
|
|
|
}
|
|
|
|
|
|
for (let i = 0; i < this.cols; i++) {
|
|
|
let loop = true;
|
|
|
let col = this[this.colPrefix + i];
|
|
|
let k = col.length - 1;
|
|
|
|
|
|
while (loop) {
|
|
|
let cur = col[k];
|
|
|
|
|
|
if (!cur || cur.height) {
|
|
|
loop = false;
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
let dom = this.$refs[`items${cur.index}`];
|
|
|
|
|
|
try {
|
|
|
if (dom && dom[0]) {
|
|
|
cur.height = dom[0].offsetHeight;
|
|
|
cur.top = dom[0].offsetTop;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
const message = `cur_${typeof cur}, dom_ ${typeof dom}, column_${i}, index_ ${index}, length_ ${this.items.length}, ${error ? error.message : ''}`;
|
|
|
|
|
|
throw new Error(message);
|
|
|
}
|
|
|
|
|
|
this.$set(this[this.colPrefix + i], k, cur);
|
|
|
k--;
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
loadItem(item, colIndex) {
|
|
|
return new Promise(r => {
|
|
|
let index = colIndex;
|
|
|
|
|
|
if (colIndex < 0) {
|
|
|
index = this.getMinHeightCol();
|
|
|
}
|
|
|
|
|
|
this[this.colPrefix + index].push(item);
|
|
|
|
|
|
if (colIndex < 0) {
|
|
|
setTimeout(() => {
|
|
|
this.$nextTick(() => {
|
|
|
r();
|
|
|
this.colsHeight[index] = this.$refs['col'+index][0].offsetHeight;
|
|
|
});
|
|
|
}, 100);
|
|
|
} else {
|
|
|
r();
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
getMinHeightCol() {
|
|
|
return this.colsHeight.indexOf(Math.min.apply(null, this.colsHeight));
|
|
|
},
|
|
|
getItemStyle(item) {
|
|
|
const style = {};
|
|
|
|
|
|
if (item.height) {
|
|
|
if (!item.unlockHight) {
|
|
|
style.height = `${item.height}px`;
|
|
|
}
|
|
|
} else if (item.willchange) {
|
|
|
style.transition = 'height 300ms cubic-bezier(0.165, 0.84, 0.44, 1)';
|
|
|
style['will-change'] = 'height';
|
|
|
style.height = `${item.height}px`;
|
|
|
style.opacity = 0;
|
|
|
} else if (!item.isThumb) {
|
|
|
style.position = 'absolute';
|
|
|
style.top = `${-1000}px`;
|
|
|
style.visibility = 'hidden';
|
|
|
}
|
|
|
return style;
|
|
|
},
|
|
|
updateCurrentItems(scrollTop) {
|
|
|
let top = scrollTop - this.$refs.eternal.offsetHeight;
|
|
|
let arr = [];
|
|
|
|
|
|
for (let i = 0; i < this.cols; i++) {
|
|
|
arr.push(this.updateColumnCurrentItems (i, top));
|
|
|
}
|
|
|
|
|
|
this.currentItems = arr;
|
|
|
},
|
|
|
updateColumnCurrentItems (index, top) {
|
|
|
let col = this[this.colPrefix + index];
|
|
|
let startIndex = Math.max(0, Math.floor(col.length * top / this.$refs['col'+index][0].offsetHeight) - this.size);
|
|
|
let endIndex = col.length;
|
|
|
|
|
|
while (startIndex < endIndex) {
|
|
|
if (col[startIndex].top > top) {
|
|
|
endIndex = startIndex - 1;
|
|
|
}
|
|
|
|
|
|
startIndex++;
|
|
|
}
|
|
|
|
|
|
for (let i = Math.max(0, Math.floor(endIndex - this.size * 2.5)); i < Math.min(this.size * 5, col.length); i++) {
|
|
|
let placeholder = Math.abs(i - endIndex) > this.size;
|
|
|
|
|
|
if (col[i].placeholder !== placeholder) {
|
|
|
this.$set(col[i], 'placeholder', placeholder);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return col[endIndex];
|
|
|
},
|
|
|
_updateList() {
|
|
|
const scrollTop = this.$el.scrollTop;
|
|
|
const heights = this.$el.offsetHeight;
|
|
|
|
|
|
// trigger load
|
|
|
if (scrollTop + this.$el.offsetHeight > heights - this.offset) {
|
|
|
this.load();
|
|
|
}
|
|
|
|
|
|
this.updateCurrentItems(scrollTop);
|
|
|
},
|
|
|
onScroll() {
|
|
|
const scrollTop = this.$el.scrollTop;
|
|
|
|
|
|
this.updateList();
|
|
|
|
|
|
this.$emit('scroll', {scrollTop});
|
|
|
},
|
|
|
},
|
|
|
components: {
|
|
|
Loading
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.recycle-scroll-reveal {
|
|
|
position: relative;
|
|
|
height: 100%;
|
|
|
overflow-x: hidden;
|
|
|
overflow-y: auto;
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
}
|
|
|
|
|
|
.recycle-scroll-reveal-main {
|
|
|
min-height: 100%;
|
|
|
}
|
|
|
|
|
|
.scroll-reveal-list {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
|
|
|
.scroll-reveal-col {
|
|
|
flex-grow: 1;
|
|
|
position: relative;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.loading {
|
|
|
padding: 20px 0;
|
|
|
text-align: center;
|
|
|
|
|
|
/deep/ .cube-loading-spinners {
|
|
|
margin: auto;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
...
|
...
|
|