11<script setup lang="ts">
22import { ref , onMounted , onUnmounted , watch } from ' vue'
3- import { NImage , NCard , NSpin , NButton , NIcon } from ' naive-ui'
3+ import { NCard , NSpin , NButton , NIcon , NImage } from ' naive-ui'
44import { OpenOutline } from ' @vicons/ionicons5'
55import type { ImageItem } from ' @/types/waterfall'
6- import { debounce } from ' lodash-es'
6+ import { debounce , throttle } from ' lodash-es'
77
88const props = defineProps <{
99 images: ImageItem []
@@ -21,6 +21,8 @@ const containerRef = ref<HTMLElement | null>(null)
2121const columns = ref <ImageItem [][]>([])
2222const columnCount = ref (1 )
2323const columnWidth = ref (240 )
24+ // 存储图片实际尺寸
25+ const imageSizes = ref <Map <string , { width: number ; height: number }>>(new Map ())
2426
2527function calculateColumnCount() {
2628 if (! containerRef .value ) return
@@ -53,17 +55,47 @@ function distributeImages() {
5355 })
5456}
5557
58+ function getImageSize(image : ImageItem ): { width: number ; height: number } {
59+ // 优先使用缓存的实际尺寸
60+ if (imageSizes .value .has (image .id )) {
61+ return imageSizes .value .get (image .id )!
62+ }
63+ // 其次使用 API 返回的尺寸
64+ if (image .width && image .height && image .width > 0 && image .height > 0 ) {
65+ return { width: image .width , height: image .height }
66+ }
67+ // 默认使用 1:1 比例
68+ return { width: 1 , height: 1 }
69+ }
70+
5671function getColumnHeight(column : ImageItem []) {
5772 return column .reduce ((height , image ) => {
58- // 使用 API 返回的尺寸计算高度
59- const scaledHeight = ( columnWidth .value * image .height ) / image .width
73+ const size = getImageSize ( image )
74+ const scaledHeight = Math . floor (( columnWidth .value * size .height ) / size .width )
6075 return height + scaledHeight + 8
6176 }, 0 )
6277}
6378
6479function calculateImageHeight(image : ImageItem ): number {
65- // 使用 API 返回的尺寸计算高度
66- return Math .floor ((columnWidth .value * image .height ) / image .width )
80+ const size = getImageSize (image )
81+ return Math .floor ((columnWidth .value * size .height ) / size .width )
82+ }
83+
84+ // 批量更新布局,合并短时间内的多次调用
85+ const debouncedDistribute = debounce (() => {
86+ distributeImages ()
87+ }, 50 )
88+
89+ function onImageLoad(imageId : string , event : Event ) {
90+ const img = event .target as HTMLImageElement
91+ if (img .naturalWidth > 0 && img .naturalHeight > 0 ) {
92+ imageSizes .value .set (imageId , {
93+ width: img .naturalWidth ,
94+ height: img .naturalHeight
95+ })
96+ // 使用 debounced 版本,50ms 内的多次调用会合并为一次
97+ debouncedDistribute ()
98+ }
6799}
68100
69101// 使用 debounce 优化 resize 处理
@@ -72,19 +104,19 @@ const handleResize = debounce(() => {
72104 distributeImages ()
73105}, 100 )
74106
75- // 使用 debounce 优化滚动处理
76- const handleScroll = debounce (() => {
107+ // 使用 throttle 优化滚动处理(throttle 比 debounce 更适合滚动场景)
108+ const handleScroll = throttle (() => {
77109 if (props .loading || ! props .hasMore ) return
78110
79111 const scrollTop = window .scrollY || document .documentElement .scrollTop
80112 const windowHeight = window .innerHeight
81113 const documentHeight = document .documentElement .scrollHeight
82114
83- // 当距离底部 100px 时触发加载
84- if (documentHeight - (scrollTop + windowHeight ) < 100 ) {
115+ // 当距离底部 300px 时触发加载(增大触发距离,提前加载)
116+ if (documentHeight - (scrollTop + windowHeight ) < 300 ) {
85117 emit (' load-more' )
86118 }
87- }, 200 )
119+ }, 100 )
88120
89121let resizeObserver: ResizeObserver | null = null
90122
@@ -115,15 +147,25 @@ onUnmounted(() => {
115147 window .removeEventListener (' resize' , handleResize )
116148 window .removeEventListener (' scroll' , handleScroll )
117149
118- // 取消未执行的 debounce 函数
150+ // 取消未执行的 debounce/throttle 函数
119151 handleResize .cancel ()
120152 handleScroll .cancel ()
153+ debouncedDistribute .cancel ()
121154})
122155
123156// 监听图片数组变化
124157watch (
125158 () => props .images ,
126- () => {
159+ (newImages ) => {
160+ // 清理不存在的图片尺寸缓存
161+ const imageIds = new Set (newImages .map (img => img .id ))
162+ const newSizes = new Map <string , { width: number ; height: number }>()
163+ imageSizes .value .forEach ((size , id ) => {
164+ if (imageIds .has (id )) {
165+ newSizes .set (id , size )
166+ }
167+ })
168+ imageSizes .value = newSizes
127169 distributeImages ()
128170 },
129171 { deep: true },
@@ -137,68 +179,65 @@ watch(columnCount, () => {
137179
138180<template >
139181 <div ref =" containerRef" class =" waterfall" >
140- <NSpin :show =" loading " >
141- <div class =" columns" >
142- <div
143- v-for =" (column, columnIndex) in columns"
144- :key =" columnIndex"
145- class =" column"
146- :style =" { width: `${columnWidth}px` }"
147- >
148- <NCard
149- v-for =" image in column "
150- :key =" image .id "
151- class="image-card"
152- :content-style =" { padding: 0 } "
153- :header-style =" { padding: 0 } "
154- :footer-style =" { padding: 0 } "
155- :segmented =" { content: false } "
156- :bordered =" false "
157- :style =" { marginBottom: ' 8px' } "
158- >
159- <div class =" image-wrapper" >
182+ <div class =" columns" >
183+ <div v-for =" (column, columnIndex) in columns" :key =" columnIndex" class =" column"
184+ :style =" { width: `${columnWidth}px` }" >
185+ <NCard v-for =" image in column " :key =" image .id " class="image-card" :content-style =" { padding: 0 } "
186+ :header-style =" { padding: 0 } " :footer-style =" { padding: 0 } " :segmented =" { content: false } "
187+ :bordered =" false " :style =" { marginBottom: ' 8px' } " >
188+ <div class =" image-wrapper" >
189+ <div class =" image-container" :style =" {
190+ width: '100%',
191+ height: `${calculateImageHeight(image)}px`,
192+ overflow: 'hidden',
193+ borderRadius: '4px',
194+ position: 'relative',
195+ backgroundColor: '#f0f0f0',
196+ }" >
160197 <NImage
161198 :src =" image .url "
162- :width =" columnWidth "
163- :height =" calculateImageHeight (image )"
164199 :preview-src =" image .originalUrl "
165- object-fit="cover"
166- :show-toolbar-tooltip =" false "
167- :preview-disabled =" false "
200+ :img-props =" {
201+ style: {
202+ width: ' 100%' ,
203+ height: ' 100%' ,
204+ objectFit: ' cover' ,
205+ objectPosition: ' top' ,
206+ display: ' block' ,
207+ }
208+ } "
168209 :style =" {
169210 width: ' 100%' ,
170- height: ' auto ' ,
211+ height: ` ${ calculateImageHeight ( image )}px ` ,
171212 display: ' block' ,
172213 borderRadius: ' 4px' ,
173214 cursor: ' zoom-in' ,
174215 } "
216+ @load =" (e : Event ) => onImageLoad (image .id , e ) "
175217 />
176- <NButton
177- quaternary
178- circle
179- size="small"
180- class="open-link"
181- tag="a"
182- :href =" image .wb_url "
183- target="_blank"
184- :style =" {
185- ' --n-text-color' : ' #ffffff' ,
186- ' --n-text-color-hover' : ' #ffffff' ,
187- ' --n-text-color-pressed' : ' #ffffff' ,
188- } "
189- >
190- <template #icon >
191- <NIcon >
192- <OpenOutline />
193- </NIcon >
194- </template >
195- </NButton >
196218 </div >
197- </NCard >
198- </div >
219+ <NButton quaternary circle size="small" class="open-link" tag="a" :href =" image .wb_url " target="_blank"
220+ :style =" {
221+ ' --n-text-color' : ' #ffffff' ,
222+ ' --n-text-color-hover' : ' #ffffff' ,
223+ ' --n-text-color-pressed' : ' #ffffff' ,
224+ } " >
225+ <template #icon >
226+ <NIcon >
227+ <OpenOutline />
228+ </NIcon >
229+ </template >
230+ </NButton >
231+ </div >
232+ </NCard >
199233 </div >
200- <div v-if =" !loading && !hasMore" class =" no-more" >没有更多了</div >
201- </NSpin >
234+ </div >
235+ <!-- 底部加载状态 -->
236+ <div v-if =" loading" class =" loading-more" >
237+ <NSpin size="small" />
238+ <span >加载中...</span >
239+ </div >
240+ <div v-else-if =" !hasMore && images.length > 0" class =" no-more" >没有更多了</div >
202241 </div >
203242</template >
204243
@@ -260,6 +299,15 @@ watch(columnCount, () => {
260299 color : var (--n-text-color-3 );
261300}
262301
302+ .loading-more {
303+ display : flex ;
304+ align-items : center ;
305+ justify-content : center ;
306+ gap : 8px ;
307+ padding : 16px ;
308+ color : var (--n-text-color-3 );
309+ }
310+
263311:deep(.n-card ) {
264312 background-color : transparent ;
265313}
0 commit comments