国产化查询机
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

434 lines
12 KiB

<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>