|
@@ -1,89 +1,156 @@
|
|
|
<script lang="ts" setup>
|
|
|
import { propTypes } from '@/utils/propTypes'
|
|
|
import { useDesign } from '@/hooks/web/useDesign'
|
|
|
-import { ref, nextTick, unref, onMounted } from 'vue'
|
|
|
-import { isString } from '@/utils/is'
|
|
|
+import { ref, nextTick, unref, onMounted, watch } from 'vue'
|
|
|
+import { useEventListener, useIntersectionObserver } from '@vueuse/core'
|
|
|
+import { debounce } from 'lodash-es'
|
|
|
|
|
|
const { getPrefixCls } = useDesign()
|
|
|
|
|
|
const prefixCls = getPrefixCls('waterfall')
|
|
|
|
|
|
+const emit = defineEmits(['loadMore'])
|
|
|
+
|
|
|
const prop = defineProps({
|
|
|
data: propTypes.arrayOf(propTypes.any),
|
|
|
- reset: propTypes.bool.def(false),
|
|
|
+ reset: propTypes.bool.def(true),
|
|
|
width: propTypes.number.def(200),
|
|
|
- height: propTypes.number.def(0),
|
|
|
gap: propTypes.number.def(20),
|
|
|
- getContainer: propTypes.func.def(() => document.body),
|
|
|
props: propTypes.objectOf(propTypes.string).def({
|
|
|
src: 'src',
|
|
|
height: 'height'
|
|
|
- })
|
|
|
+ }),
|
|
|
+ loadingText: propTypes.string.def('加载中...'),
|
|
|
+ loading: propTypes.bool.def(false),
|
|
|
+ end: propTypes.bool.def(false),
|
|
|
+ endText: propTypes.string.def('没有更多了')
|
|
|
})
|
|
|
|
|
|
const wrapEl = ref<HTMLDivElement>()
|
|
|
|
|
|
const heights = ref<number[]>([])
|
|
|
|
|
|
+const wrapHeight = ref(0)
|
|
|
+
|
|
|
+const wrapWidth = ref(0)
|
|
|
+
|
|
|
+const loadMore = ref<HTMLDivElement>()
|
|
|
+
|
|
|
// 首先确定列数 = 页面宽度 / 图片宽度
|
|
|
const cols = ref(0)
|
|
|
|
|
|
+const filterData = ref<any[]>([])
|
|
|
+
|
|
|
const filterWaterfall = async () => {
|
|
|
- const { props, width, gap, getContainer, height } = prop
|
|
|
+ const { props, width, gap } = prop
|
|
|
const data = prop.data as any[]
|
|
|
await nextTick()
|
|
|
|
|
|
- const container = (getContainer?.() || unref(wrapEl)) as HTMLElement
|
|
|
+ const container = unref(wrapEl) as HTMLElement
|
|
|
if (!container) return
|
|
|
cols.value = Math.floor(container.clientWidth / (width + gap))
|
|
|
|
|
|
const length = data.length
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
- if (i + 1 < unref(cols)) {
|
|
|
- if (height || data[i][props.height as string]) {
|
|
|
- // 如果有全局高度,就使用全局高度
|
|
|
- // 如果 data[i][props.height as string] 是字符串,只保留数字字符串
|
|
|
- const itemHeight = isString(data[i][props.height as string])
|
|
|
- ? Number(data[i][props.height as string].replace(/[^0-9]/gi, ''))
|
|
|
- : data[i][props.height as string]
|
|
|
- heights.value[i] = height || itemHeight
|
|
|
- } else {
|
|
|
- // 说明在第一列
|
|
|
- const itemEl = container.querySelector(`.${prefixCls}-item__${i}`)
|
|
|
- itemEl?.addEventListener('load', () => {
|
|
|
- const clientRect = itemEl?.getBoundingClientRect()
|
|
|
- console.log(clientRect)
|
|
|
- })
|
|
|
- // const imgEl = new Image()
|
|
|
- // imgEl.src = data[i][props.src as string]
|
|
|
- // imgEl.onload = async () => {
|
|
|
- // // const itemEl = container.querySelector(`.${prefixCls}-item__${i}`)
|
|
|
- // const clientRect = itemEl?.getBoundingClientRect()
|
|
|
- // if (clientRect) {
|
|
|
- // heights.value[i] = clientRect?.height
|
|
|
- // }
|
|
|
- // }
|
|
|
+ if (i < unref(cols)) {
|
|
|
+ heights.value[i] = data[i][props.height as string]
|
|
|
+ filterData.value.push({
|
|
|
+ ...data[i],
|
|
|
+ top: 0,
|
|
|
+ left: i * (width + gap)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 其他行,先找出最矮的那一列 和 索引
|
|
|
+ // 假设最小高度是第一个元素
|
|
|
+ let minHeight = heights.value[0]
|
|
|
+ let index = 0
|
|
|
+ // 找出最小高度
|
|
|
+ for (let j = 1; j < cols.value; j++) {
|
|
|
+ if (unref(heights)[j] < minHeight) {
|
|
|
+ minHeight = unref(heights)[j]
|
|
|
+ index = j
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ // 更新最矮高度
|
|
|
+ heights.value[index] += data[i][props.height as string] + gap
|
|
|
+ filterData.value.push({
|
|
|
+ ...data[i],
|
|
|
+ top: minHeight + gap,
|
|
|
+ left: index * (width + gap)
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
+ wrapHeight.value = Math.max(...unref(heights))
|
|
|
+ wrapWidth.value = unref(cols) * (width + gap) - gap
|
|
|
}
|
|
|
|
|
|
+watch(
|
|
|
+ () => prop.data,
|
|
|
+ async () => {
|
|
|
+ await nextTick()
|
|
|
+ filterWaterfall()
|
|
|
+ },
|
|
|
+ {
|
|
|
+ immediate: true
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
- filterWaterfall()
|
|
|
+ if (unref(prop.reset)) {
|
|
|
+ useEventListener(window, 'resize', debounce(filterWaterfall, 300))
|
|
|
+ }
|
|
|
+ useIntersectionObserver(
|
|
|
+ unref(loadMore),
|
|
|
+ ([{ isIntersecting }]) => {
|
|
|
+ if (isIntersecting && !prop.loading && !prop.end) {
|
|
|
+ emit('loadMore')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ threshold: 0.1
|
|
|
+ }
|
|
|
+ )
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <div :class="prefixCls" ref="wrapEl">
|
|
|
+ <div
|
|
|
+ :class="[prefixCls, 'flex', 'justify-center', 'items-center']"
|
|
|
+ ref="wrapEl"
|
|
|
+ :style="{
|
|
|
+ height: `${wrapHeight + 40}px`
|
|
|
+ }"
|
|
|
+ >
|
|
|
<div
|
|
|
- v-for="(item, $index) in data"
|
|
|
- :class="`${prefixCls}-item__${$index}`"
|
|
|
- :key="`water-${$index}`"
|
|
|
+ class="relative"
|
|
|
:style="{
|
|
|
- width: `${width}px`
|
|
|
+ width: `${wrapWidth}px`,
|
|
|
+ height: `${wrapHeight + 40}px`
|
|
|
}"
|
|
|
>
|
|
|
- <img :src="item[props.src as string]" class="w-full block" alt="" srcset="" />
|
|
|
+ <div
|
|
|
+ v-for="(item, $index) in filterData"
|
|
|
+ :class="[`${prefixCls}-item__${$index}`, 'absolute']"
|
|
|
+ :key="`water-${$index}`"
|
|
|
+ :style="{
|
|
|
+ width: `${width}px`,
|
|
|
+ height: `${item[props.height as string]}px`,
|
|
|
+ top: `${item.top}px`,
|
|
|
+ left: `${item.left}px`
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <img :src="item[props.src as string]" class="w-full h-full block" alt="" srcset="" />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ ref="loadMore"
|
|
|
+ class="h-40px flex justify-center absolute w-full"
|
|
|
+ :style="{
|
|
|
+ top: `${wrapHeight + gap}px`
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ {{ end ? endText : loadingText }}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|