mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 08:06:43 +08:00
feat: 优化媒体卡片与版本标志 (#650)
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
This commit is contained in:
@@ -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_url(ListenBrainz/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%;
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user