mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 01:06:41 +08:00
Improve plugin market detail layout and rating sorting
This commit is contained in:
@@ -247,75 +247,64 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog v-if="visible" v-model="visible" max-width="30rem">
|
<VDialog v-if="visible" v-model="visible" width="100%" max-width="32rem" max-height="90dvh" scrollable>
|
||||||
<VCard>
|
<VCard class="plugin-market-detail">
|
||||||
<VDialogCloseBtn v-model="visible" />
|
<VDialogCloseBtn v-model="visible" />
|
||||||
<VCardText>
|
<VCardText class="plugin-market-detail__content">
|
||||||
<VCol>
|
<header class="plugin-market-detail__header">
|
||||||
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
|
<VAvatar size="64" class="plugin-market-detail__avatar">
|
||||||
<div class="mx-auto mt-5">
|
|
||||||
<VAvatar size="64">
|
|
||||||
<VImg :src="pluginIconPath()" aspect-ratio="4/3" cover @error="imageLoadError = true" />
|
<VImg :src="pluginIconPath()" aspect-ratio="4/3" cover @error="imageLoadError = true" />
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
</div>
|
<h2 class="plugin-market-detail__title">
|
||||||
<div class="flex-grow">
|
|
||||||
<VCardItem>
|
|
||||||
<VCardTitle class="text-center text-md-left">
|
|
||||||
{{ props.plugin?.plugin_name }}
|
{{ props.plugin?.plugin_name }}
|
||||||
</VCardTitle>
|
</h2>
|
||||||
<VCardSubtitle
|
<p v-if="props.plugin?.plugin_desc" class="plugin-market-detail__description">
|
||||||
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis ..."
|
|
||||||
>
|
|
||||||
{{ props.plugin?.plugin_desc }}
|
{{ props.plugin?.plugin_desc }}
|
||||||
</VCardSubtitle>
|
</p>
|
||||||
<VList lines="one" class="border-0">
|
</header>
|
||||||
<VListItem class="ps-0">
|
|
||||||
<VListItemTitle class="text-center text-md-left">
|
<dl class="plugin-market-detail__metadata">
|
||||||
<span class="font-weight-medium">{{ t('common.version') }}:</span>
|
<div class="plugin-market-detail__metadata-row">
|
||||||
<span class="text-body-1"> v{{ props.plugin?.plugin_version }}</span>
|
<dt>{{ t('common.version') }}:</dt>
|
||||||
</VListItemTitle>
|
<dd>v{{ props.plugin?.plugin_version }}</dd>
|
||||||
</VListItem>
|
</div>
|
||||||
<VListItem class="ps-0">
|
<div class="plugin-market-detail__metadata-row">
|
||||||
<VListItemTitle class="text-center text-md-left">
|
<dt>{{ t('common.author') }}:</dt>
|
||||||
<span class="font-weight-medium">{{ t('common.author') }}:</span>
|
<dd>
|
||||||
<span class="text-body-1 cursor-pointer" @click="visitPluginPage">
|
<button type="button" class="plugin-market-detail__author" @click="visitPluginPage">
|
||||||
{{ props.plugin?.plugin_author }}
|
{{ props.plugin?.plugin_author }}
|
||||||
</span>
|
</button>
|
||||||
</VListItemTitle>
|
</dd>
|
||||||
</VListItem>
|
</div>
|
||||||
<VListItem v-if="props.plugin?.system_version" class="ps-0">
|
<div v-if="props.plugin?.system_version" class="plugin-market-detail__metadata-row">
|
||||||
<VListItemTitle class="text-center text-md-left">
|
<dt>{{ t('plugin.systemVersion') }}:</dt>
|
||||||
<span class="font-weight-medium">{{ t('plugin.systemVersion') }}:</span>
|
<dd>{{ props.plugin?.system_version }}</dd>
|
||||||
<span class="text-body-1">{{ props.plugin?.system_version }}</span>
|
</div>
|
||||||
</VListItemTitle>
|
<div v-if="rating.rating_count > 0" class="plugin-market-detail__metadata-row">
|
||||||
</VListItem>
|
<dt>{{ t('plugin.rating') }}:</dt>
|
||||||
<VListItem v-if="rating.rating_count > 0" class="ps-0">
|
<dd class="plugin-market-detail__rating-summary">
|
||||||
<VListItemTitle class="plugin-market-detail-rating-row text-center text-md-left">
|
<PluginRatingDisplay :rating="rating.average_rating" :count="rating.rating_count" :icon-size="18" />
|
||||||
<span class="font-weight-medium">{{ t('plugin.rating') }}:</span>
|
</dd>
|
||||||
<PluginRatingDisplay
|
</div>
|
||||||
:rating="rating.average_rating"
|
</dl>
|
||||||
:count="rating.rating_count"
|
|
||||||
:icon-size="18"
|
|
||||||
/>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
<VAlert
|
<VAlert
|
||||||
v-if="props.plugin?.system_version_compatible === false"
|
v-if="props.plugin?.system_version_compatible === false"
|
||||||
type="warning"
|
type="warning"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
density="compact"
|
density="compact"
|
||||||
class="mb-3"
|
class="plugin-market-detail__warning"
|
||||||
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="plugin-market-detail-actions">
|
<div class="plugin-market-detail-actions">
|
||||||
<div class="plugin-market-detail-actions__buttons">
|
<div class="plugin-market-detail-actions__buttons">
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="!isInstalled"
|
v-if="!isInstalled"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="installPlugin()"
|
|
||||||
prepend-icon="mdi-download"
|
prepend-icon="mdi-download"
|
||||||
:disabled="props.plugin?.system_version_compatible === false"
|
:disabled="props.plugin?.system_version_compatible === false"
|
||||||
|
@click="installPlugin()"
|
||||||
>
|
>
|
||||||
{{ t('plugin.installToLocal') }}
|
{{ t('plugin.installToLocal') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
@@ -328,19 +317,20 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
{{ t('plugin.update') }}
|
{{ t('plugin.update') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn variant="tonal" @click="showUpdateHistory" prepend-icon="mdi-update">
|
<VBtn variant="tonal" prepend-icon="mdi-update" @click="showUpdateHistory">
|
||||||
{{ t('plugin.versionHistory') }}
|
{{ t('plugin.versionHistory') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
<div class="plugin-market-detail-actions__downloads" v-if="props.count">
|
<div v-if="props.count" class="plugin-market-detail-actions__downloads">
|
||||||
<VIcon icon="mdi-fire" />
|
<VIcon icon="mdi-fire" size="18" />
|
||||||
{{ t('plugin.totalDownloads', { count: formatDownloadCount(props.count) }) }}
|
<span>{{ t('plugin.totalDownloads', { count: formatDownloadCount(props.count) }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isInstalled" class="plugin-market-detail-user-rating mt-5">
|
|
||||||
<div class="text-body-2 font-weight-medium mb-2">
|
<section v-if="isInstalled" class="plugin-market-detail-user-rating">
|
||||||
|
<h3 class="plugin-market-detail-user-rating__title">
|
||||||
{{ t('plugin.yourRating') }}
|
{{ t('plugin.yourRating') }}
|
||||||
</div>
|
</h3>
|
||||||
<div class="plugin-market-detail-user-rating__controls">
|
<div class="plugin-market-detail-user-rating__controls">
|
||||||
<VRating
|
<VRating
|
||||||
v-model="selectedRating"
|
v-model="selectedRating"
|
||||||
@@ -361,76 +351,181 @@ onUnmounted(() => {
|
|||||||
{{ t('plugin.submitRating') }}
|
{{ t('plugin.submitRating') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</VCardItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCol>
|
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.plugin-market-detail__content {
|
||||||
|
padding: 2.25rem 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__avatar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__title {
|
||||||
|
max-inline-size: 100%;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__description {
|
||||||
|
max-inline-size: 28rem;
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__metadata {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.8rem;
|
||||||
|
max-inline-size: 26rem;
|
||||||
|
margin: 1.5rem auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__metadata-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7rem minmax(0, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__metadata dt {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: end;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__metadata dd {
|
||||||
|
min-inline-size: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__author {
|
||||||
|
max-inline-size: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__author:hover,
|
||||||
|
.plugin-market-detail__author:focus-visible {
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__rating-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__warning {
|
||||||
|
margin-block-start: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-market-detail-actions {
|
.plugin-market-detail-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
margin-block-start: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail-actions__buttons {
|
.plugin-market-detail-actions__buttons {
|
||||||
/* 窄屏换行时用统一 gap 控制按钮间距,避免第二个按钮带左边距导致视觉偏移。 */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail-actions__downloads {
|
.plugin-market-detail-actions__downloads {
|
||||||
flex-basis: 100%;
|
display: inline-flex;
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-market-detail-rating-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.25rem;
|
gap: 0.3rem;
|
||||||
flex-wrap: wrap;
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
white-space: normal;
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail-user-rating {
|
.plugin-market-detail-user-rating {
|
||||||
padding-block-start: 1rem;
|
margin-block-start: 1.5rem;
|
||||||
|
padding-block-start: 1.25rem;
|
||||||
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
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;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-market-detail-user-rating__controls {
|
.plugin-market-detail-user-rating__controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
min-inline-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width >= 960px) {
|
@media (width < 360px) {
|
||||||
.plugin-market-detail-actions {
|
.plugin-market-detail__content {
|
||||||
justify-content: flex-start;
|
padding-inline: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail-actions__buttons {
|
.plugin-market-detail__metadata-row {
|
||||||
justify-content: flex-start;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail-actions__downloads {
|
.plugin-market-detail__metadata dt {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail-rating-row {
|
.plugin-market-detail-actions__buttons {
|
||||||
justify-content: flex-start;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail-actions__buttons :deep(.v-btn) {
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width >= 480px) {
|
||||||
|
.plugin-market-detail-user-rating__controls {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -136,6 +136,32 @@ describe('PluginMarketDetailDialog', () => {
|
|||||||
expect(screen.getByRole('button', { name: '提交评分' })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: '提交评分' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps long plugin details in aligned metadata rows', async () => {
|
||||||
|
const pluginDescription = '支持包含较长说明文字和换行内容的插件详情。\n第二行内容保持完整展示。'
|
||||||
|
const pluginAuthor = 'MoviePilot-Plugin-Author-With-A-Long-Name'
|
||||||
|
|
||||||
|
await renderDialog({
|
||||||
|
...basePlugin,
|
||||||
|
installed: true,
|
||||||
|
plugin_desc: pluginDescription,
|
||||||
|
plugin_author: pluginAuthor,
|
||||||
|
system_version: 'v2.15.0 or later',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await screen.findByText(pluginDescription)).toHaveClass('plugin-market-detail__description')
|
||||||
|
expect(screen.getByRole('button', { name: pluginAuthor })).toHaveClass('plugin-market-detail__author')
|
||||||
|
|
||||||
|
const metadata = document.querySelector('.plugin-market-detail__metadata')
|
||||||
|
const metadataRows = metadata?.querySelectorAll('.plugin-market-detail__metadata-row')
|
||||||
|
|
||||||
|
expect(metadataRows).toHaveLength(4)
|
||||||
|
metadataRows?.forEach(row => {
|
||||||
|
expect(row.querySelector(':scope > dt')).not.toBeNull()
|
||||||
|
expect(row.querySelector(':scope > dd')).not.toBeNull()
|
||||||
|
})
|
||||||
|
expect(metadataRows?.[3]?.querySelector('dd > .plugin-rating-display')).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('emits installation completion only after installation succeeds', async () => {
|
it('emits installation completion only after installation succeeds', async () => {
|
||||||
const { emitted } = await renderDialog({ ...basePlugin, installed: false })
|
const { emitted } = await renderDialog({ ...basePlugin, installed: false })
|
||||||
|
|
||||||
|
|||||||
@@ -3460,6 +3460,7 @@ export default {
|
|||||||
plugin: {
|
plugin: {
|
||||||
sort: {
|
sort: {
|
||||||
popular: 'Popular',
|
popular: 'Popular',
|
||||||
|
rating: 'Rating',
|
||||||
name: 'Plugin Name',
|
name: 'Plugin Name',
|
||||||
author: 'Author',
|
author: 'Author',
|
||||||
repository: 'Plugin Repository',
|
repository: 'Plugin Repository',
|
||||||
|
|||||||
@@ -3403,6 +3403,7 @@ export default {
|
|||||||
plugin: {
|
plugin: {
|
||||||
sort: {
|
sort: {
|
||||||
popular: '热门',
|
popular: '热门',
|
||||||
|
rating: '评分',
|
||||||
name: '插件名称',
|
name: '插件名称',
|
||||||
author: '作者',
|
author: '作者',
|
||||||
repository: '插件仓库',
|
repository: '插件仓库',
|
||||||
|
|||||||
@@ -3402,6 +3402,7 @@ export default {
|
|||||||
plugin: {
|
plugin: {
|
||||||
sort: {
|
sort: {
|
||||||
popular: '熱門',
|
popular: '熱門',
|
||||||
|
rating: '評分',
|
||||||
name: '插件名稱',
|
name: '插件名稱',
|
||||||
author: '作者',
|
author: '作者',
|
||||||
repository: '插件倉庫',
|
repository: '插件倉庫',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useToast } from 'vue-toastification'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
|
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
|
||||||
import NoDataFound from '@/components/states/NoDataFound.vue'
|
import NoDataFound from '@/components/states/NoDataFound.vue'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
|
||||||
import { getPluginTabs } from '@/router/i18n-menu'
|
import { getPluginTabs } from '@/router/i18n-menu'
|
||||||
import { useDynamicButton, type DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
import { useDynamicButton, type DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -43,7 +42,7 @@ interface PluginFolderConfig {
|
|||||||
|
|
||||||
type PluginFolderEntry = PluginFolderConfig | string[]
|
type PluginFolderEntry = PluginFolderConfig | string[]
|
||||||
type PluginFolderMap = Record<string, PluginFolderEntry>
|
type PluginFolderMap = Record<string, PluginFolderEntry>
|
||||||
type PluginSortKey = 'count' | 'plugin_name' | 'plugin_author' | 'repo_url' | 'add_time'
|
type PluginSortKey = 'count' | 'average_rating' | 'plugin_name' | 'plugin_author' | 'repo_url' | 'add_time'
|
||||||
|
|
||||||
// 市场卡片、拖拽排序和市场设置只在对应标签/操作中需要,延迟到真正使用时加载。
|
// 市场卡片、拖拽排序和市场设置只在对应标签/操作中需要,延迟到真正使用时加载。
|
||||||
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
||||||
@@ -152,6 +151,7 @@ const orderConfig = ref<PluginOrderItem[]>([])
|
|||||||
// 排序选项
|
// 排序选项
|
||||||
const sortOptions = computed<{ title: string; value: PluginSortKey }[]>(() => [
|
const sortOptions = computed<{ title: string; value: PluginSortKey }[]>(() => [
|
||||||
{ title: t('plugin.sort.popular'), value: 'count' },
|
{ title: t('plugin.sort.popular'), value: 'count' },
|
||||||
|
{ title: t('plugin.sort.rating'), value: 'average_rating' },
|
||||||
{ title: t('plugin.sort.name'), value: 'plugin_name' },
|
{ title: t('plugin.sort.name'), value: 'plugin_name' },
|
||||||
{ title: t('plugin.sort.author'), value: 'plugin_author' },
|
{ title: t('plugin.sort.author'), value: 'plugin_author' },
|
||||||
{ title: t('plugin.sort.repository'), value: 'repo_url' },
|
{ title: t('plugin.sort.repository'), value: 'repo_url' },
|
||||||
@@ -1072,7 +1072,7 @@ async function refreshData(context: KeepAliveRefreshContext = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对uninstalledList进行排序到sortedUninstalledList
|
// 对uninstalledList进行排序到sortedUninstalledList
|
||||||
watch([marketList, filterForm, activeSort, PluginStatistics], () => {
|
watch([marketList, filterForm, activeSort, PluginStatistics, PluginRatings], () => {
|
||||||
// 匹配过滤函数
|
// 匹配过滤函数
|
||||||
const match = (filter: Array<string>, value: unknown) => {
|
const match = (filter: Array<string>, value: unknown) => {
|
||||||
const text = normalizeMarketText(value).trim()
|
const text = normalizeMarketText(value).trim()
|
||||||
@@ -1107,13 +1107,18 @@ watch([marketList, filterForm, activeSort, PluginStatistics], () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 排序
|
// 排序
|
||||||
if (!isNullOrEmptyObject(PluginStatistics.value)) {
|
const sortKey = activeSort.value || 'count'
|
||||||
if (!activeSort.value || activeSort.value === 'count') {
|
if (sortKey === 'count') {
|
||||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||||
return (PluginStatistics.value[b.id || '0'] ?? 0) - (PluginStatistics.value[a.id || '0'] ?? 0)
|
return (PluginStatistics.value[b.id || '0'] ?? 0) - (PluginStatistics.value[a.id || '0'] ?? 0)
|
||||||
})
|
})
|
||||||
} else if (activeSort.value) {
|
} else if (sortKey === 'average_rating') {
|
||||||
const sortKey = activeSort.value
|
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||||
|
const aRating = PluginRatings.value[a.id]?.average_rating ?? a.average_rating ?? 0
|
||||||
|
const bRating = PluginRatings.value[b.id]?.average_rating ?? b.average_rating ?? 0
|
||||||
|
return bRating - aRating
|
||||||
|
})
|
||||||
|
} else {
|
||||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||||
if (sortKey === 'add_time') {
|
if (sortKey === 'add_time') {
|
||||||
return a.add_time !== undefined && b.add_time !== undefined && a.add_time > b.add_time ? 1 : -1
|
return a.add_time !== undefined && b.add_time !== undefined && a.add_time > b.add_time ? 1 : -1
|
||||||
@@ -1124,7 +1129,6 @@ watch([marketList, filterForm, activeSort, PluginStatistics], () => {
|
|||||||
return aValue !== undefined && bValue !== undefined && aValue > bValue ? 1 : -1
|
return aValue !== undefined && bValue !== undefined && aValue > bValue ? 1 : -1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 显示前20个
|
// 显示前20个
|
||||||
displayUninstalledList.value = sortedUninstalledList.value.splice(0, 20)
|
displayUninstalledList.value = sortedUninstalledList.value.splice(0, 20)
|
||||||
|
|||||||
@@ -701,6 +701,45 @@ describe('PluginCardListView market filtering and pagination', () => {
|
|||||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sorts market entries by rating descending when asynchronous ratings arrive', async () => {
|
||||||
|
const ratings = createDeferred<Record<string, PluginRating>>()
|
||||||
|
await renderList({
|
||||||
|
market: () => [
|
||||||
|
createPlugin({ id: 'Low', plugin_name: '低评分插件' }),
|
||||||
|
createPlugin({ id: 'Unrated', plugin_name: '未评分插件' }),
|
||||||
|
createPlugin({ id: 'High', plugin_name: '高评分插件' }),
|
||||||
|
createPlugin({ id: 'Medium', plugin_name: '中评分插件' }),
|
||||||
|
],
|
||||||
|
rating: () => ratings.promise,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await screen.findByText('market:低评分插件')).toBeInTheDocument()
|
||||||
|
getHeaderConfig().modelValue.value = 'market'
|
||||||
|
await nextTick()
|
||||||
|
const marketFilterButton = getHeaderConfig().appendButtons.find(button => button.dataAttr === 'market-filter-btn')
|
||||||
|
if (!marketFilterButton?.action) throw new Error('未注册市场过滤操作')
|
||||||
|
marketFilterButton.action()
|
||||||
|
await nextTick()
|
||||||
|
await fireEvent.click(screen.getByText('评分'))
|
||||||
|
|
||||||
|
ratings.resolve({
|
||||||
|
High: { average_rating: 4.8, plugin_id: 'High', rating_count: 12 },
|
||||||
|
Low: { average_rating: 2.1, plugin_id: 'Low', rating_count: 3 },
|
||||||
|
Medium: { average_rating: 3.6, plugin_id: 'Medium', rating_count: 6 },
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const labels = [...document.querySelectorAll('[data-testid^="market-"]')].map(node => node.textContent)
|
||||||
|
expect(labels).toEqual([
|
||||||
|
expect.stringContaining('market:高评分插件'),
|
||||||
|
expect.stringContaining('market:中评分插件'),
|
||||||
|
expect.stringContaining('market:低评分插件'),
|
||||||
|
expect.stringContaining('market:未评分插件'),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
await waitForRequestsToFinish()
|
||||||
|
})
|
||||||
|
|
||||||
it('filters and sorts market entries, labels local repos, and appends pages of 20', async () => {
|
it('filters and sorts market entries, labels local repos, and appends pages of 20', async () => {
|
||||||
const market = Array.from({ length: 25 }, (_, index) =>
|
const market = Array.from({ length: 25 }, (_, index) =>
|
||||||
createPlugin({
|
createPlugin({
|
||||||
|
|||||||
Reference in New Issue
Block a user