feat: 优化媒体卡片与版本标志 (#650)

Co-authored-by: traeagent <traeagent@users.noreply.github.com>
This commit is contained in:
jxxghp
2026-08-08 15:33:55 +08:00
committed by GitHub
co-authored by traeagent
parent 1064156062
commit 777c4ec7e3
7 changed files with 55 additions and 42 deletions
+21 -8
View File
@@ -6,7 +6,6 @@ let mediaCardIdSeed = 0
</script>
<script lang="ts" setup>
import noImage from '@images/no-image.jpeg'
import { getDisplayImageUrl, getLogoUrl } from '@/utils/imageUtils'
import api from '@/api'
import { formatRating } from '@/@core/utils/formatters'
@@ -397,16 +396,30 @@ function setupIntersectionObserver() {
}
}
// 媒体占位图标:电影/电视剧/音乐各自使用对应图标,缺失封面时统一渲染图标 + 底色占位
const placeholderIcon = computed(() => {
switch (props.media?.type) {
case '音乐':
return 'mdi-album'
case '电视剧':
return 'mdi-television-classic'
case '电影':
default:
return 'mdi-movie-open-outline'
}
})
// 计算图片地址
const getImgUrl: Ref<string> = computed(() => {
if (imageLoadError.value) return ''
if (props.media?.type === '音乐') {
// 音乐封面优先使用 cover_urlListenBrainz/MusicBrainz 统计接口返回),回退到 poster_path
const musicCover = props.media?.cover_url || props.media?.poster_path
if (!musicCover || imageLoadError.value) return ''
if (!musicCover) return ''
return getDisplayImageUrl(musicCover, globalSettings.GLOBAL_IMAGE_CACHE)
}
if (imageLoadError.value) return noImage
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
const url = props.media?.poster_path?.replace('original', 'w500')
if (!url) return ''
return getDisplayImageUrl(url, globalSettings.GLOBAL_IMAGE_CACHE)
})
@@ -481,10 +494,10 @@ onBeforeUnmount(() => {
@click.stop="handleMediaCardClick(hover.isHovering)"
>
<div
v-if="props.media?.type === '音乐' && !getImgUrl"
class="music-card-placeholder d-flex align-center justify-center"
v-if="!getImgUrl"
class="media-card-placeholder d-flex align-center justify-center"
>
<VIcon icon="mdi-album" size="64" color="medium-emphasis" />
<VIcon :icon="placeholderIcon" size="64" color="medium-emphasis" />
</div>
<VImg
v-else
@@ -578,7 +591,7 @@ onBeforeUnmount(() => {
inline-size: 100%;
}
.music-card-placeholder {
.media-card-placeholder {
aspect-ratio: 2 / 3;
background: rgb(var(--v-theme-surface-variant));
block-size: 100%;
+31 -31
View File
@@ -12,7 +12,6 @@ import { useGlobalSettingsStore } from '@/stores'
import { openSharedDialog } from '@/composables/useSharedDialog'
import { getDisplayImageUrl } from '@/utils/imageUtils'
import { buildMusicDetailRoute } from '@/utils/music'
import noImage from '@images/no-image.jpeg'
const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue'))
const SubscribeFilesDialog = defineAsyncComponent(() => import('../dialog/SubscribeFilesDialog.vue'))
@@ -421,40 +420,41 @@ watch(
},
)
// 音乐订阅:背景图与海报可能只存在其一,两者都缺失时不再使用通用占位图,
// 改为渲染与音乐媒体卡片一致的胶片占位背景(专辑图标),避免出现空白图片区。
const isMusicSubscribe = computed(() => props.media?.type === '音乐')
// 媒体占位图标:电影/电视剧/音乐各自使用对应图标,缺失封面时统一渲染图标 + 底色占位
const placeholderIcon = computed(() => {
switch (props.media?.type) {
case '音乐':
return 'mdi-album'
case '电视剧':
return 'mdi-television-classic'
case '电影':
default:
return 'mdi-movie-open-outline'
}
})
// 计算backdrop图片地址
const backdropUrl = computed(() => {
if (isMusicSubscribe.value) {
if (backdropLoadError.value) return ''
const url = props.media?.backdrop || props.media?.poster
if (!url) return ''
return getDisplayImageUrl(url, globalSettings.GLOBAL_IMAGE_CACHE)
}
const url = backdropLoadError.value ? noImage : props.media?.backdrop || props.media?.poster || noImage
return getDisplayImageUrl(url || '', globalSettings.GLOBAL_IMAGE_CACHE)
if (backdropLoadError.value) return ''
const url = props.media?.backdrop || props.media?.poster
if (!url) return ''
return getDisplayImageUrl(url, globalSettings.GLOBAL_IMAGE_CACHE)
})
// 计算海报图片地址
const posterUrl = computed(() => {
if (isMusicSubscribe.value) {
if (posterLoadError.value) return ''
const url = props.media?.poster || props.media?.backdrop
if (!url) return ''
return getDisplayImageUrl(url, globalSettings.GLOBAL_IMAGE_CACHE)
}
const url = posterLoadError.value ? noImage : props.media?.poster || noImage
return getDisplayImageUrl(url || '', globalSettings.GLOBAL_IMAGE_CACHE)
if (posterLoadError.value) return ''
const url = props.media?.poster || props.media?.backdrop
if (!url) return ''
return getDisplayImageUrl(url, globalSettings.GLOBAL_IMAGE_CACHE)
})
// 音乐订阅缺失封面时展示胶片占位背景,对齐音乐媒体卡片
const showMusicPlaceholder = computed(() => isMusicSubscribe.value && !backdropUrl.value)
// 缺失封面时展示媒体占位背景(图标 + 底色),对齐音乐媒体卡片
const showPlaceholder = computed(() => !backdropUrl.value)
// 占位背景出现时同步标记图片已加载,让卡片正文与徽标正常渲染
watch(
showMusicPlaceholder,
showPlaceholder,
show => {
if (show) imageLoaded.value = true
},
@@ -539,10 +539,10 @@ function handleCardClick() {
</div>
<template #image v-if="display.smAndUp.value">
<div
v-if="showMusicPlaceholder"
class="subscribe-card-music-placeholder d-flex align-center justify-center"
v-if="showPlaceholder"
class="subscribe-card-placeholder d-flex align-center justify-center"
>
<VIcon icon="mdi-album" size="64" color="medium-emphasis" />
<VIcon :icon="placeholderIcon" size="64" color="medium-emphasis" />
</div>
<VImg
v-else
@@ -567,10 +567,10 @@ function handleCardClick() {
<template v-if="display.xs.value">
<div class="subscribe-card-mobile-media">
<div
v-if="showMusicPlaceholder"
class="subscribe-card-music-placeholder d-flex align-center justify-center"
v-if="showPlaceholder"
class="subscribe-card-placeholder d-flex align-center justify-center"
>
<VIcon icon="mdi-album" size="64" color="medium-emphasis" />
<VIcon :icon="placeholderIcon" size="64" color="medium-emphasis" />
</div>
<VImg
v-else
@@ -972,8 +972,8 @@ function handleCardClick() {
background-image: linear-gradient(180deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
}
/* 音乐订阅缺失封面时的胶片占位背景,对齐音乐媒体卡片 */
.subscribe-card-music-placeholder {
/* 缺失封面时的媒体占位背景(图标 + 底色),对齐音乐媒体卡片 */
.subscribe-card-placeholder {
block-size: 100%;
inline-size: 100%;
background: rgb(var(--v-theme-surface-variant));
@@ -354,7 +354,7 @@ describe('MediaCard', () => {
const { container } = await renderCard(media)
expect(container.querySelector('.music-card-placeholder .v-icon')).not.toBeNull()
expect(container.querySelector('.media-card-placeholder .v-icon')).not.toBeNull()
expect(container.querySelector('img[src*="no-image"]')).toBeNull()
})
@@ -150,7 +150,7 @@ describe('SubscribeCard display and progress', () => {
await fireEvent.error(image as HTMLImageElement)
await waitFor(() => expect(container.querySelector<HTMLImageElement>('img')?.src).toContain('no-image'))
await waitFor(() => expect(container.querySelector('.subscribe-card-placeholder')).toBeInTheDocument())
})
it('falls back from backdrop to poster for music subscriptions, then to the album placeholder', async () => {
@@ -166,7 +166,7 @@ describe('SubscribeCard display and progress', () => {
// 背景图与海报都缺失:渲染与音乐媒体卡片一致的胶片占位背景
const { container } = await renderCard({ backdrop: undefined, poster: undefined, type: '音乐' })
const placeholder = container.querySelector('.subscribe-card-music-placeholder')
const placeholder = container.querySelector('.subscribe-card-placeholder')
expect(placeholder).toBeInTheDocument()
expect(placeholder?.querySelector('.v-icon, [class*="mdi-album"]')).not.toBeNull()
expect(container.querySelector('img')).toBeNull()