A:列表大数据量展示完成

This commit is contained in:
jimmy committed 2025-08-23 15:28:50 +08:00
1 parent 03ab075d9d
commit 902583beb0
3 files changed
+223 -49

No files matched your search

@@ -27,45 +27,85 @@ export async function getPhotos(params?: PhotosParams): Promise<ApiResponse<Phot
}
/**
* 根据时间线数据获取具体某天的照片
* 根据时间线数据获取具体某天的照片(分页获取所有照片)
*/
export async function getPhotosByDate(date: string, limit = 50, offset = 0): Promise<Photo[]> {
const response:ApiResponse<PhotosData> = await getPhotos({
start_date: date,
end_date: date,
limit,
offset
})
if (response.code !== 200 || !response.data) {
return []
}
const { hash, isImage, takenAt, ratio } = response.data
// 转换列式存储数据为行式存储
export async function getPhotosByDate(date: string, totalCount: number): Promise<Photo[]> {
const photos: Photo[] = []
const count = Math.min(hash.length, isImage.length, takenAt.length, ratio.length)
const pageSize = 1000 // API最大限制
let offset = 0
console.log(`开始获取 ${date} 的照片,总计 ${totalCount} 张`)
while (photos.length < totalCount) {
try {
const response: ApiResponse<PhotosData> = await getPhotos({
start_date: date,
end_date: date,
limit: Math.min(pageSize, totalCount - photos.length),
offset
})
for (let i = 0; i < count; i++) {
// 为justified-layout计算实际尺寸
const photoRatio = ratio[i] || 1
const baseHeight = 200
const width = Math.round(baseHeight * photoRatio)
const height = baseHeight
if (response.code !== 200 || !response.data) {
console.warn(`获取照片失败 ${date}, offset: ${offset}`)
break
}
photos.push({
hash: hash[i],
isImage: isImage[i],
takenAt: takenAt[i],
ratio: photoRatio,
width,
height
})
const { hash, isImage, takenAt, ratio } = response.data
const count = Math.min(hash.length, isImage.length, takenAt.length, ratio.length)
if (count === 0) {
console.log(`没有更多照片了 ${date}, offset: ${offset}`)
break
}
// 转换列式存储数据为行式存储
for (let i = 0; i < count; i++) {
const photoRatio = ratio[i] || 1
const baseHeight = 200
const width = Math.round(baseHeight * photoRatio)
const height = baseHeight
photos.push({
hash: hash[i],
isImage: isImage[i],
takenAt: takenAt[i],
ratio: photoRatio,
width,
height,
loaded: false, // 预渲染优化:初始未加载真实图片
inViewport: false, // 预渲染优化:初始不在视口中
placeholder: generatePlaceholderColor() // 预渲染优化:占位符颜色
})
}
offset += count
console.log(`已获取 ${photos.length}/${totalCount} 张照片 (${date})`)
// 如果返回的数量小于请求数量,说明已经到底了
if (count < Math.min(pageSize, totalCount - offset + count)) {
break
}
} catch (error) {
console.error(`获取照片失败 ${date}, offset: ${offset}:`, error)
break
}
}
console.log(`完成获取 ${date} 的照片,实际获取 ${photos.length} 张`)
return photos
}
/**
* 生成占位符颜色
*/
function generatePlaceholderColor(): string {
const colors = [
'#f0f2f5', '#fafafa', '#f5f5f5', '#e8e8e8', '#d9d9d9',
'#bfbfbf', '#8c8c8c', '#595959', '#434343', '#262626'
]
return colors[Math.floor(Math.random() * colors.length)]
}
/**
* 获取照片缩略图URL
*/
@@ -158,12 +198,12 @@ export async function getFullTimeline(params?: TimelineParams): Promise<MonthGro
const monthGroups = groupTimelineByMonth(timelineResponse.data)
console.log('处理后的月份分组:', monthGroups)
// 为每个日期加载照片数据(限制加载数量,避免一次性加载太多)
// 为每个日期加载照片比例数据(分页获取所有照片)
for (const monthGroup of monthGroups) {
for (const dayGroup of monthGroup.days) {
try {
// 每天最多加载30张照片用于展示
const photos = await getPhotosByDate(dayGroup.date, 30, 0)
// 使用时间线中的count信息,分页获取当天所有照片的比例数据
const photos = await getPhotosByDate(dayGroup.date, dayGroup.count)
dayGroup.photos = photos
} catch (error) {
console.error(`Failed to load photos for ${dayGroup.date}:`, error)
+4
View File
@@ -59,6 +59,10 @@ export interface Photo {
// 为justified-layout计算用的尺寸信息
width: number
height: number
// 预渲染优化字段
loaded?: boolean // 是否已加载真实图片
inViewport?: boolean // 是否在视口中
placeholder?: string // 占位符颜色
}
// 时间线组织数据结构
+146 -16
View File
@@ -66,16 +66,26 @@
}"
@click="handlePhotoClick(photo)"
>
<!-- 延迟加载的图片 -->
<img
v-if="photo.loaded"
:src="photo.thumbnailUrl"
:alt="`照片 ${photo.hash.slice(0, 8)}`"
class="photo-image"
:style="{
backgroundColor: photo.color
}"
@error="handleImageError"
loading="lazy"
@load="handleImageLoad(photo)"
/>
<!-- 占位符背景 -->
<div
v-else
class="photo-placeholder"
:style="{
backgroundColor: photo.color,
width: '100%',
height: '100%'
}"
:data-hash="photo.hash"
></div>
<!-- 开发时显示比例信息 -->
<div v-if="false" class="photo-debug-info">
<span class="photo-ratio">{{ photo.ratio.toFixed(2) }}</span>
@@ -138,9 +148,12 @@ function convertApiPhotosToPhotos(apiPhotos: ApiPhoto[]): Photo[] {
return apiPhotos.map(apiPhoto => ({
...apiPhoto,
id: apiPhoto.hash, // 使用hash作为id
color: generateRandomColor(), // 占位颜色
color: apiPhoto.placeholder || generateRandomColor(), // 使用预设占位颜色
thumbnailUrl: getThumbnailUrl(apiPhoto.hash, '400'),
box: { top: 0, left: 0, width: 0, height: 0 } // 稍后计算
box: { top: 0, left: 0, width: 0, height: 0 }, // 稍后计算
// 预渲染优化字段
loaded: apiPhoto.loaded || false,
inViewport: apiPhoto.inViewport || false
}))
}
@@ -275,29 +288,102 @@ function recalculatePhotosLayout() {
})
}
// 视口观察器
let viewportObserver: IntersectionObserver | null = null
// 处理照片点击事件
function handlePhotoClick(photo: Photo) {
// TODO: 实现照片详情查看或大图预览功能
console.log('Photo clicked:', photo)
}
// 处理图片加载完成
function handleImageLoad(photo: Photo) {
console.log(`图片加载完成: ${photo.hash.slice(0, 8)}`)
}
// 处理图片加载失败
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement
// 显示占位颜色背景
const hash = img.alt?.match(/照片 (\w+)/)?.[1] || 'unknown'
console.warn(`图片加载失败: ${hash}`)
// 恢复占位符显示
img.style.display = 'none'
const parent = img.parentElement
if (parent) {
parent.style.backgroundColor = '#f0f0f0'
parent.innerHTML = `
<div class="photo-error">
<span>📷</span>
<span>加载失败</span>
</div>
`
// 找到对应的照片对象并重置loaded状态
timelineItems.value = timelineItems.value.map(item => {
if (item.type === 'photos' && item.photos) {
return {
...item,
photos: item.photos.map(p =>
p.hash.startsWith(hash) ? { ...p, loaded: false } : p
)
}
}
return item
})
}
}
// 初始化视口观察器
function initViewportObserver() {
if (viewportObserver) {
viewportObserver.disconnect()
}
viewportObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const hash = (entry.target as HTMLElement).dataset.hash
if (hash && entry.isIntersecting) {
// 照片进入视口,开始加载
loadPhotoImage(hash)
}
})
},
{
// 提前200px开始加载
rootMargin: '200px',
threshold: 0
}
)
}
// 加载具体照片图片
function loadPhotoImage(hash: string) {
timelineItems.value = timelineItems.value.map(item => {
if (item.type === 'photos' && item.photos) {
return {
...item,
photos: item.photos.map(photo => {
if (photo.hash === hash && !photo.loaded) {
console.log(`开始加载图片: ${hash.slice(0, 8)}`)
return { ...photo, loaded: true, inViewport: true }
}
return photo
})
}
}
return item
})
}
// 观察所有占位符元素
function observePhotos() {
if (!viewportObserver) return
// 观察当前所有的占位符元素
nextTick(() => {
const placeholders = document.querySelectorAll('.photo-placeholder[data-hash]')
placeholders.forEach(placeholder => {
viewportObserver?.observe(placeholder)
})
console.log(`开始观察 ${placeholders.length} 个照片占位符`)
})
}
// 初始化数据
onMounted(async () => {
await nextTick()
@@ -308,9 +394,16 @@ onMounted(async () => {
containerWidth.value = container.clientWidth
}
// 初始化视口观察器
initViewportObserver()
// 加载时间线数据
await loadTimelineData()
// 数据加载完成后开始观察照片
await nextTick()
observePhotos()
// 监听窗口大小变化
const handleResize = () => {
if (container) {
@@ -318,15 +411,21 @@ onMounted(async () => {
if (Math.abs(newWidth - containerWidth.value) > 50) { // 只有明显变化时才重新计算
containerWidth.value = newWidth
recalculatePhotosLayout()
// 重新计算后重新观察
nextTick(() => observePhotos())
}
}
}
window.addEventListener('resize', handleResize)
// 清理事件监听器
// 清理事件监听器和观察器
return () => {
window.removeEventListener('resize', handleResize)
if (viewportObserver) {
viewportObserver.disconnect()
viewportObserver = null
}
}
})
</script>
@@ -490,8 +589,39 @@ onMounted(async () => {
transition: opacity 0.3s ease;
}
.photo-image:not([src]) {
.photo-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: background-color 0.2s ease;
}
.photo-placeholder::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 24px;
height: 24px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
animation: placeholder-spin 1s linear infinite;
opacity: 0;
transition: opacity 0.3s ease;
}
.photo-placeholder:hover::after {
opacity: 1;
}
@keyframes placeholder-spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* 图片加载失败显示 */