|
|
<template> <!-- 参考 https://blog.csdn.net/weixin_44198965/article/details/147831036 -->
<div class="waterfall-container"> <!-- 列容器 --> <div ref="columnWrapper" class="waterfall-column-wrapper"> <!-- 瀑布流项 --> <div v-for="(item, index) in renderedList" :key="item.id || index" v-lazy-load="item.image || item.cover" class="waterfall-item" > <!-- 图片类型 --> <div v-if="item.type === 'image'" class="item-content" @click="jump(item.linkUrl)"> <img :src="placeholder" :alt="item.title || '图片'" class="item-image" > <div v-if="item.title" class="item-title">{{ item.title }}</div> </div>
<!-- 视频类型 --> <div v-else-if="item.type === 'video'" class="item-content"> <video :poster="placeholder" controls class="item-video" > <source :src="item.videoUrl" type="video/mp4"> </video> <div v-if="item.title" class="item-title">{{ item.title }}</div> </div>
<!-- 图文类型 --> <div v-else class="item-content"> <img v-if="item.image" :src="placeholder" :alt="item.title || '图文'" class="item-image" > <div v-if="item.title" class="item-title">{{ item.title }}</div> <div v-if="item.desc" class="item-desc">{{ item.desc }}</div> </div> </div> </div>
<!-- 加载状态 --> <div v-if="loading" class="loading-more"> 加载中... </div> <div v-if="isComplete && renderedList.length > 0" class="load-complete"> 没有更多内容了 </div> </div></template>
<script>export default { name: 'Waterfall',
directives: { lazyLoad: { inserted(el, binding, vnode) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = el.tagName === 'IMG' ? el : el.querySelector('img') if (img) { img.src = binding.value // 图片加载完成后重新布局
img.onload = () => { vnode.context.layoutItems() } // 图片加载错误处理
img.onerror = () => { if (vnode.context.placeholder) { img.src = vnode.context.placeholder } } } observer.unobserve(el) } }) }, { rootMargin: '0px 0px 100px 0px' // 提前100px加载
})
observer.observe(el)
el._observer = observer }, unbind(el) { if (el._observer) { el._observer.disconnect() } } } },
// 定义组件props
props: { // 数据源数组
list: { type: Array, required: true, default: () => [] }, // 列数配置
columns: { type: Number, default: 2, validator: function(value) { return value >= 1 && value <= 5 // 限制列数在1-5之间
} }, // 列间距
gap: { type: Number, default: 10 }, // 是否开启懒加载
lazyLoad: { type: Boolean, default: true }, // 触底加载的阈值(距离底部多少像素触发加载)
loadThreshold: { type: Number, default: 100 }, // 图片加载时的占位图
placeholder: { type: String, default: '改成你的图片' }, totalItems: { type: Number, default: 0 } },
data() { return { waterfallRef: null, // 瀑布流容器引用
columnHeights: [], // 每列当前高度数组
columnWrapperRef: null, // 列容器引用
loading: false, // 加载状态
isComplete: false, // 是否已加载全部数据
renderedList: [], // 已渲染的数据列表
pageSize: 20, // 每次加载的数据量
currentPage: 1 // 当前页码
} },
computed: { // 计算列宽度(根据容器宽度和列数计算)
columnWidth() { if (!this.waterfallRef) return 0 const containerWidth = this.waterfallRef.clientWidth return (containerWidth - (this.columns - 1) * this.gap) / this.columns } },
watch: { // 监听列数变化
columns(newVal, oldVal) { if (newVal !== oldVal) { this.resetLayout() } },
// 监听数据源变化
list: { handler(newList) { this.renderedList = newList // 直接同步父组件数据(无需分页切片)
this.$nextTick(() => this.layoutItems()) }, deep: true } },
mounted() { this.scrollContainer = this.$el this.waterfallRef = this.$el this.columnWrapperRef = this.$el.querySelector('.waterfall-column-wrapper')
this.resetLayout() if (this.lazyLoad) { this.scrollContainer.addEventListener('scroll', this.handleScroll) } window.addEventListener('resize', this.handleResize) },
beforeDestroy() { if (this.lazyLoad && this.scrollContainer) { this.scrollContainer.removeEventListener('scroll', this.handleScroll) } window.removeEventListener('resize', this.handleResize) },
methods: { // 初始化列高度数组
initColumnHeights() { this.columnHeights = new Array(this.columns).fill(0) },
// 初始化渲染数据
initRenderList() { this.renderedList = this.list.slice(0, this.pageSize) this.currentPage = 1 this.isComplete = this.renderedList.length >= this.list.length },
// 重置瀑布流布局
resetLayout() { this.initColumnHeights() this.initRenderList() this.$nextTick(() => { this.layoutItems() }) },
// 瀑布流布局算法
layoutItems() { if (!this.columnWrapperRef || !this.waterfallRef) return
// 获取所有子元素
const items = this.columnWrapperRef.children if (!items || items.length === 0) return
// 初始化列高度
this.initColumnHeights()
// 临时列高度副本用于计算
const tempColumnHeights = [...this.columnHeights]
// 遍历所有子元素进行布局
Array.from(items).forEach((item, index) => { // 找到当前最短的列
const minHeight = Math.min(...tempColumnHeights) const columnIndex = tempColumnHeights.indexOf(minHeight)
// 计算位置
const left = columnIndex * (this.columnWidth + this.gap) const top = minHeight
// 应用样式
item.style.position = 'absolute' item.style.width = `${this.columnWidth}px` item.style.left = `${left}px` item.style.top = `${top}px` item.style.transition = 'all 0.3s ease'
// 更新列高度
tempColumnHeights[columnIndex] += item.clientHeight + this.gap })
// 更新列高度
this.columnHeights = tempColumnHeights
// 设置容器高度
const maxHeight = Math.max(...tempColumnHeights) this.columnWrapperRef.style.height = `${maxHeight}px` },
// 滚动事件处理
// handleScroll() {
// if (this.loading || this.isComplete) return
// const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// const windowHeight = window.innerHeight
// const scrollHeight = document.documentElement.scrollHeight
// // 判断是否触底
// if (scrollTop + windowHeight >= scrollHeight - this.loadThreshold) {
// this.loadMore()
// }
// },
handleScroll() { if (this.loading || (this.totalItems > 0 && this.renderedList.length >= this.totalItems)) { return }
// 获取滚动容器(父组件的.content-main)
const scrollContainer = this.$parent.$el.querySelector('.content-main') || window let scrollTop, clientHeight, scrollHeight
if (scrollContainer === window) { scrollTop = document.documentElement.scrollTop || document.body.scrollTop clientHeight = window.innerHeight scrollHeight = document.documentElement.scrollHeight } else { // 关键:用滚动容器的实际尺寸计算(避免父组件padding影响)
scrollTop = scrollContainer.scrollTop clientHeight = scrollContainer.clientHeight scrollHeight = scrollContainer.scrollHeight }
// 触底阈值:距离底部100px时触发加载(可调整)
if (scrollTop + clientHeight >= scrollHeight - this.loadThreshold) { this.$emit('scroll-end') // 触发父组件的loadMoreData
console.log('触发加载更多') } },
// 加载更多数据
loadMore() { if (this.loading || this.isComplete) return
this.loading = true
// 模拟异步加载
setTimeout(() => { try { const nextPage = this.currentPage + 1 const startIndex = (nextPage - 1) * this.pageSize const endIndex = nextPage * this.pageSize
if (startIndex >= this.list.length) { this.isComplete = true return }
const newItems = this.list.slice(startIndex, endIndex) this.renderedList = [...this.renderedList, ...newItems] this.currentPage = nextPage
// 检查是否已加载全部数据
if (endIndex >= this.list.length) { this.isComplete = true }
// 等待DOM更新后重新布局
this.$nextTick(() => { this.layoutItems() }) } catch (error) { console.error('加载更多数据失败:', error) } finally { this.loading = false } }, 500) }, jump(url) { window.location.href = url }, // 响应式处理窗口大小变化
handleResize() { this.resetLayout() } }}</script>
<style scoped>.waterfall-container { position: relative; width: calc(100%); height: 800px; overflow: hidden; overflow-y: auto;}
.waterfall-column-wrapper { position: relative; width: 100%;
}
.waterfall-item { margin-bottom: 10px; break-inside: avoid; box-sizing: border-box; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transition: all 0.3s ease;}
.waterfall-item:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);}
.item-content { padding: 12px;}
.item-image, .item-video { width: 100%; border-radius: 4px; display: block; background: #f5f5f5;}
.item-video { aspect-ratio: 9/16; object-fit: cover;}
.item-title { margin-top: 8px; font-size: 24px; line-height: 1.4; color: #333; font-weight: bold; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;}
.item-desc { margin-top: 6px; font-size: 12px; color: #666; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;}
.loading-more, .load-complete { text-align: center; padding: 20px 0; color: #999; font-size: 14px;}</style>
|