diff --git a/src/components/cards/PluginCard.vue b/src/components/cards/PluginCard.vue
index 9fdb624e..a7cd52c0 100644
--- a/src/components/cards/PluginCard.vue
+++ b/src/components/cards/PluginCard.vue
@@ -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(
{{ props.plugin?.plugin_name }}
@@ -721,8 +732,22 @@ watch(
-
-
+
+
+
+
+
+ {{ cardRatingValue }}
@@ -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 */
diff --git a/src/components/cards/__tests__/PluginCardAbout.spec.ts b/src/components/cards/__tests__/PluginCardAbout.spec.ts
index 61d20ef3..5dcf9929 100644
--- a/src/components/cards/__tests__/PluginCardAbout.spec.ts
+++ b/src/components/cards/__tests__/PluginCardAbout.spec.ts
@@ -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()
+ })
})
diff --git a/src/components/dialog/PluginMarketDetailDialog.vue b/src/components/dialog/PluginMarketDetailDialog.vue
index 3f87f133..e2e65fa8 100644
--- a/src/components/dialog/PluginMarketDetailDialog.vue
+++ b/src/components/dialog/PluginMarketDetailDialog.vue
@@ -247,7 +247,7 @@ onUnmounted(() => {
-
+
@@ -359,7 +359,7 @@ onUnmounted(() => {
diff --git a/src/components/dialog/__tests__/PluginMarketDetailDialog.spec.ts b/src/components/dialog/__tests__/PluginMarketDetailDialog.spec.ts
index a5a8cf35..9c42d952 100644
--- a/src/components/dialog/__tests__/PluginMarketDetailDialog.spec.ts
+++ b/src/components/dialog/__tests__/PluginMarketDetailDialog.spec.ts
@@ -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')