feat: refactor waterfall to be more efficient
This commit is contained in:
parent
fb0bc57542
commit
5d61b4d000
24
package.json
24
package.json
|
@ -56,6 +56,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"jsonwebtoken": "^8.5.0",
|
||||
"knex": "^0.16.3",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"moment": "^2.24.0",
|
||||
"multer": "^1.4.1",
|
||||
"mysql": "^2.16.0",
|
||||
|
@ -117,7 +118,28 @@
|
|||
],
|
||||
"class-methods-use-this": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-plusplus": [
|
||||
"error",
|
||||
{
|
||||
"allowForLoopAfterthoughts": true
|
||||
}
|
||||
],
|
||||
"no-underscore-dangle": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"_id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"js": "never",
|
||||
"ts": "never"
|
||||
}
|
||||
],
|
||||
"vue/attribute-hyphenation": 0,
|
||||
"vue/html-closing-bracket-newline": [
|
||||
"error",
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
v-if="showWaterfall"
|
||||
:gutterWidth="10"
|
||||
:gutterHeight="4"
|
||||
:options="{fitWidth: true}"
|
||||
:itemWidth="width"
|
||||
:items="gridFiles">
|
||||
<template v-slot="{item}">
|
||||
|
@ -451,6 +452,10 @@ div.actions {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.waterfall {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.waterfall-item:hover {
|
||||
div.actions {
|
||||
opacity: 1;
|
||||
|
|
|
@ -1,51 +1,20 @@
|
|||
<template>
|
||||
<div class="waterfall">
|
||||
<WaterfallItem v-for="(item, index) in items" :key="item.id" :ref="`item-${item.id}`" :width="itemWidth">
|
||||
<div ref="waterfall" class="waterfall">
|
||||
<WaterfallItem
|
||||
v-for="(item, index) in items"
|
||||
:key="item.id"
|
||||
:style="{ width: `${itemWidth}px`, marginBottom: `${gutterHeight}px` }"
|
||||
:width="itemWidth">
|
||||
<slot :item="item" />
|
||||
</WaterfallItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WaterfallItem from './WaterfallItem.vue';
|
||||
|
||||
const quickSort = (arr, type) => {
|
||||
const left = [];
|
||||
const right = [];
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
const povis = arr[0];
|
||||
for (let i = 1; i < arr.length; i++) {
|
||||
if (arr[i][type] < povis[type]) {
|
||||
left.push(arr[i]);
|
||||
} else {
|
||||
right.push(arr[i]);
|
||||
}
|
||||
}
|
||||
return quickSort(left, type).concat(povis, quickSort(right, type));
|
||||
};
|
||||
|
||||
const getMinIndex = (arr) => {
|
||||
let pos = 0;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[pos] > arr[i]) {
|
||||
pos = i;
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
};
|
||||
|
||||
const _ = {
|
||||
on(el, type, func, capture = false) {
|
||||
el.addEventListener(type, func, capture);
|
||||
},
|
||||
off(el, type, func, capture = false) {
|
||||
el.removeEventListener(type, func, capture);
|
||||
},
|
||||
};
|
||||
|
||||
const sum = (arr) => arr.reduce((acc, val) => acc + val, 0);
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
const Masonry = isBrowser ? window.Masonry || require('masonry-layout') : null;
|
||||
const imagesloaded = isBrowser ? require('imagesloaded') : null;
|
||||
|
||||
export default {
|
||||
name: 'Waterfall',
|
||||
|
@ -53,159 +22,112 @@ export default {
|
|||
WaterfallItem,
|
||||
},
|
||||
props: {
|
||||
gutterWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
gutterHeight: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
resizable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
},
|
||||
fixWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
minCol: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
maxCol: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
percent: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
itemWidth: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer: null,
|
||||
colNum: 0,
|
||||
lastWidth: 0,
|
||||
percentWidthArr: [],
|
||||
readyChildCount: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
items() {
|
||||
this.$nextTick(() => this.render('watch'));
|
||||
itemWidth: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
gutterWidth: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
gutterHeight: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$on('itemRender', () => {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
this.timer = setTimeout(() => {
|
||||
this.render('created');
|
||||
}, 0);
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.resizeHandle();
|
||||
this.$watch('resizable', this.resizeHandle);
|
||||
this.initializeMasonry();
|
||||
this.imagesLoaded();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$off('itemRender');
|
||||
_.off(window, 'resize', this.render);
|
||||
updated() {
|
||||
this.performLayout();
|
||||
this.imagesLoaded();
|
||||
},
|
||||
unmounted() {
|
||||
this.masonry.destroy();
|
||||
},
|
||||
methods: {
|
||||
calulate(arr) {
|
||||
const pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth;
|
||||
// 百分比布局计算
|
||||
if (this.percent) {
|
||||
this.colNum = this.percent.length;
|
||||
const total = sum(this.percent);
|
||||
this.percentWidthArr = this.percent.map((value) => (value / total) * pageWidth);
|
||||
this.lastWidth = 0;
|
||||
// 正常布局计算
|
||||
} else {
|
||||
this.colNum = parseInt(pageWidth / (arr.width + this.gutterWidth), 10);
|
||||
if (this.minCol && this.colNum < this.minCol) {
|
||||
this.colNum = this.minCol;
|
||||
this.lastWidth = 0;
|
||||
} else if (this.maxCol && this.colNum > this.maxCol) {
|
||||
this.colNum = this.maxCol;
|
||||
this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
|
||||
} else {
|
||||
this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
|
||||
}
|
||||
imagesLoaded() {
|
||||
const node = this.$refs.waterfall;
|
||||
imagesloaded(
|
||||
node,
|
||||
() => {
|
||||
this.masonry.layout();
|
||||
},
|
||||
);
|
||||
},
|
||||
performLayout() {
|
||||
const diff = this.diffDomChildren();
|
||||
if (diff.removed.length > 0) {
|
||||
this.masonry.remove(diff.removed);
|
||||
this.masonry.reloadItems();
|
||||
}
|
||||
if (diff.appended.length > 0) {
|
||||
this.masonry.appended(diff.appended);
|
||||
this.masonry.reloadItems();
|
||||
}
|
||||
if (diff.prepended.length > 0) {
|
||||
this.masonry.prepended(diff.prepended);
|
||||
}
|
||||
if (diff.moved.length > 0) {
|
||||
this.masonry.reloadItems();
|
||||
}
|
||||
this.masonry.layout();
|
||||
},
|
||||
diffDomChildren() {
|
||||
const oldChildren = this.domChildren.filter((element) => !!element.parentNode);
|
||||
const newChildren = this.getNewDomChildren();
|
||||
const removed = oldChildren.filter((oldChild) => !newChildren.includes(oldChild));
|
||||
const domDiff = newChildren.filter((newChild) => !oldChildren.includes(newChild));
|
||||
const prepended = domDiff.filter((newChild, index) => newChildren[index] === newChild);
|
||||
const appended = domDiff.filter((el) => !prepended.includes(el));
|
||||
let moved = [];
|
||||
if (removed.length === 0) {
|
||||
moved = oldChildren.filter((child, index) => index !== newChildren.indexOf(child));
|
||||
}
|
||||
this.domChildren = newChildren;
|
||||
return {
|
||||
old: oldChildren,
|
||||
new: newChildren,
|
||||
removed,
|
||||
appended,
|
||||
prepended,
|
||||
moved,
|
||||
};
|
||||
},
|
||||
initializeMasonry() {
|
||||
if (!this.masonry) {
|
||||
this.masonry = new Masonry(
|
||||
this.$refs.waterfall,
|
||||
{
|
||||
columnWidth: this.itemWidth,
|
||||
gutter: this.gutterWidth,
|
||||
...this.options,
|
||||
},
|
||||
);
|
||||
this.domChildren = this.getNewDomChildren();
|
||||
}
|
||||
},
|
||||
resizeHandle() {
|
||||
if (this.resizable) {
|
||||
_.on(window, 'resize', this.render, false);
|
||||
} else {
|
||||
_.off(window, 'resize', this.render, false);
|
||||
}
|
||||
},
|
||||
render(context) {
|
||||
console.log(context);
|
||||
if (!this.items) return;
|
||||
// 重新排序
|
||||
let childArr = [];
|
||||
childArr = this.items.map(({ id }) => this.$refs[`item-${id}`][0].getMeta());
|
||||
childArr = quickSort(childArr, 'order');
|
||||
// 计算列数
|
||||
this.calulate(childArr[0]);
|
||||
const offsetArr = Array(this.colNum).fill(0);
|
||||
// 渲染
|
||||
childArr.forEach((child) => {
|
||||
const position = getMinIndex(offsetArr);
|
||||
// 百分比布局渲染
|
||||
if (this.percent) {
|
||||
let left = 0;
|
||||
child.el.style.width = `${this.percentWidthArr[position]}px`;
|
||||
if (position === 0) {
|
||||
left = 0;
|
||||
} else {
|
||||
for (let i = 0; i < position; i++) {
|
||||
left += this.percentWidthArr[i];
|
||||
}
|
||||
}
|
||||
child.el.style.left = `${left}px`;
|
||||
// 正常布局渲染
|
||||
} else {
|
||||
if (this.align === 'left') { // eslint-disable-line no-lonely-if
|
||||
child.el.style.left = `${position * (child.width + this.gutterWidth)}px`;
|
||||
} else if (this.align === 'right') {
|
||||
child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth}px`;
|
||||
} else {
|
||||
child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth / 2}px`;
|
||||
}
|
||||
}
|
||||
if (child.height === 0) {
|
||||
return;
|
||||
}
|
||||
child.el.style.top = `${offsetArr[position]}px`;
|
||||
offsetArr[position] += child.height + this.gutterHeight;
|
||||
this.$el.style.height = `${Math.max(...offsetArr)}px`;
|
||||
});
|
||||
this.$emit('rendered', this);
|
||||
getNewDomChildren() {
|
||||
const node = this.$refs.waterfall;
|
||||
const children = this.options && this.options.itemSelector
|
||||
? node.querySelectorAll(this.options.itemSelector) : node.children;
|
||||
return Array.prototype.slice.call(children);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.waterfall {
|
||||
position: relative;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
.wfi {
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,80 +3,8 @@
|
|||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import imagesLoaded from 'imagesloaded';
|
||||
|
||||
export default {
|
||||
name: 'WaterfallItem',
|
||||
props: {
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
video: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
itemWidth: 0,
|
||||
height: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$watch(() => this.height, this.emit);
|
||||
},
|
||||
mounted() {
|
||||
this.$el.style.display = 'none';
|
||||
this.$el.style.width = `${this.width}px`;
|
||||
this.emit();
|
||||
if (this.video) {
|
||||
// find first video object
|
||||
const videoEl = this.$slots.default.find((e) => e.tag?.toLowerCase() === 'video');
|
||||
const el = videoEl.elm;
|
||||
|
||||
// add event listener for video loaded
|
||||
el.onloadeddata = () => {
|
||||
this.$el.style.left = '-9999px';
|
||||
this.$el.style.top = '-9999px';
|
||||
this.$el.style.display = 'block';
|
||||
this.height = el.offsetHeight + 5;
|
||||
this.itemWidth = el.offsetWidth;
|
||||
};
|
||||
} else {
|
||||
imagesLoaded(this.$el, () => {
|
||||
this.$el.style.left = '-9999px';
|
||||
this.$el.style.top = '-9999px';
|
||||
this.$el.style.display = 'block';
|
||||
this.height = this.$el.offsetHeight;
|
||||
this.itemWidth = this.$el.offsetWidth;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emit() {
|
||||
this.$parent.$emit('itemRender');
|
||||
},
|
||||
getMeta() {
|
||||
return {
|
||||
el: this.$el,
|
||||
height: this.height,
|
||||
width: this.itemWidth,
|
||||
order: this.order,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.waterfall-item {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue