mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 07:29:33 +08:00
增加多地区每日图片抓取能力
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
<div class="fixed inset-0 z-40">
|
||||
<div
|
||||
ref="calendarPanel"
|
||||
class="fixed bg-gradient-to-br from-black/30 via-black/20 to-black/30 backdrop-blur-xl rounded-3xl p-3 sm:p-4 w-[calc(100%-1rem)] sm:w-full max-w-[95vw] sm:max-w-[420px] shadow-2xl border border-white/10 cursor-move select-none"
|
||||
class="fixed bg-gradient-to-br from-black/30 via-black/20 to-black/30 backdrop-blur-xl rounded-3xl p-3 sm:p-4 w-[calc(100%-1rem)] sm:w-full max-w-[95vw] sm:max-w-[420px] shadow-2xl border border-white/10 cursor-move select-none touch-none"
|
||||
:style="{ left: panelPos.x + 'px', top: panelPos.y + 'px' }"
|
||||
@mousedown="startDrag"
|
||||
@touchstart="startDrag"
|
||||
@touchstart.passive="startDrag"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 拖动手柄指示器 -->
|
||||
@@ -28,7 +28,7 @@
|
||||
<!-- 年月选择器 -->
|
||||
<div class="flex items-center justify-center gap-1 sm:gap-1.5 mb-0.5">
|
||||
<!-- 年份选择 -->
|
||||
<Select v-model="currentYearString" @update:modelValue="onYearChange">
|
||||
<Select v-model="currentYearString">
|
||||
<SelectTrigger
|
||||
class="w-[90px] sm:w-[105px] h-6 sm:h-7 bg-white/10 text-white border-white/20 hover:bg-white/20 backdrop-blur-md font-bold text-xs sm:text-sm px-1.5 sm:px-2"
|
||||
@click.stop
|
||||
@@ -49,7 +49,7 @@
|
||||
</Select>
|
||||
|
||||
<!-- 月份选择 -->
|
||||
<Select v-model="currentMonthString" @update:modelValue="onMonthChange">
|
||||
<Select v-model="currentMonthString">
|
||||
<SelectTrigger
|
||||
class="w-[65px] sm:w-[75px] h-6 sm:h-7 bg-white/10 text-white border-white/20 hover:bg-white/20 backdrop-blur-md font-bold text-xs sm:text-sm px-1.5 sm:px-2"
|
||||
@click.stop
|
||||
@@ -214,7 +214,8 @@ interface CalendarDay {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
selectedDate: string // YYYY-MM-DD
|
||||
selectedDate?: string,
|
||||
mkt?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -229,10 +230,14 @@ const panelPos = ref({ x: 0, y: 0 })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
|
||||
// 响应式窗口大小
|
||||
const windowSize = ref({ width: window.innerWidth, height: window.innerHeight })
|
||||
const isMobile = computed(() => windowSize.value.width < 768)
|
||||
|
||||
// 计算图片实际显示区域(与ImageView保持一致)
|
||||
const getImageDisplayBounds = () => {
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
const windowWidth = windowSize.value.width
|
||||
const windowHeight = windowSize.value.height
|
||||
|
||||
// 必应图片通常是16:9或类似宽高比
|
||||
const imageAspectRatio = 16 / 9
|
||||
@@ -271,11 +276,10 @@ const getImageDisplayBounds = () => {
|
||||
const initPanelPosition = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const bounds = getImageDisplayBounds()
|
||||
const isMobile = window.innerWidth < 640 // sm breakpoint
|
||||
|
||||
if (isMobile) {
|
||||
// 移动端:在图片区域内居中显示
|
||||
const panelWidth = Math.min(bounds.width - 16, window.innerWidth - 16)
|
||||
if (isMobile.value) {
|
||||
// 移动端:居中显示,尽量在图片内,但不强求
|
||||
const panelWidth = Math.min(bounds.width - 16, windowSize.value.width - 16)
|
||||
const panelHeight = 580 // 估计高度
|
||||
panelPos.value = {
|
||||
x: Math.max(bounds.left, bounds.left + (bounds.width - panelWidth) / 2),
|
||||
@@ -316,20 +320,6 @@ const currentMonthString = computed({
|
||||
}
|
||||
})
|
||||
|
||||
// 年份改变处理
|
||||
const onYearChange = (value: any) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
currentYear.value = Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
// 月份改变处理
|
||||
const onMonthChange = (value: any) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
currentMonth.value = Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成年份选项(从2009年到当前年份+10年)
|
||||
const yearOptions = computed(() => {
|
||||
const currentYearValue = new Date().getFullYear()
|
||||
@@ -379,8 +369,20 @@ const loadHolidaysForYear = async (year: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口缩放处理
|
||||
const handleResize = () => {
|
||||
windowSize.value = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
}
|
||||
initPanelPosition()
|
||||
}
|
||||
|
||||
// 组件挂载时加载当前年份的假期数据
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
const currentYearValue = currentYear.value
|
||||
loadHolidaysForYear(currentYearValue)
|
||||
// 预加载前后一年的数据
|
||||
@@ -404,7 +406,9 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
if (e instanceof MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
isDragging.value = true
|
||||
|
||||
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
||||
@@ -417,7 +421,7 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
|
||||
document.addEventListener('mousemove', onDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
||||
document.addEventListener('touchmove', onDrag, { passive: true })
|
||||
document.addEventListener('touchend', stopDrag)
|
||||
}
|
||||
|
||||
@@ -425,7 +429,7 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
if (e instanceof TouchEvent) {
|
||||
if (e instanceof MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
@@ -435,15 +439,26 @@ const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||
const newX = clientX - dragStart.value.x
|
||||
const newY = clientY - dragStart.value.y
|
||||
|
||||
// 限制在图片实际显示区域内
|
||||
// 限制在有效区域内
|
||||
if (calendarPanel.value) {
|
||||
const rect = calendarPanel.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
|
||||
let minX, maxX, minY, maxY
|
||||
|
||||
if (isMobile.value) {
|
||||
// 移动端:不限制区域,限制在视口内即可
|
||||
minX = 0
|
||||
maxX = windowSize.value.width - rect.width
|
||||
minY = 0
|
||||
maxY = windowSize.value.height - rect.height
|
||||
} else {
|
||||
// 桌面端:限制在图片实际显示区域内
|
||||
const bounds = getImageDisplayBounds()
|
||||
minX = bounds.left
|
||||
maxX = bounds.right - rect.width
|
||||
minY = bounds.top
|
||||
maxY = bounds.bottom - rect.height
|
||||
}
|
||||
|
||||
panelPos.value = {
|
||||
x: Math.max(minX, Math.min(newX, maxX)),
|
||||
@@ -517,7 +532,7 @@ const createDayObject = (date: Date, isCurrentMonth: boolean): CalendarDay => {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const selectedDate = new Date(props.selectedDate)
|
||||
const selectedDate = new Date(props.selectedDate || new Date())
|
||||
selectedDate.setHours(0, 0, 0, 0)
|
||||
|
||||
// 转换为农历
|
||||
@@ -626,6 +641,9 @@ const goToToday = () => {
|
||||
// 清理
|
||||
import { onUnmounted } from 'vue'
|
||||
onUnmounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
|
||||
@@ -2,11 +2,12 @@ import { ref, onMounted, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { bingPaperApi } from '@/lib/api-service'
|
||||
import type { ImageMeta } from '@/lib/api-types'
|
||||
import { getDefaultMkt } from '@/lib/mkt-utils'
|
||||
|
||||
/**
|
||||
* 获取今日图片
|
||||
*/
|
||||
export function useTodayImage() {
|
||||
export function useTodayImage(mkt?: string) {
|
||||
const image = ref<ImageMeta | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
@@ -15,7 +16,7 @@ export function useTodayImage() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
image.value = await bingPaperApi.getTodayImageMeta()
|
||||
image.value = await bingPaperApi.getTodayImageMeta(mkt || getDefaultMkt())
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
console.error('Failed to fetch today image:', e)
|
||||
@@ -46,8 +47,9 @@ export function useImageList(pageSize = 30) {
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const currentMonth = ref<string | undefined>(undefined)
|
||||
const currentMkt = ref<string | undefined>(getDefaultMkt())
|
||||
|
||||
const fetchImages = async (page = 1, month?: string) => {
|
||||
const fetchImages = async (page = 1, month?: string, mkt?: string) => {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
@@ -55,7 +57,8 @@ export function useImageList(pageSize = 30) {
|
||||
try {
|
||||
const params: any = {
|
||||
page,
|
||||
page_size: pageSize
|
||||
page_size: pageSize,
|
||||
mkt: mkt || currentMkt.value || getDefaultMkt()
|
||||
}
|
||||
if (month) {
|
||||
params.month = month
|
||||
@@ -84,7 +87,7 @@ export function useImageList(pageSize = 30) {
|
||||
|
||||
const loadMore = () => {
|
||||
if (!loading.value && hasMore.value) {
|
||||
fetchImages(currentPage.value + 1, currentMonth.value)
|
||||
fetchImages(currentPage.value + 1, currentMonth.value, currentMkt.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +95,14 @@ export function useImageList(pageSize = 30) {
|
||||
currentMonth.value = month
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
fetchImages(1, month)
|
||||
fetchImages(1, month, currentMkt.value)
|
||||
}
|
||||
|
||||
const filterByMkt = (mkt?: string) => {
|
||||
currentMkt.value = mkt
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
fetchImages(1, currentMonth.value, mkt)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -106,10 +116,11 @@ export function useImageList(pageSize = 30) {
|
||||
hasMore,
|
||||
loadMore,
|
||||
filterByMonth,
|
||||
filterByMkt,
|
||||
refetch: () => {
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
fetchImages(1, currentMonth.value)
|
||||
fetchImages(1, currentMonth.value, currentMkt.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +128,7 @@ export function useImageList(pageSize = 30) {
|
||||
/**
|
||||
* 获取指定日期的图片
|
||||
*/
|
||||
export function useImageByDate(dateRef: Ref<string>) {
|
||||
export function useImageByDate(dateRef: Ref<string>, mktRef?: Ref<string | undefined>) {
|
||||
const image = ref<ImageMeta | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
@@ -126,7 +137,7 @@ export function useImageByDate(dateRef: Ref<string>) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
image.value = await bingPaperApi.getImageMetaByDate(dateRef.value)
|
||||
image.value = await bingPaperApi.getImageMetaByDate(dateRef.value, mktRef?.value || getDefaultMkt())
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
console.error(`Failed to fetch image for date ${dateRef.value}:`, e)
|
||||
@@ -135,10 +146,16 @@ export function useImageByDate(dateRef: Ref<string>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听日期变化,自动重新获取
|
||||
watch(dateRef, () => {
|
||||
fetchImage()
|
||||
}, { immediate: true })
|
||||
// 监听日期和地区变化,自动重新获取
|
||||
if (mktRef) {
|
||||
watch([dateRef, mktRef], () => {
|
||||
fetchImage()
|
||||
}, { immediate: true })
|
||||
} else {
|
||||
watch(dateRef, () => {
|
||||
fetchImage()
|
||||
}, { immediate: true })
|
||||
}
|
||||
|
||||
return {
|
||||
image,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
UpdateTokenRequest,
|
||||
ChangePasswordRequest,
|
||||
Config,
|
||||
Region,
|
||||
ImageMeta,
|
||||
ImageListParams,
|
||||
ManualFetchRequest,
|
||||
@@ -109,6 +110,7 @@ export class BingPaperApiService {
|
||||
if (params?.page) searchParams.set('page', params.page.toString())
|
||||
if (params?.page_size) searchParams.set('page_size', params.page_size.toString())
|
||||
if (params?.month) searchParams.set('month', params.month)
|
||||
if (params?.mkt) searchParams.set('mkt', params.mkt)
|
||||
|
||||
const queryString = searchParams.toString()
|
||||
const endpoint = queryString ? `/images?${queryString}` : '/images'
|
||||
@@ -116,48 +118,61 @@ export class BingPaperApiService {
|
||||
return apiClient.get<ImageMeta[]>(endpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的地区列表
|
||||
*/
|
||||
async getRegions(): Promise<Region[]> {
|
||||
return apiClient.get<Region[]>('/regions')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日图片元数据
|
||||
*/
|
||||
async getTodayImageMeta(): Promise<ImageMeta> {
|
||||
return apiClient.get<ImageMeta>('/image/today/meta')
|
||||
async getTodayImageMeta(mkt?: string): Promise<ImageMeta> {
|
||||
const endpoint = mkt ? `/image/today/meta?mkt=${mkt}` : '/image/today/meta'
|
||||
return apiClient.get<ImageMeta>(endpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期图片元数据
|
||||
*/
|
||||
async getImageMetaByDate(date: string): Promise<ImageMeta> {
|
||||
return apiClient.get<ImageMeta>(`/image/date/${date}/meta`)
|
||||
async getImageMetaByDate(date: string, mkt?: string): Promise<ImageMeta> {
|
||||
const endpoint = mkt ? `/image/date/${date}/meta?mkt=${mkt}` : `/image/date/${date}/meta`
|
||||
return apiClient.get<ImageMeta>(endpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机图片元数据
|
||||
*/
|
||||
async getRandomImageMeta(): Promise<ImageMeta> {
|
||||
return apiClient.get<ImageMeta>('/image/random/meta')
|
||||
async getRandomImageMeta(mkt?: string): Promise<ImageMeta> {
|
||||
const endpoint = mkt ? `/image/random/meta?mkt=${mkt}` : '/image/random/meta'
|
||||
return apiClient.get<ImageMeta>(endpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建图片 URL
|
||||
*/
|
||||
getTodayImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg'): string {
|
||||
getTodayImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg', mkt?: string): string {
|
||||
const params = new URLSearchParams({ variant, format })
|
||||
if (mkt) params.set('mkt', mkt)
|
||||
return `${apiConfig.baseURL}/image/today?${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建指定日期图片 URL
|
||||
*/
|
||||
getImageUrlByDate(date: string, variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg'): string {
|
||||
getImageUrlByDate(date: string, variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg', mkt?: string): string {
|
||||
const params = new URLSearchParams({ variant, format })
|
||||
if (mkt) params.set('mkt', mkt)
|
||||
return `${apiConfig.baseURL}/image/date/${date}?${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建随机图片 URL
|
||||
*/
|
||||
getRandomImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg'): string {
|
||||
getRandomImageUrl(variant: ImageVariant = 'UHD', format: ImageFormat = 'jpg', mkt?: string): string {
|
||||
const params = new URLSearchParams({ variant, format })
|
||||
if (mkt) params.set('mkt', mkt)
|
||||
return `${apiConfig.baseURL}/image/random?${params.toString()}`
|
||||
}
|
||||
|
||||
@@ -197,6 +212,7 @@ export const {
|
||||
manualFetch,
|
||||
manualCleanup,
|
||||
getImages,
|
||||
getRegions,
|
||||
getTodayImageMeta,
|
||||
getImageMetaByDate,
|
||||
getRandomImageMeta,
|
||||
|
||||
@@ -61,6 +61,11 @@ export interface Config {
|
||||
Token: TokenConfig
|
||||
Feature: FeatureConfig
|
||||
Web: WebConfig
|
||||
Fetcher: FetcherConfig
|
||||
}
|
||||
|
||||
export interface FetcherConfig {
|
||||
Regions: string[]
|
||||
}
|
||||
|
||||
export interface AdminConfig {
|
||||
@@ -69,6 +74,7 @@ export interface AdminConfig {
|
||||
|
||||
export interface APIConfig {
|
||||
Mode: string // 'local' | 'redirect'
|
||||
EnableMktFallback: boolean
|
||||
}
|
||||
|
||||
export interface CronConfig {
|
||||
@@ -147,6 +153,7 @@ export interface WebConfig {
|
||||
|
||||
export interface ImageMeta {
|
||||
date?: string
|
||||
mkt?: string
|
||||
title?: string
|
||||
copyright?: string
|
||||
copyrightlink?: string // 图片的详细版权链接(指向 Bing 搜索页面)
|
||||
@@ -173,6 +180,12 @@ export interface ImageListParams extends PaginationParams {
|
||||
page?: number // 页码(从1开始)
|
||||
page_size?: number // 每页数量
|
||||
month?: string // 按月份过滤(格式:YYYY-MM)
|
||||
mkt?: string // 地区编码
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ManualFetchRequest {
|
||||
|
||||
75
webapp/src/lib/mkt-utils.ts
Normal file
75
webapp/src/lib/mkt-utils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
const MKT_STORAGE_KEY = 'bing_paper_selected_mkt'
|
||||
const DEFAULT_MKT = 'zh-CN'
|
||||
|
||||
/**
|
||||
* 默认地区列表 (兜底用)
|
||||
*/
|
||||
export const DEFAULT_REGIONS = [
|
||||
{ value: 'zh-CN', label: '中国 (zh-CN)' },
|
||||
{ value: 'en-US', label: '美国 (en-US)' },
|
||||
{ value: 'ja-JP', label: '日本 (ja-JP)' },
|
||||
{ value: 'en-AU', label: '澳大利亚 (en-AU)' },
|
||||
{ value: 'en-GB', label: '英国 (en-GB)' },
|
||||
{ value: 'de-DE', label: '德国 (de-DE)' },
|
||||
{ value: 'en-NZ', label: '新西兰 (en-NZ)' },
|
||||
{ value: 'en-CA', label: '加拿大 (en-CA)' },
|
||||
{ value: 'fr-FR', label: '法国 (fr-FR)' },
|
||||
{ value: 'it-IT', label: '意大利 (it-IT)' },
|
||||
{ value: 'es-ES', label: '西班牙 (es-ES)' },
|
||||
{ value: 'pt-BR', label: '巴西 (pt-BR)' },
|
||||
{ value: 'ko-KR', label: '韩国 (ko-KR)' },
|
||||
{ value: 'en-IN', label: '印度 (en-IN)' },
|
||||
{ value: 'ru-RU', label: '俄罗斯 (ru-RU)' },
|
||||
{ value: 'zh-HK', label: '中国香港 (zh-HK)' },
|
||||
{ value: 'zh-TW', label: '中国台湾 (zh-TW)' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 支持的地区列表 (优先使用后端提供的)
|
||||
*/
|
||||
export let SUPPORTED_REGIONS = [...DEFAULT_REGIONS]
|
||||
|
||||
/**
|
||||
* 更新支持的地区列表
|
||||
*/
|
||||
export function setSupportedRegions(regions: typeof DEFAULT_REGIONS): void {
|
||||
SUPPORTED_REGIONS = regions
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器首选地区
|
||||
*/
|
||||
export function getBrowserMkt(): string {
|
||||
const lang = navigator.language || (navigator as any).userLanguage
|
||||
if (!lang) return DEFAULT_MKT
|
||||
|
||||
// 尝试精确匹配
|
||||
const exactMatch = SUPPORTED_REGIONS.find(r => r.value.toLowerCase() === lang.toLowerCase())
|
||||
if (exactMatch) return exactMatch.value
|
||||
|
||||
// 尝试模糊匹配 (前两个字符,如 en-GB 匹配 en-US)
|
||||
const prefix = lang.split('-')[0].toLowerCase()
|
||||
const prefixMatch = SUPPORTED_REGIONS.find(r => r.value.split('-')[0].toLowerCase() === prefix)
|
||||
if (prefixMatch) return prefixMatch.value
|
||||
|
||||
return DEFAULT_MKT
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选择的地区 (优先从 localStorage 获取,其次从浏览器获取)
|
||||
*/
|
||||
export function getDefaultMkt(): string {
|
||||
const saved = localStorage.getItem(MKT_STORAGE_KEY)
|
||||
if (saved && SUPPORTED_REGIONS.some(r => r.value === saved)) {
|
||||
return saved
|
||||
}
|
||||
return getBrowserMkt()
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存选择的地区
|
||||
*/
|
||||
export function setSavedMkt(mkt: string): void {
|
||||
localStorage.setItem(MKT_STORAGE_KEY, mkt)
|
||||
}
|
||||
@@ -102,6 +102,16 @@
|
||||
local: 直接返回图片流; redirect: 重定向到存储位置
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Label for="api-fallback">启用地区不存在时兜底</Label>
|
||||
<Switch
|
||||
id="api-fallback"
|
||||
v-model="config.API.EnableMktFallback"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
如果请求的地区无数据,自动回退到默认地区
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -372,6 +382,31 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 抓取配置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>抓取配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label>抓取地区</Label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||
<div v-for="region in allRegions" :key="region.value" class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
:id="'region-'+region.value"
|
||||
:checked="config.Fetcher.Regions.includes(region.value)"
|
||||
@update:checked="(checked: any) => toggleRegion(region.value, !!checked)"
|
||||
/>
|
||||
<Label :for="'region-'+region.value" class="text-sm font-normal cursor-pointer">{{ region.label }}</Label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
勾选需要定期抓取壁纸的地区。如果不勾选任何地区,默认将只抓取 zh-CN。
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 功能特性配置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -414,6 +449,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { apiService } from '@/lib/api-service'
|
||||
import type { Config } from '@/lib/api-types'
|
||||
@@ -424,9 +460,12 @@ const loadError = ref('')
|
||||
const saveLoading = ref(false)
|
||||
const dsnError = ref('')
|
||||
|
||||
// 所有可选地区列表
|
||||
const allRegions = ref<any[]>([])
|
||||
|
||||
const config = ref<Config>({
|
||||
Admin: { PasswordBcrypt: '' },
|
||||
API: { Mode: 'local' },
|
||||
API: { Mode: 'local', EnableMktFallback: true },
|
||||
Cron: { Enabled: true, DailySpec: '0 9 * * *' },
|
||||
DB: { Type: 'sqlite', DSN: '' },
|
||||
Feature: { WriteDailyFiles: true },
|
||||
@@ -464,12 +503,37 @@ const config = ref<Config>({
|
||||
}
|
||||
},
|
||||
Token: { DefaultTTL: '168h' },
|
||||
Web: { Path: './webapp/dist' }
|
||||
Web: { Path: './webapp/dist' },
|
||||
Fetcher: { Regions: [] }
|
||||
})
|
||||
|
||||
const configJson = ref('')
|
||||
const jsonError = ref('')
|
||||
|
||||
// 获取所有地区
|
||||
const fetchRegions = async () => {
|
||||
try {
|
||||
const data = await apiService.getRegions()
|
||||
allRegions.value = data
|
||||
} catch (err) {
|
||||
console.error('获取地区列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRegion = (regionValue: string, checked: boolean) => {
|
||||
if (!config.value.Fetcher.Regions) {
|
||||
config.value.Fetcher.Regions = []
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
if (!config.value.Fetcher.Regions.includes(regionValue)) {
|
||||
config.value.Fetcher.Regions.push(regionValue)
|
||||
}
|
||||
} else {
|
||||
config.value.Fetcher.Regions = config.value.Fetcher.Regions.filter(r => r !== regionValue)
|
||||
}
|
||||
}
|
||||
|
||||
// DSN 示例
|
||||
const dsnExamples = computed(() => {
|
||||
switch (config.value.DB.Type) {
|
||||
@@ -602,6 +666,7 @@ const handleSaveConfig = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRegions()
|
||||
fetchConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -103,6 +103,10 @@
|
||||
<code class="text-yellow-400 min-w-24">format</code>
|
||||
<span class="text-white/50">格式: jpg (默认: jpg)</span>
|
||||
</div>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">mkt</code>
|
||||
<span class="text-white/50">地区编码 (如 zh-CN, en-US, ja-JP),默认由服务器自动探测</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,6 +186,10 @@
|
||||
<code class="text-yellow-400 min-w-24">format</code>
|
||||
<span class="text-white/50">格式 (默认: jpg)</span>
|
||||
</div>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">mkt</code>
|
||||
<span class="text-white/50">地区编码 (如 zh-CN, en-US, ja-JP)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -254,6 +262,10 @@
|
||||
<code class="text-yellow-400 min-w-24">format</code>
|
||||
<span class="text-white/50">格式 (默认: jpg)</span>
|
||||
</div>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<code class="text-yellow-400 min-w-24">mkt</code>
|
||||
<span class="text-white/50">地区编码 (如 zh-CN, en-US, ja-JP)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -334,6 +346,10 @@
|
||||
<code class="text-yellow-400 min-w-32">date</code>
|
||||
<span class="text-white/60">图片日期(格式:YYYY-MM-DD)</span>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<code class="text-yellow-400 min-w-32">mkt</code>
|
||||
<span class="text-white/60">地区编码(如 zh-CN, en-US)</span>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<code class="text-yellow-400 min-w-32">title</code>
|
||||
<span class="text-white/60">图片标题</span>
|
||||
@@ -458,24 +474,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { API_BASE_URL } from '@/lib/api-config'
|
||||
import { getDefaultMkt } from '@/lib/mkt-utils'
|
||||
|
||||
const baseURL = ref(API_BASE_URL)
|
||||
const previewImage = ref<string | null>(null)
|
||||
const defaultMkt = getDefaultMkt()
|
||||
|
||||
// 获取今日图片示例
|
||||
const getTodayImageExample = () => {
|
||||
return `${baseURL.value}/image/today?variant=UHD&format=jpg`
|
||||
return `${baseURL.value}/image/today?variant=UHD&format=jpg&mkt=${defaultMkt}`
|
||||
}
|
||||
|
||||
// 获取指定日期图片示例
|
||||
const getDateImageExample = () => {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
return `${baseURL.value}/image/date/${today}?variant=1920x1080&format=jpg`
|
||||
return `${baseURL.value}/image/date/${today}?variant=1920x1080&format=jpg&mkt=${defaultMkt}`
|
||||
}
|
||||
|
||||
// 获取随机图片示例
|
||||
const getRandomImageExample = () => {
|
||||
return `${baseURL.value}/image/random?variant=UHD&format=jpg`
|
||||
return `${baseURL.value}/image/random?variant=UHD&format=jpg&mkt=${defaultMkt}`
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
|
||||
@@ -79,6 +79,23 @@
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- 地区选择 -->
|
||||
<Select v-model="selectedMkt" @update:model-value="onMktChange">
|
||||
<SelectTrigger class="w-[180px] bg-white/10 backdrop-blur-md text-white border-white/20 hover:bg-white/15 hover:border-white/30 focus:ring-white/50 shadow-lg">
|
||||
<SelectValue placeholder="选择地区" />
|
||||
</SelectTrigger>
|
||||
<SelectContent class="bg-gray-900/95 backdrop-blur-xl border-white/20 text-white">
|
||||
<SelectItem
|
||||
v-for="region in regions"
|
||||
:key="region.value"
|
||||
:value="region.value"
|
||||
class="focus:bg-white/10 focus:text-white cursor-pointer"
|
||||
>
|
||||
{{ region.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<!-- 年份选择 -->
|
||||
<Select v-model="selectedYear" @update:model-value="onYearChange">
|
||||
<SelectTrigger class="w-[180px] bg-white/10 backdrop-blur-md text-white border-white/20 hover:bg-white/15 hover:border-white/30 focus:ring-white/50 shadow-lg">
|
||||
@@ -153,7 +170,7 @@
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="getImageUrl(image.date!)"
|
||||
:src="getImageUrl(image)"
|
||||
:alt="image.title || 'Bing Image'"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
loading="lazy"
|
||||
@@ -248,6 +265,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useImageList } from '@/composables/useImages'
|
||||
import { bingPaperApi } from '@/lib/api-service'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getDefaultMkt, setSavedMkt, SUPPORTED_REGIONS, setSupportedRegions } from '@/lib/mkt-utils'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -258,18 +276,21 @@ import {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 地区列表
|
||||
const regions = ref(SUPPORTED_REGIONS)
|
||||
|
||||
// 顶部最新图片(独立加载,不受筛选影响)
|
||||
const latestImage = ref<any>(null)
|
||||
const todayLoading = ref(false)
|
||||
|
||||
// 历史图片列表(使用服务端分页和筛选,每页15张)
|
||||
const { images, loading, hasMore, loadMore, filterByMonth } = useImageList(15)
|
||||
const { images, loading, hasMore, loadMore, filterByMonth, filterByMkt } = useImageList(15)
|
||||
|
||||
// 加载顶部最新图片
|
||||
const loadLatestImage = async () => {
|
||||
todayLoading.value = true
|
||||
try {
|
||||
const params = { page: 1, page_size: 1 }
|
||||
const params: any = { page: 1, page_size: 1, mkt: selectedMkt.value }
|
||||
const result = await bingPaperApi.getImages(params)
|
||||
if (result.length > 0) {
|
||||
latestImage.value = result[0]
|
||||
@@ -281,8 +302,17 @@ const loadLatestImage = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载顶部图片
|
||||
onMounted(() => {
|
||||
// 初始化加载
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const backendRegions = await bingPaperApi.getRegions()
|
||||
if (backendRegions && backendRegions.length > 0) {
|
||||
regions.value = backendRegions
|
||||
setSupportedRegions(backendRegions)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch regions:', error)
|
||||
}
|
||||
loadLatestImage()
|
||||
})
|
||||
|
||||
@@ -315,9 +345,22 @@ const nextUpdateTime = computed(() => {
|
||||
})
|
||||
|
||||
// 筛选相关状态
|
||||
const selectedMkt = ref(getDefaultMkt())
|
||||
const selectedYear = ref('')
|
||||
const selectedMonth = ref('')
|
||||
|
||||
const onMktChange = () => {
|
||||
setSavedMkt(selectedMkt.value)
|
||||
filterByMkt(selectedMkt.value)
|
||||
loadLatestImage()
|
||||
|
||||
// 重置懒加载状态
|
||||
imageVisibility.value = []
|
||||
setTimeout(() => {
|
||||
setupObserver()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 懒加载相关
|
||||
const imageRefs = ref<(HTMLElement | null)[]>([])
|
||||
const imageVisibility = ref<boolean[]>([])
|
||||
@@ -374,11 +417,14 @@ const onFilterChange = () => {
|
||||
|
||||
// 重置筛选
|
||||
const resetFilters = () => {
|
||||
selectedMkt.value = getDefaultMkt()
|
||||
selectedYear.value = ''
|
||||
selectedMonth.value = ''
|
||||
|
||||
// 重置为加载默认数据
|
||||
filterByMkt(selectedMkt.value)
|
||||
filterByMonth(undefined)
|
||||
loadLatestImage()
|
||||
|
||||
// 重置懒加载状态
|
||||
imageVisibility.value = []
|
||||
@@ -521,12 +567,12 @@ const formatDate = (dateStr?: string) => {
|
||||
// 获取最新图片 URL(顶部大图使用UHD高清)
|
||||
const getLatestImageUrl = () => {
|
||||
if (!latestImage.value?.date) return ''
|
||||
return bingPaperApi.getImageUrlByDate(latestImage.value.date, 'UHD', 'jpg')
|
||||
return bingPaperApi.getImageUrlByDate(latestImage.value.date, 'UHD', 'jpg', latestImage.value.mkt)
|
||||
}
|
||||
|
||||
// 获取图片 URL(缩略图 - 使用较小分辨率节省流量)
|
||||
const getImageUrl = (date: string) => {
|
||||
return bingPaperApi.getImageUrlByDate(date, '640x480', 'jpg')
|
||||
const getImageUrl = (image: any) => {
|
||||
return bingPaperApi.getImageUrlByDate(image.date!, '640x480', 'jpg', image.mkt)
|
||||
}
|
||||
|
||||
// 查看图片详情
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<!-- 拖动手柄 -->
|
||||
<div
|
||||
@mousedown="startDrag"
|
||||
@touchstart="startDrag"
|
||||
@touchstart.passive="startDrag"
|
||||
class="absolute top-2 left-1/2 -translate-x-1/2 w-12 h-1 bg-white/30 rounded-full cursor-move hover:bg-white/50 transition-colors touch-none"
|
||||
></div>
|
||||
|
||||
@@ -178,10 +178,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useImageByDate } from '@/composables/useImages'
|
||||
import { bingPaperApi } from '@/lib/api-service'
|
||||
import { getDefaultMkt } from '@/lib/mkt-utils'
|
||||
import Calendar from '@/components/ui/calendar/Calendar.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -202,12 +203,17 @@ const getInitialCalendarState = (): boolean => {
|
||||
}
|
||||
|
||||
const currentDate = ref(route.params.date as string)
|
||||
const currentMkt = ref(route.query.mkt as string || getDefaultMkt())
|
||||
const showInfo = ref(true)
|
||||
const showCalendar = ref(getInitialCalendarState())
|
||||
const navigating = ref(false)
|
||||
const imageOpacity = ref(1)
|
||||
const imageTransitioning = ref(false)
|
||||
|
||||
// 响应式窗口大小
|
||||
const windowSize = ref({ width: window.innerWidth, height: window.innerHeight })
|
||||
const isMobile = computed(() => windowSize.value.width < 768)
|
||||
|
||||
// 前后日期可用性
|
||||
const hasPreviousDay = ref(true)
|
||||
const hasNextDay = ref(true)
|
||||
@@ -222,11 +228,10 @@ let animationFrameId: number | null = null
|
||||
|
||||
// 计算图片实际显示区域(考虑图片宽高比和object-contain)
|
||||
const getImageDisplayBounds = () => {
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
const windowWidth = windowSize.value.width
|
||||
const windowHeight = windowSize.value.height
|
||||
|
||||
// 必应图片通常是16:9或类似宽高比
|
||||
// 使用UHD分辨率: 1920x1080 (16:9)
|
||||
// 必应图片通常是16:9
|
||||
const imageAspectRatio = 16 / 9
|
||||
const windowAspectRatio = windowWidth / windowHeight
|
||||
|
||||
@@ -259,22 +264,35 @@ const getImageDisplayBounds = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化浮窗位置(居中偏下,限制在图片显示区域内)
|
||||
// 初始化浮窗位置(限制在图片显示区域内,移动端默认展示在底部)
|
||||
const initPanelPosition = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const bounds = getImageDisplayBounds()
|
||||
const panelWidth = Math.min(bounds.width * 0.9, 448) // max-w-md = 448px
|
||||
|
||||
infoPanelPos.value = {
|
||||
x: bounds.left + (bounds.width - panelWidth) / 2,
|
||||
y: Math.max(bounds.top, bounds.bottom - 280) // 距底部280px,避免与控制栏重叠
|
||||
if (isMobile.value) {
|
||||
// 移动端:默认居中靠下,不严格限制在图片内(因为要求可以不限制)
|
||||
// 但为了好看,我们还是给它一个默认位置
|
||||
const panelWidth = windowSize.value.width * 0.9
|
||||
infoPanelPos.value = {
|
||||
x: (windowSize.value.width - panelWidth) / 2,
|
||||
y: windowSize.value.height - 240 // 靠下
|
||||
}
|
||||
} else {
|
||||
// 桌面端:限制在图片区域内
|
||||
const panelWidth = Math.min(bounds.width * 0.9, 448) // max-w-md = 448px
|
||||
infoPanelPos.value = {
|
||||
x: bounds.left + (bounds.width - panelWidth) / 2,
|
||||
y: Math.max(bounds.top, bounds.bottom - 280) // 距底部280px,避免与控制栏重叠
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始拖动
|
||||
const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault()
|
||||
if (e instanceof MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
isDragging.value = true
|
||||
|
||||
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
|
||||
@@ -287,7 +305,7 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
|
||||
document.addEventListener('mousemove', onDrag, { passive: false })
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
||||
document.addEventListener('touchmove', onDrag, { passive: true })
|
||||
document.addEventListener('touchend', stopDrag)
|
||||
}
|
||||
|
||||
@@ -295,7 +313,9 @@ const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
e.preventDefault()
|
||||
if (e instanceof MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// 取消之前的动画帧
|
||||
if (animationFrameId !== null) {
|
||||
@@ -310,15 +330,26 @@ const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||
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 // 预留底部控制栏空间
|
||||
let minX, maxX, minY, maxY
|
||||
|
||||
if (isMobile.value) {
|
||||
// 移动端:不限制区域,限制在视口内即可
|
||||
minX = 0
|
||||
maxX = windowSize.value.width - rect.width
|
||||
minY = 0
|
||||
maxY = windowSize.value.height - rect.height
|
||||
} else {
|
||||
// 桌面端:限制在图片实际显示区域内,考虑底部控制栏高度(约80px)
|
||||
const bounds = getImageDisplayBounds()
|
||||
minX = bounds.left
|
||||
maxX = bounds.right - rect.width
|
||||
minY = bounds.top
|
||||
maxY = bounds.bottom - rect.height - 80 // 预留底部控制栏空间
|
||||
}
|
||||
|
||||
infoPanelPos.value = {
|
||||
x: Math.max(minX, Math.min(newX, maxX)),
|
||||
@@ -347,12 +378,12 @@ const stopDrag = () => {
|
||||
}
|
||||
|
||||
// 使用 composable 获取图片数据(传递 ref,自动响应日期变化)
|
||||
const { image, loading, error } = useImageByDate(currentDate)
|
||||
const { image, loading, error } = useImageByDate(currentDate, currentMkt)
|
||||
|
||||
// 检测指定日期是否有数据
|
||||
const checkDateAvailability = async (dateStr: string): Promise<boolean> => {
|
||||
try {
|
||||
await bingPaperApi.getImageMetaByDate(dateStr)
|
||||
await bingPaperApi.getImageMetaByDate(dateStr, currentMkt.value)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
@@ -384,9 +415,6 @@ const checkAdjacentDates = async () => {
|
||||
checkingDates.value = false
|
||||
}
|
||||
|
||||
// 初始化位置
|
||||
initPanelPosition()
|
||||
|
||||
// 监听showCalendar变化并自动保存到localStorage
|
||||
watch(showCalendar, (newValue) => {
|
||||
try {
|
||||
@@ -401,6 +429,20 @@ watch(currentDate, () => {
|
||||
checkAdjacentDates()
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听路由变化,支持前进后退
|
||||
watch(() => route.params.date, (newDate) => {
|
||||
if (newDate && newDate !== currentDate.value) {
|
||||
currentDate.value = newDate as string
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => route.query.mkt, (newMkt) => {
|
||||
const mkt = (newMkt as string) || getDefaultMkt()
|
||||
if (mkt !== currentMkt.value) {
|
||||
currentMkt.value = mkt
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return ''
|
||||
@@ -415,7 +457,7 @@ const formatDate = (dateStr?: string) => {
|
||||
|
||||
// 获取完整图片 URL
|
||||
const getFullImageUrl = () => {
|
||||
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg')
|
||||
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg', currentMkt.value)
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
@@ -432,10 +474,10 @@ const preloadImage = (url: string): Promise<void> => {
|
||||
const preloadImageAndData = async (date: string): Promise<void> => {
|
||||
try {
|
||||
// 并行预加载图片和数据
|
||||
const imageUrl = bingPaperApi.getImageUrlByDate(date, 'UHD', 'jpg')
|
||||
const imageUrl = bingPaperApi.getImageUrlByDate(date, 'UHD', 'jpg', currentMkt.value)
|
||||
await Promise.all([
|
||||
preloadImage(imageUrl),
|
||||
bingPaperApi.getImageMetaByDate(date)
|
||||
bingPaperApi.getImageMetaByDate(date, currentMkt.value)
|
||||
])
|
||||
} catch (error) {
|
||||
console.warn('Failed to preload image or data:', error)
|
||||
@@ -461,7 +503,7 @@ const switchToDate = async (newDate: string) => {
|
||||
|
||||
// 3. 更新日期(此时图片和数据已经预加载完成)
|
||||
currentDate.value = newDate
|
||||
router.replace(`/image/${newDate}`)
|
||||
router.replace(`/image/${newDate}?mkt=${currentMkt.value}`)
|
||||
|
||||
// 4. 等待一个微任务,确保 DOM 更新
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
@@ -534,18 +576,29 @@ const handleKeydown = (e: KeyboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加键盘事件监听
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('resize', initPanelPosition)
|
||||
// 窗口缩放处理
|
||||
const handleResize = () => {
|
||||
windowSize.value = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
}
|
||||
initPanelPosition()
|
||||
}
|
||||
|
||||
// 清理
|
||||
import { onUnmounted } from 'vue'
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
// 初始化浮窗位置
|
||||
initPanelPosition()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('resize', initPanelPosition)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
|
||||
Reference in New Issue
Block a user