From 86d6517267f8c16274bec10596f3468caa9a80ac Mon Sep 17 00:00:00 2001 From: hanxuanyu <2252193204@qq.com> Date: Thu, 29 Jan 2026 12:37:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E9=A1=B5=E9=9D=A2=E7=BB=86?= =?UTF-8?q?=E8=8A=82=E4=BC=98=E5=8C=96=EF=BC=9A=E5=9B=BE=E7=89=87=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E9=A1=B5=E9=9D=A2=E6=82=AC=E6=B5=AE=E7=AA=97=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E8=A2=AB=E9=81=AE=E6=8C=A1=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9B=E9=A6=96=E9=A1=B5=E5=A4=A7=E5=9B=BE?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=8B=AC=E7=AB=8B=E7=9A=84=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=EF=BC=8C=E4=B8=8D=E5=8F=97=E4=B8=8B=E6=96=B9?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E7=BB=93=E6=9E=9C=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webapp/src/views/Home.vue | 59 +++++++++++++-- webapp/src/views/ImageView.vue | 128 +++++++++++++++++++++++++-------- 2 files changed, 152 insertions(+), 35 deletions(-) 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) + } } })