diff --git a/webapp/src/views/Home.vue b/webapp/src/views/Home.vue index 60dfc31..e1b384e 100644 --- a/webapp/src/views/Home.vue +++ b/webapp/src/views/Home.vue @@ -258,12 +258,33 @@ import { const router = useRouter() -// 获取图片列表(使用服务端分页和筛选,每页15张) +// 顶部最新图片(独立加载,不受筛选影响) +const latestImage = ref(null) +const todayLoading = ref(false) + +// 历史图片列表(使用服务端分页和筛选,每页15张) const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(15) -// 从图片列表获取最新图片作为顶部展示 -const latestImage = computed(() => images.value.length > 0 ? images.value[0] : null) -const todayLoading = computed(() => loading.value && images.value.length === 0) +// 加载顶部最新图片 +const loadLatestImage = async () => { + todayLoading.value = true + try { + const params = { page: 1, page_size: 1 } + const result = await bingPaperApi.getImages(params) + if (result.length > 0) { + latestImage.value = result[0] + } + } catch (error) { + console.error('Failed to load latest image:', error) + } finally { + todayLoading.value = false + } +} + +// 初始化加载顶部图片 +onMounted(() => { + loadLatestImage() +}) // 判断最新图片是否为今天的图片 const isToday = computed(() => { @@ -306,11 +327,11 @@ let observer: IntersectionObserver | null = null const loadMoreTrigger = ref(null) let loadMoreObserver: IntersectionObserver | null = null -// 计算可用的年份列表(基于当前日期生成,从2020年到当前年份) +// 计算可用的年份列表(基于当前日期生成,计算前20年) const availableYears = computed(() => { const currentYear = new Date().getFullYear() const years: number[] = [] - for (let year = currentYear; year >= 2020; year--) { + for (let year = currentYear; year >= currentYear - 20; year--) { years.push(year) } return years @@ -463,8 +484,10 @@ const setupLoadMoreObserver = () => { } } -// 初始化时设置无限滚动 +// 初始化 onMounted(() => { + loadLatestImage() + if (images.value.length > 0) { imageVisibility.value = new Array(images.value.length).fill(false) setTimeout(() => { @@ -527,3 +550,25 @@ const openCopyrightLink = (link: string) => { overflow: hidden; } + + diff --git a/webapp/src/views/ImageView.vue b/webapp/src/views/ImageView.vue index 9214fee..b811f70 100644 --- a/webapp/src/views/ImageView.vue +++ b/webapp/src/views/ImageView.vue @@ -43,9 +43,16 @@
(null) const infoPanelPos = ref({ x: 0, y: 0 }) const isDragging = ref(false) const dragStart = ref({ x: 0, y: 0 }) +let animationFrameId: number | null = null -// 初始化浮窗位置(居中偏下) +// 计算图片实际显示区域(考虑图片宽高比和object-contain) +const getImageDisplayBounds = () => { + const windowWidth = window.innerWidth + const windowHeight = window.innerHeight + + // 必应图片通常是16:9或类似宽高比 + // 使用UHD分辨率: 1920x1080 (16:9) + const imageAspectRatio = 16 / 9 + const windowAspectRatio = windowWidth / windowHeight + + let displayWidth: number + let displayHeight: number + let offsetX: number + let offsetY: number + + if (windowAspectRatio > imageAspectRatio) { + // 窗口更宽,图片上下占满,左右留黑边 + displayHeight = windowHeight + displayWidth = displayHeight * imageAspectRatio + offsetX = (windowWidth - displayWidth) / 2 + offsetY = 0 + } else { + // 窗口更高,图片左右占满,上下留黑边 + displayWidth = windowWidth + displayHeight = displayWidth / imageAspectRatio + offsetX = 0 + offsetY = (windowHeight - displayHeight) / 2 + } + + return { + left: offsetX, + top: offsetY, + right: offsetX + displayWidth, + bottom: offsetY + displayHeight, + width: displayWidth, + height: displayHeight + } +} + +// 初始化浮窗位置(居中偏下,限制在图片显示区域内) const initPanelPosition = () => { if (typeof window !== 'undefined') { - const windowWidth = window.innerWidth - const windowHeight = window.innerHeight - const panelWidth = Math.min(windowWidth * 0.9, 448) // max-w-md = 448px + const bounds = getImageDisplayBounds() + const panelWidth = Math.min(bounds.width * 0.9, 448) // max-w-md = 448px + const bottomControlHeight = 80 // 底部控制栏高度 + infoPanelPos.value = { - x: (windowWidth - panelWidth) / 2, - y: windowHeight - 200 // 距底部200px + x: bounds.left + (bounds.width - panelWidth) / 2, + y: Math.max(bounds.top, bounds.bottom - 280) // 距底部280px,避免与控制栏重叠 } } } @@ -235,42 +283,61 @@ const startDrag = (e: MouseEvent | TouchEvent) => { y: clientY - infoPanelPos.value.y } - document.addEventListener('mousemove', onDrag) + document.addEventListener('mousemove', onDrag, { passive: false }) document.addEventListener('mouseup', stopDrag) document.addEventListener('touchmove', onDrag, { passive: false }) document.addEventListener('touchend', stopDrag) } -// 拖动中 +// 拖动中 - 使用 requestAnimationFrame 优化性能 const onDrag = (e: MouseEvent | TouchEvent) => { if (!isDragging.value) return - if (e instanceof TouchEvent) { - e.preventDefault() + e.preventDefault() + + // 取消之前的动画帧 + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId) } - const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX - const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY - - const newX = clientX - dragStart.value.x - const newY = clientY - dragStart.value.y - - // 限制在视口内 - if (infoPanel.value) { - const rect = infoPanel.value.getBoundingClientRect() - const maxX = window.innerWidth - rect.width - const maxY = window.innerHeight - rect.height + // 使用 requestAnimationFrame 进行节流优化 + animationFrameId = requestAnimationFrame(() => { + const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX + const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY - infoPanelPos.value = { - x: Math.max(0, Math.min(newX, maxX)), - y: Math.max(0, Math.min(newY, maxY)) + const newX = clientX - dragStart.value.x + const newY = clientY - dragStart.value.y + + // 限制在图片实际显示区域内,考虑底部控制栏高度(约80px) + if (infoPanel.value) { + const rect = infoPanel.value.getBoundingClientRect() + const bounds = getImageDisplayBounds() + + const minX = bounds.left + const maxX = bounds.right - rect.width + const minY = bounds.top + const maxY = bounds.bottom - rect.height - 80 // 预留底部控制栏空间 + + infoPanelPos.value = { + x: Math.max(minX, Math.min(newX, maxX)), + y: Math.max(minY, Math.min(newY, maxY)) + } } - } + + animationFrameId = null + }) } // 停止拖动 const stopDrag = () => { isDragging.value = false + + // 清理动画帧 + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId) + animationFrameId = null + } + document.removeEventListener('mousemove', onDrag) document.removeEventListener('mouseup', stopDrag) document.removeEventListener('touchmove', onDrag) @@ -430,6 +497,11 @@ onUnmounted(() => { document.removeEventListener('mouseup', stopDrag) document.removeEventListener('touchmove', onDrag) document.removeEventListener('touchend', stopDrag) + + // 清理动画帧 + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId) + } } })