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