A:完成虚拟列表初始展示
This commit is contained in:
1 parent
fbfd77ec93
commit
03ab075d9d
6 files changed
+544
-98
No files matched your search
@@ -13,7 +13,7 @@ import type { HttpConfig } from '@/types/http.ts'
|
||||
// 2. 配置管理器 (config/httpConfig.ts)
|
||||
class HttpConfigManager {
|
||||
private config: HttpConfig = {
|
||||
baseURL: import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:9484',
|
||||
baseURL: import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:9484/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { httpClient } from '@/utils/http'
|
||||
import type {
|
||||
TimelineParams,
|
||||
TimelineResponse,
|
||||
PhotosParams,
|
||||
PhotosResponse,
|
||||
Photo,
|
||||
DayGroup,
|
||||
MonthGroup, PhotosData
|
||||
} from '@/types/api'
|
||||
import type { ApiResponse } from '@/types/http.ts'
|
||||
|
||||
/**
|
||||
* 获取照片时间线统计数据
|
||||
*/
|
||||
export async function getTimeline(params?: TimelineParams): Promise<ApiResponse<TimelineResponse>> {
|
||||
const response = await httpClient.get<TimelineResponse>('/v1/photos/timeline', params)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取照片列表数据
|
||||
*/
|
||||
export async function getPhotos(params?: PhotosParams): Promise<ApiResponse<PhotosData>> {
|
||||
const response = await httpClient.get<PhotosData>('/v1/photos', params)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据时间线数据获取具体某天的照片
|
||||
*/
|
||||
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
|
||||
|
||||
// 转换列式存储数据为行式存储
|
||||
const photos: Photo[] = []
|
||||
const count = Math.min(hash.length, isImage.length, takenAt.length, ratio.length)
|
||||
|
||||
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
|
||||
|
||||
photos.push({
|
||||
hash: hash[i],
|
||||
isImage: isImage[i],
|
||||
takenAt: takenAt[i],
|
||||
ratio: photoRatio,
|
||||
width,
|
||||
height
|
||||
})
|
||||
}
|
||||
|
||||
return photos
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取照片缩略图URL
|
||||
*/
|
||||
export function getThumbnailUrl(hash: string, size = '400x400'): string {
|
||||
const baseUrl = import.meta.env.VITE_APP_API_URL
|
||||
return `${baseUrl}/v1/photo/${hash}?format=thumbnail&size=${size}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取照片原图URL
|
||||
*/
|
||||
export function getOriginalUrl(hash: string): string {
|
||||
const baseUrl = import.meta.env.VITE_APP_API_URL
|
||||
return `${baseUrl}/v1/photo/${hash}?format=original`
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间线数据转换为月份分组数据
|
||||
*/
|
||||
export function groupTimelineByMonth(timelineData: { date: string; count: number }[]): MonthGroup[] {
|
||||
const monthMap = new Map<string, MonthGroup>()
|
||||
|
||||
for (const item of timelineData) {
|
||||
const date = new Date(item.date)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const monthKey = `${year}-${month.toString().padStart(2, '0')}`
|
||||
|
||||
if (!monthMap.has(monthKey)) {
|
||||
monthMap.set(monthKey, {
|
||||
year,
|
||||
month,
|
||||
title: `${year}年${month}月`,
|
||||
subtitle: '0张照片',
|
||||
days: [],
|
||||
totalCount: 0
|
||||
})
|
||||
}
|
||||
|
||||
const monthGroup = monthMap.get(monthKey)!
|
||||
monthGroup.totalCount += item.count
|
||||
|
||||
// 添加日期组
|
||||
monthGroup.days.push({
|
||||
date: item.date,
|
||||
photos: [], // 这里暂时为空,需要时再加载
|
||||
count: item.count
|
||||
})
|
||||
}
|
||||
|
||||
// 更新subtitle并排序
|
||||
const result = Array.from(monthMap.values())
|
||||
.map(month => ({
|
||||
...month,
|
||||
subtitle: `${month.totalCount}张照片`,
|
||||
days: month.days.sort((a, b) => b.date.localeCompare(a.date)) // 按日期倒序
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// 按年月倒序
|
||||
if (a.year !== b.year) return b.year - a.year
|
||||
return b.month - a.month
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的时间线数据(包含照片)
|
||||
*/
|
||||
export async function getFullTimeline(params?: TimelineParams): Promise<MonthGroup[]> {
|
||||
console.log('正在获取时间线数据,参数:', params)
|
||||
|
||||
try {
|
||||
const timelineResponse = await getTimeline(params)
|
||||
console.log('时间线API响应:', timelineResponse)
|
||||
|
||||
// 检查响应结构
|
||||
if (!timelineResponse) {
|
||||
throw new Error('API响应为空')
|
||||
}
|
||||
|
||||
if (timelineResponse.code !== 200) {
|
||||
throw new Error(timelineResponse.message || '获取时间线数据失败')
|
||||
}
|
||||
|
||||
if (!Array.isArray(timelineResponse.data)) {
|
||||
throw new Error('时间线数据格式错误')
|
||||
}
|
||||
|
||||
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)
|
||||
dayGroup.photos = photos
|
||||
} catch (error) {
|
||||
console.error(`Failed to load photos for ${dayGroup.date}:`, error)
|
||||
dayGroup.photos = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return monthGroups
|
||||
} catch (error) {
|
||||
console.error('获取时间线数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// API相关类型定义
|
||||
|
||||
// 时间线API响应
|
||||
export interface TimelineItem {
|
||||
date: string // YYYY-MM-DD格式
|
||||
count: number // 该日期的照片数量
|
||||
}
|
||||
|
||||
export interface TimelineResponse {
|
||||
code: number
|
||||
message: string
|
||||
data: TimelineItem[]
|
||||
}
|
||||
|
||||
// 照片列表API响应(列式存储格式)
|
||||
export interface PhotosData {
|
||||
hash: string[] // 照片hash数组
|
||||
isImage: boolean[] // 是否为图片数组
|
||||
takenAt: string[] // 拍摄时间数组 ISO格式
|
||||
ratio: number[] // 宽高比数组
|
||||
}
|
||||
|
||||
export interface PhotosResponse {
|
||||
code: number
|
||||
message: string
|
||||
data: PhotosData
|
||||
}
|
||||
|
||||
// 照片详情响应
|
||||
export interface PhotoDetails {
|
||||
// 这里需要根据实际API响应结构补充
|
||||
// 目前从API文档看只有获取照片文件的接口,没有详情结构
|
||||
}
|
||||
|
||||
// API请求参数
|
||||
export interface TimelineParams {
|
||||
start_date?: string // YYYY-MM-DD格式
|
||||
end_date?: string // YYYY-MM-DD格式
|
||||
}
|
||||
|
||||
export interface PhotosParams {
|
||||
limit?: number // 分页大小,最大1000
|
||||
offset?: number // 偏移量
|
||||
start_date?: string // YYYY-MM-DD格式
|
||||
end_date?: string // YYYY-MM-DD格式
|
||||
}
|
||||
|
||||
export interface PhotoRequestParams {
|
||||
format?: 'original' | 'thumbnail'
|
||||
size?: string // 格式:WIDTHxHEIGHT,如 400x400
|
||||
}
|
||||
|
||||
// 内部使用的照片数据结构
|
||||
export interface Photo {
|
||||
hash: string
|
||||
isImage: boolean
|
||||
takenAt: string
|
||||
ratio: number
|
||||
// 为justified-layout计算用的尺寸信息
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
// 时间线组织数据结构
|
||||
export interface DayGroup {
|
||||
date: string // YYYY-MM-DD
|
||||
photos: Photo[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface MonthGroup {
|
||||
year: number
|
||||
month: number
|
||||
title: string // 如:"2024年1月"
|
||||
subtitle: string // 如:"128张照片"
|
||||
days: DayGroup[]
|
||||
totalCount: number
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
declare module 'justified-layout' {
|
||||
interface GeometryOptions {
|
||||
containerWidth?: number
|
||||
targetRowHeight?: number
|
||||
targetRowHeightTolerance?: number
|
||||
boxSpacing?: number
|
||||
containerPadding?: number
|
||||
}
|
||||
|
||||
interface Box {
|
||||
top: number
|
||||
left: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface Geometry {
|
||||
containerHeight: number
|
||||
boxes: Box[]
|
||||
}
|
||||
|
||||
interface PhotoInput {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function justifiedLayout(photos: PhotoInput[], options?: GeometryOptions): Geometry
|
||||
export = justifiedLayout
|
||||
}
|
||||
@@ -115,15 +115,13 @@ const removePath = async (item: LibraryPath) => {
|
||||
// 开始任务
|
||||
const startTask = async () => {
|
||||
const enabledPaths = libraryPaths.value.filter((item) => item.enabled)
|
||||
message.error('创建任务失败')
|
||||
return
|
||||
if (enabledPaths.length === 0) {
|
||||
message.warning('请至少选择一个启用的路径')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await httpClient.post('/api/task/create', {
|
||||
const response = await httpClient.post('v1/library/indexed', {
|
||||
paths: enabledPaths.map((item) => item.path),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<div class="photos-timeline">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">正在加载照片时间线...</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="timelineItems.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📷</div>
|
||||
<div class="empty-text">暂无照片</div>
|
||||
<div class="empty-subtitle">开始上传一些照片来创建你的时间线吧</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间线内容 -->
|
||||
<DynamicScroller
|
||||
v-else
|
||||
ref="scrollerRef"
|
||||
:items="timelineItems"
|
||||
:min-item-size="100"
|
||||
@@ -47,11 +62,22 @@
|
||||
top: photo.box.top + 'px',
|
||||
left: photo.box.left + 'px',
|
||||
width: photo.box.width + 'px',
|
||||
height: photo.box.height + 'px',
|
||||
backgroundColor: photo.color
|
||||
height: photo.box.height + 'px'
|
||||
}"
|
||||
@click="handlePhotoClick(photo)"
|
||||
>
|
||||
<div class="photo-placeholder">
|
||||
<img
|
||||
:src="photo.thumbnailUrl"
|
||||
:alt="`照片 ${photo.hash.slice(0, 8)}`"
|
||||
class="photo-image"
|
||||
:style="{
|
||||
backgroundColor: photo.color
|
||||
}"
|
||||
@error="handleImageError"
|
||||
loading="lazy"
|
||||
/>
|
||||
<!-- 开发时显示比例信息 -->
|
||||
<div v-if="false" class="photo-debug-info">
|
||||
<span class="photo-ratio">{{ photo.ratio.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,13 +93,13 @@ import { ref, onMounted, nextTick } from 'vue'
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||
import justifiedLayout from 'justified-layout'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import { getFullTimeline, getThumbnailUrl } from '@/services/timelineService'
|
||||
import type { Photo as ApiPhoto, MonthGroup, DayGroup } from '@/types/api'
|
||||
|
||||
interface Photo {
|
||||
interface Photo extends ApiPhoto {
|
||||
id: string
|
||||
width: number
|
||||
height: number
|
||||
ratio: number
|
||||
color: string
|
||||
color?: string // 占位颜色,用于加载时显示
|
||||
thumbnailUrl: string
|
||||
box: {
|
||||
top: number
|
||||
left: number
|
||||
@@ -89,12 +115,15 @@ interface TimelineItem {
|
||||
subtitle?: string
|
||||
height?: number
|
||||
photos?: Photo[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const scrollerRef = ref()
|
||||
const containerWidth = ref(1200)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
// 生成随机颜色
|
||||
// 生成随机颜色作为占位符
|
||||
function generateRandomColor(): string {
|
||||
const colors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
@@ -104,28 +133,23 @@ function generateRandomColor(): string {
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
// 生成随机照片数据
|
||||
function generateRandomPhotos(count: number): Photo[] {
|
||||
const photos: Photo[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const width = Math.floor(Math.random() * 400) + 200 // 200-600px
|
||||
const height = Math.floor(Math.random() * 400) + 200 // 200-600px
|
||||
const ratio = width / height
|
||||
|
||||
photos.push({
|
||||
id: `photo-${i}`,
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
color: generateRandomColor(),
|
||||
box: { top: 0, left: 0, width: 0, height: 0 } // 先设置默认值,后面会计算
|
||||
})
|
||||
}
|
||||
return photos
|
||||
// 转换API照片数据为组件用的照片数据
|
||||
function convertApiPhotosToPhotos(apiPhotos: ApiPhoto[]): Photo[] {
|
||||
return apiPhotos.map(apiPhoto => ({
|
||||
...apiPhoto,
|
||||
id: apiPhoto.hash, // 使用hash作为id
|
||||
color: generateRandomColor(), // 占位颜色
|
||||
thumbnailUrl: getThumbnailUrl(apiPhoto.hash, '400'),
|
||||
box: { top: 0, left: 0, width: 0, height: 0 } // 稍后计算
|
||||
}))
|
||||
}
|
||||
|
||||
// 使用 justified-layout 计算照片布局
|
||||
function calculatePhotosLayout(photos: Photo[]): { photos: Photo[], height: number } {
|
||||
if (photos.length === 0) {
|
||||
return { photos: [], height: 0 }
|
||||
}
|
||||
|
||||
const photoInputs = photos.map(photo => ({
|
||||
width: photo.width,
|
||||
height: photo.height
|
||||
@@ -141,7 +165,7 @@ function calculatePhotosLayout(photos: Photo[]): { photos: Photo[], height: numb
|
||||
|
||||
const layoutPhotos = photos.map((photo, index) => ({
|
||||
...photo,
|
||||
box: geometry.boxes[index]
|
||||
box: geometry.boxes[index] || { top: 0, left: 0, width: 0, height: 0 }
|
||||
}))
|
||||
|
||||
return {
|
||||
@@ -150,58 +174,56 @@ function calculatePhotosLayout(photos: Photo[]): { photos: Photo[], height: numb
|
||||
}
|
||||
}
|
||||
|
||||
// 生成时间线数据
|
||||
function generateTimelineData(): TimelineItem[] {
|
||||
// 转换API数据为时间线展示数据
|
||||
function convertMonthGroupsToTimelineItems(monthGroups: MonthGroup[]): TimelineItem[] {
|
||||
const items: TimelineItem[] = []
|
||||
const months = [
|
||||
{ title: '2024年1月', subtitle: '128张照片' },
|
||||
{ title: '2023年12月', subtitle: '95张照片' },
|
||||
{ title: '2023年11月', subtitle: '156张照片' }
|
||||
]
|
||||
|
||||
months.forEach((month, monthIndex) => {
|
||||
monthGroups.forEach((monthGroup, monthIndex) => {
|
||||
// 添加月份标题
|
||||
items.push({
|
||||
id: `month-${monthIndex}`,
|
||||
id: `month-${monthGroup.year}-${monthGroup.month}`,
|
||||
type: 'month',
|
||||
title: month.title,
|
||||
subtitle: month.subtitle
|
||||
title: monthGroup.title,
|
||||
subtitle: monthGroup.subtitle
|
||||
})
|
||||
|
||||
// 添加回忆卡片 (随机)
|
||||
// 添加回忆卡片 (随机,约40%概率出现)
|
||||
if (Math.random() > 0.6) {
|
||||
items.push({
|
||||
id: `memory-${monthIndex}`,
|
||||
id: `memory-${monthGroup.year}-${monthGroup.month}`,
|
||||
type: 'memory',
|
||||
title: '回忆',
|
||||
subtitle: '一年前的今天'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加日期和照片组
|
||||
const daysInMonth = Math.floor(Math.random() * 8) + 3 // 3-10天
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dayId = `day-${monthIndex}-${day}`
|
||||
|
||||
// 添加日期卡片
|
||||
items.push({
|
||||
id: `date-${dayId}`,
|
||||
type: 'date',
|
||||
title: `${month.title.slice(0, 7)}${day}日`
|
||||
})
|
||||
// 添加每天的照片数据
|
||||
monthGroup.days.forEach((dayGroup, dayIndex) => {
|
||||
if (dayGroup.photos.length > 0) {
|
||||
const dayId = `day-${monthGroup.year}-${monthGroup.month}-${dayIndex}`
|
||||
|
||||
// 添加照片组
|
||||
const photoCount = Math.floor(Math.random() * 20) + 5 // 5-24张照片
|
||||
const photos = generateRandomPhotos(photoCount)
|
||||
const layout = calculatePhotosLayout(photos)
|
||||
// 添加日期卡片
|
||||
const date = new Date(dayGroup.date)
|
||||
const dateTitle = `${date.getMonth() + 1}月${date.getDate()}日`
|
||||
|
||||
items.push({
|
||||
id: `photos-${dayId}`,
|
||||
type: 'photos',
|
||||
height: layout.height,
|
||||
photos: layout.photos
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
id: `date-${dayId}`,
|
||||
type: 'date',
|
||||
title: dateTitle
|
||||
})
|
||||
|
||||
// 转换并布局照片
|
||||
const photos = convertApiPhotosToPhotos(dayGroup.photos)
|
||||
const layout = calculatePhotosLayout(photos)
|
||||
|
||||
items.push({
|
||||
id: `photos-${dayId}`,
|
||||
type: 'photos',
|
||||
height: layout.height,
|
||||
photos: layout.photos
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return items
|
||||
@@ -210,43 +232,98 @@ function generateTimelineData(): TimelineItem[] {
|
||||
// 响应式数据
|
||||
const timelineItems = ref<TimelineItem[]>([])
|
||||
|
||||
// 加载时间线数据
|
||||
async function loadTimelineData() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
// 获取最近2年的数据
|
||||
const endDate = new Date().toISOString().split('T')[0]
|
||||
const startDate = new Date(Date.now() - 2 * 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||||
|
||||
const monthGroups = await getFullTimeline({
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
})
|
||||
|
||||
// 转换为时间线展示数据
|
||||
timelineItems.value = convertMonthGroupsToTimelineItems(monthGroups)
|
||||
} catch (err) {
|
||||
console.error('Failed to load timeline data:', err)
|
||||
error.value = err instanceof Error ? err.message : '加载时间线数据失败'
|
||||
|
||||
// 降级到空状态,不显示错误给用户,在控制台记录即可
|
||||
timelineItems.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重新计算照片布局
|
||||
function recalculatePhotosLayout() {
|
||||
timelineItems.value = timelineItems.value.map(item => {
|
||||
if (item.type === 'photos' && item.photos) {
|
||||
const layout = calculatePhotosLayout(item.photos)
|
||||
return {
|
||||
...item,
|
||||
height: layout.height,
|
||||
photos: layout.photos
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// 处理照片点击事件
|
||||
function handlePhotoClick(photo: Photo) {
|
||||
// TODO: 实现照片详情查看或大图预览功能
|
||||
console.log('Photo clicked:', photo)
|
||||
}
|
||||
|
||||
// 处理图片加载失败
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement
|
||||
// 显示占位颜色背景
|
||||
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>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
|
||||
// 获取容器宽度
|
||||
const container = document.querySelector('.photos-timeline')
|
||||
if (container) {
|
||||
containerWidth.value = container.clientWidth
|
||||
}
|
||||
|
||||
// 生成时间线数据
|
||||
timelineItems.value = generateTimelineData()
|
||||
|
||||
|
||||
// 加载时间线数据
|
||||
await loadTimelineData()
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (container) {
|
||||
const newWidth = container.clientWidth
|
||||
if (Math.abs(newWidth - containerWidth.value) > 50) { // 只有明显变化时才重新计算
|
||||
containerWidth.value = newWidth
|
||||
// 重新计算所有照片布局
|
||||
timelineItems.value = timelineItems.value.map(item => {
|
||||
if (item.type === 'photos' && item.photos) {
|
||||
const layout = calculatePhotosLayout(item.photos)
|
||||
return {
|
||||
...item,
|
||||
height: layout.height,
|
||||
photos: layout.photos
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
recalculatePhotosLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
|
||||
// 清理事件监听器
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
@@ -265,6 +342,62 @@ onMounted(async () => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--border-color);
|
||||
border-top: 3px solid var(--primary-color, #007bff);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 月份标题样式 */
|
||||
.month-header {
|
||||
padding: 40px 20px 20px 20px;
|
||||
@@ -339,11 +472,9 @@ onMounted(async () => {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.photo-item:hover {
|
||||
@@ -352,16 +483,46 @@ onMounted(async () => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.photo-placeholder {
|
||||
.photo-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.photo-image:not([src]) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 图片加载失败显示 */
|
||||
.photo-error {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-color-secondary);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.photo-error span:first-child {
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 开发调试信息 */
|
||||
.photo-debug-info {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.photo-ratio {
|
||||
@@ -388,21 +549,21 @@ onMounted(async () => {
|
||||
.month-header {
|
||||
padding: 32px 16px 16px 16px;
|
||||
}
|
||||
|
||||
|
||||
.month-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
.memory-card {
|
||||
margin: 12px 16px;
|
||||
}
|
||||
|
||||
|
||||
.date-card {
|
||||
padding: 12px 16px 8px 16px;
|
||||
}
|
||||
|
||||
|
||||
.photos-grid {
|
||||
margin: 0 16px 20px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Reference in new issue
Block a user