mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-03-08 07:29:32 +08:00
Compare commits
4 Commits
7433bc2e7e
...
5e3defc63d
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e3defc63d | |||
| ea99a31248 | |||
| 86d6517267 | |||
| 2e5eeaf425 |
@@ -229,27 +229,65 @@ const panelPos = ref({ x: 0, y: 0 })
|
|||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragStart = ref({ x: 0, y: 0 })
|
const dragStart = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
// 初始化面板位置(移动端居中,桌面端右上角)
|
// 计算图片实际显示区域(与ImageView保持一致)
|
||||||
|
const getImageDisplayBounds = () => {
|
||||||
|
const windowWidth = window.innerWidth
|
||||||
|
const windowHeight = window.innerHeight
|
||||||
|
|
||||||
|
// 必应图片通常是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 = () => {
|
const initPanelPosition = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const windowWidth = window.innerWidth
|
const bounds = getImageDisplayBounds()
|
||||||
const windowHeight = window.innerHeight
|
const isMobile = window.innerWidth < 640 // sm breakpoint
|
||||||
const isMobile = windowWidth < 640 // sm breakpoint
|
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// 移动端:居中显示
|
// 移动端:在图片区域内居中显示
|
||||||
|
const panelWidth = Math.min(bounds.width - 16, window.innerWidth - 16)
|
||||||
const panelHeight = 580 // 估计高度
|
const panelHeight = 580 // 估计高度
|
||||||
panelPos.value = {
|
panelPos.value = {
|
||||||
x: 8,
|
x: Math.max(bounds.left, bounds.left + (bounds.width - panelWidth) / 2),
|
||||||
y: Math.max(8, (windowHeight - panelHeight) / 2)
|
y: Math.max(bounds.top + 8, bounds.top + (bounds.height - panelHeight) / 2)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 桌面端:右上角
|
// 桌面端:在图片区域右上角
|
||||||
const panelWidth = Math.min(420, windowWidth * 0.9)
|
const panelWidth = Math.min(420, bounds.width * 0.9)
|
||||||
const panelHeight = 600
|
const panelHeight = 600
|
||||||
panelPos.value = {
|
panelPos.value = {
|
||||||
x: windowWidth - panelWidth - 40,
|
x: bounds.right - panelWidth - 40,
|
||||||
y: Math.min(80, (windowHeight - panelHeight) / 2)
|
y: Math.max(bounds.top + 80, bounds.top + (bounds.height - panelHeight) / 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,15 +435,19 @@ const onDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
const newX = clientX - dragStart.value.x
|
const newX = clientX - dragStart.value.x
|
||||||
const newY = clientY - dragStart.value.y
|
const newY = clientY - dragStart.value.y
|
||||||
|
|
||||||
// 限制在视口内
|
// 限制在图片实际显示区域内
|
||||||
if (calendarPanel.value) {
|
if (calendarPanel.value) {
|
||||||
const rect = calendarPanel.value.getBoundingClientRect()
|
const rect = calendarPanel.value.getBoundingClientRect()
|
||||||
const maxX = window.innerWidth - rect.width
|
const bounds = getImageDisplayBounds()
|
||||||
const maxY = window.innerHeight - rect.height
|
|
||||||
|
const minX = bounds.left
|
||||||
|
const maxX = bounds.right - rect.width
|
||||||
|
const minY = bounds.top
|
||||||
|
const maxY = bounds.bottom - rect.height
|
||||||
|
|
||||||
panelPos.value = {
|
panelPos.value = {
|
||||||
x: Math.max(0, Math.min(newX, maxX)),
|
x: Math.max(minX, Math.min(newX, maxX)),
|
||||||
y: Math.max(0, Math.min(newY, maxY))
|
y: Math.max(minY, Math.min(newY, maxY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,52 @@
|
|||||||
<div class="w-12 h-12 border-4 border-white/20 border-t-white rounded-full animate-spin"></div>
|
<div class="w-12 h-12 border-4 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="todayImage" class="relative h-full w-full group">
|
<div v-else-if="latestImage" class="relative h-full w-full group">
|
||||||
<!-- 背景图片 -->
|
<!-- 背景图片 -->
|
||||||
<div class="absolute inset-0">
|
<div class="absolute inset-0">
|
||||||
<img
|
<img
|
||||||
:src="getTodayImageUrl()"
|
:src="getLatestImageUrl()"
|
||||||
:alt="todayImage.title || 'Today\'s Bing Image'"
|
:alt="latestImage.title || 'Latest Bing Image'"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 更新提示(仅在非今日图片时显示) - 右上角简约徽章 -->
|
||||||
|
<div v-if="!isToday" class="absolute top-4 right-4 md:top-8 md:right-8 z-20">
|
||||||
|
<div class="flex items-center gap-1.5 px-3 py-1.5 bg-black/30 backdrop-blur-md rounded-full border border-white/10 text-white/70 text-xs">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>下次更新 {{ nextUpdateTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 内容叠加层 -->
|
<!-- 内容叠加层 -->
|
||||||
<div class="relative h-full flex flex-col justify-end p-8 md:p-16 z-10">
|
<div class="relative h-full flex flex-col justify-end p-8 md:p-16 z-10">
|
||||||
<div class="max-w-4xl space-y-4 transform transition-transform duration-500 group-hover:translate-y-[-10px]">
|
<div class="max-w-4xl space-y-4 transform transition-transform duration-500 group-hover:translate-y-[-10px]">
|
||||||
<div class="inline-block px-4 py-2 bg-white/10 backdrop-blur-md rounded-full text-white/90 text-sm font-medium">
|
<div class="inline-block px-4 py-2 bg-white/10 backdrop-blur-md rounded-full text-white/90 text-sm font-medium">
|
||||||
今日精选 · {{ formatDate(todayImage.date) }}
|
{{ isToday ? '今日精选' : '最新图片' }} · {{ formatDate(latestImage.date) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-4xl md:text-6xl font-bold text-white leading-tight drop-shadow-2xl">
|
<h1 class="text-4xl md:text-6xl font-bold text-white leading-tight drop-shadow-2xl">
|
||||||
{{ todayImage.title || '必应每日一图' }}
|
{{ latestImage.title || '必应每日一图' }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p v-if="todayImage.copyright" class="text-lg md:text-xl text-white/80 max-w-2xl">
|
<p v-if="latestImage.copyright" class="text-lg md:text-xl text-white/80 max-w-2xl">
|
||||||
{{ todayImage.copyright }}
|
{{ latestImage.copyright }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex gap-4 pt-4">
|
<div class="flex gap-4 pt-4">
|
||||||
<button
|
<button
|
||||||
@click="viewImage(todayImage.date!)"
|
@click="viewImage(latestImage.date!)"
|
||||||
class="px-6 py-3 bg-white text-gray-900 rounded-lg font-semibold hover:bg-white/90 transition-all transform hover:scale-105 shadow-xl"
|
class="px-6 py-3 bg-white text-gray-900 rounded-lg font-semibold hover:bg-white/90 transition-all transform hover:scale-105 shadow-xl"
|
||||||
>
|
>
|
||||||
查看大图
|
查看大图
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="todayImage.copyrightlink"
|
v-if="latestImage.copyrightlink"
|
||||||
@click="openCopyrightLink(todayImage.copyrightlink)"
|
@click="openCopyrightLink(latestImage.copyrightlink)"
|
||||||
class="px-6 py-3 bg-white/10 backdrop-blur-md text-white rounded-lg font-semibold hover:bg-white/20 transition-all border border-white/30"
|
class="px-6 py-3 bg-white/10 backdrop-blur-md text-white rounded-lg font-semibold hover:bg-white/20 transition-all border border-white/30"
|
||||||
>
|
>
|
||||||
了解更多
|
了解更多
|
||||||
@@ -235,7 +245,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useTodayImage, useImageList } from '@/composables/useImages'
|
import { useImageList } from '@/composables/useImages'
|
||||||
import { bingPaperApi } from '@/lib/api-service'
|
import { bingPaperApi } from '@/lib/api-service'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
@@ -248,12 +258,62 @@ import {
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 获取今日图片
|
// 顶部最新图片(独立加载,不受筛选影响)
|
||||||
const { image: todayImage, loading: todayLoading } = useTodayImage()
|
const latestImage = ref<any>(null)
|
||||||
|
const todayLoading = ref(false)
|
||||||
|
|
||||||
// 获取图片列表(使用服务端分页和筛选,每页15张)
|
// 历史图片列表(使用服务端分页和筛选,每页15张)
|
||||||
const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(15)
|
const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(15)
|
||||||
|
|
||||||
|
// 加载顶部最新图片
|
||||||
|
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(() => {
|
||||||
|
if (!latestImage.value?.date) return false
|
||||||
|
const imageDate = new Date(latestImage.value.date).toDateString()
|
||||||
|
const today = new Date().toDateString()
|
||||||
|
return imageDate === today
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算下次更新时间提示
|
||||||
|
const nextUpdateTime = computed(() => {
|
||||||
|
const now = new Date()
|
||||||
|
const hours = now.getHours()
|
||||||
|
|
||||||
|
// 更新时间点:8:20, 12:20, 16:20, 20:20, 0:20, 4:20
|
||||||
|
const updateHours = [0, 4, 8, 12, 16, 20]
|
||||||
|
const updateMinute = 20
|
||||||
|
|
||||||
|
// 找到下一个更新时间点
|
||||||
|
for (const hour of updateHours) {
|
||||||
|
if (hours < hour || (hours === hour && now.getMinutes() < updateMinute)) {
|
||||||
|
return `${String(hour).padStart(2, '0')}:${String(updateMinute).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果今天没有下一个更新点,返回明天的第一个更新点
|
||||||
|
return `次日 00:20`
|
||||||
|
})
|
||||||
|
|
||||||
// 筛选相关状态
|
// 筛选相关状态
|
||||||
const selectedYear = ref('')
|
const selectedYear = ref('')
|
||||||
const selectedMonth = ref('')
|
const selectedMonth = ref('')
|
||||||
@@ -267,11 +327,11 @@ let observer: IntersectionObserver | null = null
|
|||||||
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
const loadMoreTrigger = ref<HTMLElement | null>(null)
|
||||||
let loadMoreObserver: IntersectionObserver | null = null
|
let loadMoreObserver: IntersectionObserver | null = null
|
||||||
|
|
||||||
// 计算可用的年份列表(基于当前日期生成,从2020年到当前年份)
|
// 计算可用的年份列表(基于当前日期生成,计算前20年)
|
||||||
const availableYears = computed(() => {
|
const availableYears = computed(() => {
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
const years: number[] = []
|
const years: number[] = []
|
||||||
for (let year = currentYear; year >= 2020; year--) {
|
for (let year = currentYear; year >= currentYear - 20; year--) {
|
||||||
years.push(year)
|
years.push(year)
|
||||||
}
|
}
|
||||||
return years
|
return years
|
||||||
@@ -424,8 +484,10 @@ const setupLoadMoreObserver = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化时设置无限滚动
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadLatestImage()
|
||||||
|
|
||||||
if (images.value.length > 0) {
|
if (images.value.length > 0) {
|
||||||
imageVisibility.value = new Array(images.value.length).fill(false)
|
imageVisibility.value = new Array(images.value.length).fill(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -456,9 +518,10 @@ const formatDate = (dateStr?: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取今日图片 URL
|
// 获取最新图片 URL(顶部大图使用UHD高清)
|
||||||
const getTodayImageUrl = () => {
|
const getLatestImageUrl = () => {
|
||||||
return bingPaperApi.getTodayImageUrl('UHD', 'jpg')
|
if (!latestImage.value?.date) return ''
|
||||||
|
return bingPaperApi.getImageUrlByDate(latestImage.value.date, 'UHD', 'jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取图片 URL(缩略图 - 使用较小分辨率节省流量)
|
// 获取图片 URL(缩略图 - 使用较小分辨率节省流量)
|
||||||
@@ -487,3 +550,25 @@ const openCopyrightLink = (link: string) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 隐藏滚动条但保持滚动功能 */
|
||||||
|
body {
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE 10+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE 10+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -43,9 +43,16 @@
|
|||||||
<div
|
<div
|
||||||
v-if="showInfo && !showCalendar"
|
v-if="showInfo && !showCalendar"
|
||||||
ref="infoPanel"
|
ref="infoPanel"
|
||||||
class="fixed w-[90%] max-w-md bg-black/40 backdrop-blur-lg rounded-xl p-4 transform transition-all duration-300 z-10 select-none"
|
class="fixed w-[90%] max-w-md bg-black/40 backdrop-blur-lg rounded-xl p-4 z-20 select-none"
|
||||||
:style="{ left: infoPanelPos.x + 'px', top: infoPanelPos.y + 'px' }"
|
:class="{
|
||||||
:class="{ 'opacity-100': showInfo && !showCalendar, 'opacity-0 pointer-events-none': showCalendar }"
|
'opacity-100': showInfo && !showCalendar,
|
||||||
|
'opacity-0 pointer-events-none': showCalendar,
|
||||||
|
'transition-opacity duration-300': !isDragging
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
transform: `translate(${infoPanelPos.x}px, ${infoPanelPos.y}px)`,
|
||||||
|
willChange: isDragging ? 'transform' : 'auto'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<!-- 拖动手柄 -->
|
<!-- 拖动手柄 -->
|
||||||
<div
|
<div
|
||||||
@@ -208,16 +215,56 @@ const infoPanel = ref<HTMLElement | null>(null)
|
|||||||
const infoPanelPos = ref({ x: 0, y: 0 })
|
const infoPanelPos = ref({ x: 0, y: 0 })
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragStart = ref({ x: 0, y: 0 })
|
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 = () => {
|
const initPanelPosition = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const windowWidth = window.innerWidth
|
const bounds = getImageDisplayBounds()
|
||||||
const windowHeight = window.innerHeight
|
const panelWidth = Math.min(bounds.width * 0.9, 448) // max-w-md = 448px
|
||||||
const panelWidth = Math.min(windowWidth * 0.9, 448) // max-w-md = 448px
|
|
||||||
infoPanelPos.value = {
|
infoPanelPos.value = {
|
||||||
x: (windowWidth - panelWidth) / 2,
|
x: bounds.left + (bounds.width - panelWidth) / 2,
|
||||||
y: windowHeight - 200 // 距底部200px
|
y: Math.max(bounds.top, bounds.bottom - 280) // 距底部280px,避免与控制栏重叠
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,42 +282,61 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
|||||||
y: clientY - infoPanelPos.value.y
|
y: clientY - infoPanelPos.value.y
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('mousemove', onDrag)
|
document.addEventListener('mousemove', onDrag, { passive: false })
|
||||||
document.addEventListener('mouseup', stopDrag)
|
document.addEventListener('mouseup', stopDrag)
|
||||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
document.addEventListener('touchmove', onDrag, { passive: false })
|
||||||
document.addEventListener('touchend', stopDrag)
|
document.addEventListener('touchend', stopDrag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖动中
|
// 拖动中 - 使用 requestAnimationFrame 优化性能
|
||||||
const onDrag = (e: MouseEvent | TouchEvent) => {
|
const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||||
if (!isDragging.value) return
|
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
|
// 使用 requestAnimationFrame 进行节流优化
|
||||||
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
|
animationFrameId = requestAnimationFrame(() => {
|
||||||
|
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
||||||
const newX = clientX - dragStart.value.x
|
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
|
||||||
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
|
|
||||||
|
|
||||||
infoPanelPos.value = {
|
const newX = clientX - dragStart.value.x
|
||||||
x: Math.max(0, Math.min(newX, maxX)),
|
const newY = clientY - dragStart.value.y
|
||||||
y: Math.max(0, Math.min(newY, maxY))
|
|
||||||
|
// 限制在图片实际显示区域内,考虑底部控制栏高度(约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 = () => {
|
const stopDrag = () => {
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
|
|
||||||
|
// 清理动画帧
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId)
|
||||||
|
animationFrameId = null
|
||||||
|
}
|
||||||
|
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('touchmove', onDrag)
|
document.removeEventListener('touchmove', onDrag)
|
||||||
@@ -430,6 +496,11 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('touchmove', onDrag)
|
document.removeEventListener('touchmove', onDrag)
|
||||||
document.removeEventListener('touchend', stopDrag)
|
document.removeEventListener('touchend', stopDrag)
|
||||||
|
|
||||||
|
// 清理动画帧
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user