A:列表大数据量展示完成
This commit is contained in:
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)
|
||||
|
||||
@@ -59,6 +59,10 @@ export interface Photo {
|
||||
// 为justified-layout计算用的尺寸信息
|
||||
width: number
|
||||
height: number
|
||||
// 预渲染优化字段
|
||||
loaded?: boolean // 是否已加载真实图片
|
||||
inViewport?: boolean // 是否在视口中
|
||||
placeholder?: string // 占位符颜色
|
||||
}
|
||||
|
||||
// 时间线组织数据结构
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
/* 图片加载失败显示 */
|
||||
|
||||
Reference in new issue
Block a user