diff --git a/src/api/types.ts b/src/api/types.ts index 57cc4990..16f57ab3 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -300,8 +300,10 @@ export interface DownloadHistory { seasons?: string // 集 Exx episodes?: string - // 海报或背景图 + // 背景图 image?: string + // 海报 + poster?: string // 下载器 Hash download_hash?: string // 种子名称 diff --git a/src/components/cards/DownloadingCard.vue b/src/components/cards/DownloadingCard.vue index 29dc6024..7e9bc81a 100644 --- a/src/components/cards/DownloadingCard.vue +++ b/src/components/cards/DownloadingCard.vue @@ -25,13 +25,13 @@ const imageLoadError = ref(false) const media = computed(() => props.info?.media ?? {}) watch( - () => media.value.image, + () => media.value.poster, () => { imageLoadError.value = false }, ) -const hasPosterImage = computed(() => Boolean(media.value.image && !imageLoadError.value)) +const hasPosterImage = computed(() => Boolean(media.value.poster && !imageLoadError.value)) const mediaTitle = computed(() => media.value.title || props.info?.name || props.info?.title || t('common.unknown')) @@ -149,111 +149,124 @@ async function deleteDownload() { @@ -268,11 +281,15 @@ async function deleteDownload() { inline-size: 100%; } +.downloading-card-shell { + border-radius: var(--app-surface-radius); +} + .downloading-card { display: grid; min-block-size: 12rem; color: rgb(var(--v-theme-on-surface)); - grid-template-columns: 8.25rem minmax(0, 1fr); + grid-template-columns: 8rem minmax(0, 1fr); } .downloading-card__poster { @@ -339,9 +356,9 @@ async function deleteDownload() { .downloading-card__title-meta { margin-inline-start: 0.35rem; - color: rgb(var(--v-theme-primary)); + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); font-size: 0.76rem; - font-weight: 650; + font-weight: 500; white-space: nowrap; } @@ -444,7 +461,7 @@ async function deleteDownload() { @container (width <= 25rem) { .downloading-card { min-block-size: 11rem; - grid-template-columns: 6.75rem minmax(0, 1fr); + grid-template-columns: 7.333rem minmax(0, 1fr); } .downloading-card__poster { @@ -478,10 +495,6 @@ async function deleteDownload() { } @container (width <= 21rem) { - .downloading-card { - grid-template-columns: 6.25rem minmax(0, 1fr); - } - .downloading-card__body { padding-inline: 0.65rem !important; } diff --git a/src/components/cards/__tests__/DownloadingCard.spec.ts b/src/components/cards/__tests__/DownloadingCard.spec.ts index f7accddd..bfa883f3 100644 --- a/src/components/cards/__tests__/DownloadingCard.spec.ts +++ b/src/components/cards/__tests__/DownloadingCard.spec.ts @@ -13,7 +13,7 @@ function downloading(overrides: Partial = {}): DownloadingInfo left_time: '1 小时', media: { episode: 'E02', - image: 'https://images.example.com/poster.jpg', + poster: 'https://images.example.com/poster.jpg', season: 'S01', title: '测试媒体', }, @@ -71,6 +71,32 @@ describe('DownloadingCard display and pause state', () => { expect(container.querySelector('.v-card-text .v-progress-linear')).not.toBeInTheDocument() }) + it('only renders a centered cover image when a poster is available', async () => { + const { container, rerender } = await renderCard() + + expect(container.querySelector('.downloading-card__image')).toBeInTheDocument() + + await rerender({ + downloaderName: 'qb-main', + info: downloading({ media: { backdrop: 'https://images.example.com/backdrop.jpg' } }), + }) + + expect(container.querySelector('.downloading-card')).toHaveClass('downloading-card--no-image') + expect(container.querySelector('.downloading-card__image')).not.toBeInTheDocument() + }) + + it('applies the shared lift state to the outer shell on hover', async () => { + const { container } = await renderCard() + const hoverArea = container.querySelector('.downloading-card-hover-area')! + const shell = container.querySelector('.downloading-card-shell')! + + await fireEvent.mouseEnter(hoverArea) + await waitFor(() => expect(shell).toHaveClass('app-hover-lift-card--hovering')) + + await fireEvent.mouseLeave(hoverArea) + await waitFor(() => expect(shell).not.toHaveClass('app-hover-lift-card--hovering')) + }) + it('uses the current operation and downloader name, changing state only on business success', async () => { const stopRequested = vi.fn() const startRequested = vi.fn() diff --git a/src/components/dialog/DownloadHistoryDialog.vue b/src/components/dialog/DownloadHistoryDialog.vue index 048908b2..8839f4d9 100644 --- a/src/components/dialog/DownloadHistoryDialog.vue +++ b/src/components/dialog/DownloadHistoryDialog.vue @@ -22,6 +22,7 @@ const pageSize = 30 const loading = ref(false) const isRefreshed = ref(false) +/** 分页加载下载历史,并将新页追加到现有列表。 */ async function loadHistory({ done }: { done: (status: 'empty' | 'error' | 'ok') => void }) { if (loading.value) { done('ok') @@ -54,6 +55,7 @@ async function loadHistory({ done }: { done: (status: 'empty' | 'error' | 'ok') } } +/** 删除指定下载历史,并在成功后同步移除当前列表项。 */ async function deleteHistory(item: DownloadHistory) { try { const result: { success?: boolean } = await api.delete('history/download', { data: item }) @@ -68,15 +70,19 @@ async function deleteHistory(item: DownloadHistory) { } } +/** 优先返回海报,缺失时使用背景图,并统一转换为可展示地址。 */ function getHistoryImage(item: DownloadHistory) { - if (!item.image) return noImage - return getDisplayImageUrl(item.image, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE) + const image = item.poster || item.image + if (!image) return noImage + return getDisplayImageUrl(image, globalSettingsStore.globalSettings.GLOBAL_IMAGE_CACHE) } +/** 返回下载历史的主标题。 */ function getHistoryTitle(item: DownloadHistory) { return item.title || item.torrent_name || t('dialog.downloadHistory.unknownTitle') } +/** 合并下载历史的季号和集号。 */ function getSeasonEpisode(item: DownloadHistory) { return `${item.seasons || ''}${item.episodes || ''}` } @@ -118,18 +124,19 @@ function getSeasonEpisode(item: DownloadHistory) {