Improve plugin card statuses and compact market details

This commit is contained in:
jxxghp
2026-08-04 12:51:35 +08:00
parent df1e72e871
commit b4afba99bd
4 changed files with 106 additions and 38 deletions

View File

@@ -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 */

View File

@@ -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()
})
})

View File

@@ -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>

View File

@@ -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')