mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-13 09:34:47 +08:00
Improve plugin card statuses and compact market details
This commit is contained in:
@@ -39,6 +39,16 @@ const emit = defineEmits(['remove', 'save', 'actionDone'])
|
||||
// 多语言
|
||||
const { t } = useI18n()
|
||||
|
||||
const hasCardRating = computed(() => (props.plugin?.rating_count || 0) > 0)
|
||||
const hasCardStatus = computed(() => Boolean(props.plugin?.has_update) || hasCardRating.value)
|
||||
const cardRatingValue = computed(() => Number(props.plugin?.average_rating || 0).toFixed(1))
|
||||
const cardRatingSummary = computed(() =>
|
||||
t('plugin.ratingSummary', {
|
||||
rating: cardRatingValue.value,
|
||||
count: props.plugin?.rating_count || 0,
|
||||
}),
|
||||
)
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
@@ -642,6 +652,7 @@ watch(
|
||||
<VCardText class="px-2 pt-2 pb-0">
|
||||
<VCardTitle
|
||||
class="text-white px-2 pb-0 text-lg text-shadow whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
:class="{ 'plugin-card__title--with-status': hasCardStatus }"
|
||||
>
|
||||
<VBadge dot inline :color="props.plugin?.state ? 'success' : 'secondary'" />
|
||||
{{ props.plugin?.plugin_name }}
|
||||
@@ -721,8 +732,22 @@ watch(
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon icon="mdi-new-box" class="text-white" />
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="plugin-card__status plugin-card__status--update"
|
||||
:aria-label="t('plugin.hasUpdate')"
|
||||
:title="t('plugin.hasUpdate')"
|
||||
>
|
||||
<VIcon icon="mdi-new-box" class="text-white" size="20" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasCardRating"
|
||||
class="plugin-card__status plugin-card__status--rating"
|
||||
:aria-label="cardRatingSummary"
|
||||
:title="cardRatingSummary"
|
||||
>
|
||||
<VIcon icon="mdi-star" color="warning" size="16" />
|
||||
<span>{{ cardRatingValue }}</span>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
@@ -736,6 +761,28 @@ watch(
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.plugin-card__title--with-status {
|
||||
padding-inline-end: 4rem !important;
|
||||
}
|
||||
|
||||
.plugin-card__status {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: white;
|
||||
inset-block-start: 0.625rem;
|
||||
inset-inline-end: 0.625rem;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 65%);
|
||||
}
|
||||
|
||||
.plugin-card__status--rating {
|
||||
gap: 0.125rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-cover-blurred::before {
|
||||
position: absolute;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
|
||||
@@ -122,4 +122,39 @@ describe('PluginCard about menu', () => {
|
||||
await waitFor(() => expect(replace).toHaveBeenCalledWith('https://github.com/example/plugins'))
|
||||
expect(popup.opener).toBeNull()
|
||||
})
|
||||
|
||||
it('prioritizes the update status over the rating status', async () => {
|
||||
await renderWithProviders(PluginCard, {
|
||||
props: {
|
||||
plugin: { ...plugin, has_update: true, average_rating: 4.3, rating_count: 12 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByLabelText('有更新')).toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('4.3 分,共 12 人评分')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the rating status when no update is available', async () => {
|
||||
await renderWithProviders(PluginCard, {
|
||||
props: {
|
||||
plugin: { ...plugin, has_update: false, average_rating: 4.3, rating_count: 12 },
|
||||
},
|
||||
})
|
||||
|
||||
const ratingStatus = screen.getByLabelText('4.3 分,共 12 人评分')
|
||||
|
||||
expect(ratingStatus).toHaveClass('plugin-card__status--rating')
|
||||
expect(ratingStatus).toHaveTextContent('4.3')
|
||||
expect(screen.queryByLabelText('有更新')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('leaves the status position empty without an update or rating', async () => {
|
||||
await renderWithProviders(PluginCard, {
|
||||
props: {
|
||||
plugin: { ...plugin, has_update: false, average_rating: 0, rating_count: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(document.querySelector('.plugin-card__status')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -247,7 +247,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog v-if="visible" v-model="visible" width="100%" max-width="32rem" max-height="90dvh" scrollable>
|
||||
<VDialog v-if="visible" v-model="visible" width="100%" max-width="25rem" max-height="90dvh" scrollable>
|
||||
<VCard class="plugin-market-detail">
|
||||
<VDialogCloseBtn v-model="visible" />
|
||||
<VCardText class="plugin-market-detail__content">
|
||||
@@ -359,7 +359,7 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.plugin-market-detail__content {
|
||||
padding: 2.25rem 1.5rem 1.5rem;
|
||||
padding: 1.75rem 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail__header {
|
||||
@@ -375,16 +375,16 @@ onUnmounted(() => {
|
||||
|
||||
.plugin-market-detail__title {
|
||||
max-inline-size: 100%;
|
||||
margin: 1rem 0 0;
|
||||
font-size: 1.25rem;
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.plugin-market-detail__description {
|
||||
max-inline-size: 28rem;
|
||||
margin: 0.35rem 0 0;
|
||||
max-inline-size: 24rem;
|
||||
margin: 0.25rem 0 0;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
@@ -394,16 +394,15 @@ onUnmounted(() => {
|
||||
|
||||
.plugin-market-detail__metadata {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
max-inline-size: 26rem;
|
||||
margin: 1.5rem auto 0;
|
||||
gap: 0.625rem;
|
||||
margin: 1.125rem auto 0;
|
||||
}
|
||||
|
||||
.plugin-market-detail__metadata-row {
|
||||
display: grid;
|
||||
grid-template-columns: 7rem minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
@@ -421,6 +420,7 @@ onUnmounted(() => {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.plugin-market-detail__author {
|
||||
@@ -448,15 +448,15 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.plugin-market-detail__warning {
|
||||
margin-block-start: 1.25rem;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-block-start: 1.25rem;
|
||||
gap: 0.5rem;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions__buttons {
|
||||
@@ -478,13 +478,13 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.plugin-market-detail-user-rating {
|
||||
margin-block-start: 1.5rem;
|
||||
padding-block-start: 1.25rem;
|
||||
margin-block-start: 1.25rem;
|
||||
padding-block-start: 1rem;
|
||||
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.plugin-market-detail-user-rating__title {
|
||||
margin: 0 0 0.75rem;
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
@@ -495,7 +495,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
@@ -504,15 +504,6 @@ onUnmounted(() => {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail__metadata-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail__metadata dt {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions__buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -521,11 +512,4 @@ onUnmounted(() => {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 480px) {
|
||||
.plugin-market-detail-user-rating__controls {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('PluginMarketDetailDialog', () => {
|
||||
expect(screen.getByRole('button', { name: '提交评分' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps long plugin details in aligned metadata rows', async () => {
|
||||
it('keeps long plugin details in centered metadata rows', async () => {
|
||||
const pluginDescription = '支持包含较长说明文字和换行内容的插件详情。\n第二行内容保持完整展示。'
|
||||
const pluginAuthor = 'MoviePilot-Plugin-Author-With-A-Long-Name'
|
||||
|
||||
@@ -148,7 +148,9 @@ describe('PluginMarketDetailDialog', () => {
|
||||
system_version: 'v2.15.0 or later',
|
||||
})
|
||||
|
||||
expect(await screen.findByText(pluginDescription)).toHaveClass('plugin-market-detail__description')
|
||||
const description = document.querySelector('.plugin-market-detail__description')
|
||||
|
||||
expect(description).toHaveTextContent('支持包含较长说明文字和换行内容的插件详情。 第二行内容保持完整展示。')
|
||||
expect(screen.getByRole('button', { name: pluginAuthor })).toHaveClass('plugin-market-detail__author')
|
||||
|
||||
const metadata = document.querySelector('.plugin-market-detail__metadata')
|
||||
|
||||
Reference in New Issue
Block a user