feat: Waterfall
This commit is contained in:
parent
69ec5f0d7d
commit
d543e56efb
|
@ -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>
|
||||
|
|
|
@ -32,7 +32,7 @@ axiosInstance.interceptors.response.use(
|
|||
return res
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.log('err' + error) // for debug
|
||||
console.log('err: ' + error) // for debug
|
||||
ElMessage.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
|
|
@ -11,29 +11,51 @@ const data = ref<any>([])
|
|||
const getList = () => {
|
||||
const list: any = []
|
||||
for (let i = 0; i < 20; i++) {
|
||||
// 随机 100, 500 之间的整数
|
||||
const height = Mock.Random.integer(100, 500)
|
||||
const width = Mock.Random.integer(100, 500)
|
||||
list.push(
|
||||
Mock.mock({
|
||||
width,
|
||||
height,
|
||||
id: toAnyString(),
|
||||
image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)')
|
||||
image_uri: Mock.Random.image(`${width}x${height}`)
|
||||
})
|
||||
)
|
||||
}
|
||||
data.value = [...unref(data), ...list]
|
||||
console.log('【data】:', data.value)
|
||||
if (unref(data).length >= 60) {
|
||||
end.value = true
|
||||
}
|
||||
}
|
||||
getList()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const end = ref(false)
|
||||
|
||||
const loadMore = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
getList()
|
||||
loading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap :title="t('router.waterfall')">
|
||||
<Waterfall
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:end="end"
|
||||
:props="{
|
||||
src: 'image_uri',
|
||||
height: 'height'
|
||||
}"
|
||||
@load-more="loadMore"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue