|
|
@@ -0,0 +1,114 @@
|
|
|
+<template>
|
|
|
+ <div class="app-waterfall">
|
|
|
+ <ul ref="warterfallRef" v-if="dataList.length">
|
|
|
+ <li v-for="(item, index) in dataList" :key="index">
|
|
|
+ <slot :item="item">{{ item }}</slot>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { shallowRef, watch, nextTick, onActivated, onMounted } from 'vue'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ //数据列表
|
|
|
+ dataList: {
|
|
|
+ type: Array,
|
|
|
+ default: () => ([])
|
|
|
+ },
|
|
|
+ //列数
|
|
|
+ column: {
|
|
|
+ type: Number,
|
|
|
+ default: 2
|
|
|
+ },
|
|
|
+ //间距
|
|
|
+ gap: {
|
|
|
+ type: Number,
|
|
|
+ default: 10
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const warterfallRef = shallowRef<HTMLDivElement>()
|
|
|
+
|
|
|
+const state: { total: number; hightList: number[]; } = {
|
|
|
+ total: 0,
|
|
|
+ hightList: [] //瀑布流高度列表
|
|
|
+}
|
|
|
+
|
|
|
+const render = () => {
|
|
|
+ state.total = 0
|
|
|
+ state.hightList = []
|
|
|
+
|
|
|
+ nextTick(async () => {
|
|
|
+ const el = warterfallRef.value
|
|
|
+ if (el) {
|
|
|
+ const nodes = el.querySelectorAll('li')
|
|
|
+
|
|
|
+ for (let i = state.total; i < nodes.length; i++) {
|
|
|
+ const li = nodes[i]
|
|
|
+ const images = li.querySelectorAll('img')
|
|
|
+
|
|
|
+ // 等待所有图片加载完成
|
|
|
+ for (let n = 0; n < images.length; n++) {
|
|
|
+ await new Promise<void>((resolve) => {
|
|
|
+ if (images[n].complete) {
|
|
|
+ resolve()
|
|
|
+ } else {
|
|
|
+ images[n].onload = () => resolve()
|
|
|
+ images[n].onerror = () => resolve()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!el.offsetWidth) {
|
|
|
+ // 在 keepalive 组件下,如果在数据未全部加载出来之前跳转了页面,这会导致元素获取不到宽度,从而影响瀑布流布局的正常显示
|
|
|
+ // 所以需要停止还未加载完成的数据渲染,待页面重新打开后继续加载
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ const space = (props.column - 1) * props.gap // 总间距
|
|
|
+ const width = (el.offsetWidth - space) / props.column // 每列的宽度
|
|
|
+
|
|
|
+ li.style.width = width + 'px'
|
|
|
+ li.style.opacity = '1'
|
|
|
+
|
|
|
+ //判断是否首行
|
|
|
+ if (i < props.column) {
|
|
|
+ li.style.top = '0'
|
|
|
+ li.style.left = (width * i) + (props.gap * i) + 'px'
|
|
|
+ state.hightList.push(li.offsetHeight + props.gap)
|
|
|
+ } else {
|
|
|
+ const minHeight = Math.min(...state.hightList) // 获取数组中最小值
|
|
|
+ const index = state.hightList.findIndex((e) => e === minHeight) // 最小值的索引位置
|
|
|
+ li.style.top = minHeight + 'px'
|
|
|
+ li.style.left = (width * index) + (props.gap * index) + 'px'
|
|
|
+ state.hightList[index] += li.offsetHeight + props.gap
|
|
|
+ }
|
|
|
+
|
|
|
+ state.total++
|
|
|
+ }
|
|
|
+
|
|
|
+ const maxHeight = Math.max(...state.hightList); // 获取数组中最大值
|
|
|
+ el.style.height = (maxHeight - props.gap) + 'px'
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+watch(() => props.dataList, () => render())
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ render()
|
|
|
+
|
|
|
+ onActivated(() => {
|
|
|
+ // 对未完成加载的数据进行渲染
|
|
|
+ if (props.dataList.length > state.total) {
|
|
|
+ render()
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less">
|
|
|
+@import './index.less';
|
|
|
+</style>
|