mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-10 16:13:28 +08:00
Improve plugin market detail layout and rating sorting
This commit is contained in:
@@ -247,190 +247,285 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog v-if="visible" v-model="visible" max-width="30rem">
|
||||
<VCard>
|
||||
<VDialog v-if="visible" v-model="visible" width="100%" max-width="32rem" max-height="90dvh" scrollable>
|
||||
<VCard class="plugin-market-detail">
|
||||
<VDialogCloseBtn v-model="visible" />
|
||||
<VCardText>
|
||||
<VCol>
|
||||
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
|
||||
<div class="mx-auto mt-5">
|
||||
<VAvatar size="64">
|
||||
<VImg :src="pluginIconPath()" aspect-ratio="4/3" cover @error="imageLoadError = true" />
|
||||
</VAvatar>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<VCardItem>
|
||||
<VCardTitle class="text-center text-md-left">
|
||||
{{ props.plugin?.plugin_name }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle
|
||||
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardSubtitle>
|
||||
<VList lines="one" class="border-0">
|
||||
<VListItem class="ps-0">
|
||||
<VListItemTitle class="text-center text-md-left">
|
||||
<span class="font-weight-medium">{{ t('common.version') }}:</span>
|
||||
<span class="text-body-1"> v{{ props.plugin?.plugin_version }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem class="ps-0">
|
||||
<VListItemTitle class="text-center text-md-left">
|
||||
<span class="font-weight-medium">{{ t('common.author') }}:</span>
|
||||
<span class="text-body-1 cursor-pointer" @click="visitPluginPage">
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="props.plugin?.system_version" class="ps-0">
|
||||
<VListItemTitle class="text-center text-md-left">
|
||||
<span class="font-weight-medium">{{ t('plugin.systemVersion') }}:</span>
|
||||
<span class="text-body-1">{{ props.plugin?.system_version }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="rating.rating_count > 0" class="ps-0">
|
||||
<VListItemTitle class="plugin-market-detail-rating-row text-center text-md-left">
|
||||
<span class="font-weight-medium">{{ t('plugin.rating') }}:</span>
|
||||
<PluginRatingDisplay
|
||||
:rating="rating.average_rating"
|
||||
:count="rating.rating_count"
|
||||
:icon-size="18"
|
||||
/>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VAlert
|
||||
v-if="props.plugin?.system_version_compatible === false"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-3"
|
||||
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||
/>
|
||||
<div class="plugin-market-detail-actions">
|
||||
<div class="plugin-market-detail-actions__buttons">
|
||||
<VBtn
|
||||
v-if="!isInstalled"
|
||||
color="primary"
|
||||
@click="installPlugin()"
|
||||
prepend-icon="mdi-download"
|
||||
:disabled="props.plugin?.system_version_compatible === false"
|
||||
>
|
||||
{{ t('plugin.installToLocal') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else-if="props.plugin?.has_update"
|
||||
color="primary"
|
||||
prepend-icon="mdi-arrow-up-circle-outline"
|
||||
:disabled="props.plugin?.system_version_compatible === false"
|
||||
@click="installPlugin()"
|
||||
>
|
||||
{{ t('plugin.update') }}
|
||||
</VBtn>
|
||||
<VBtn variant="tonal" @click="showUpdateHistory" prepend-icon="mdi-update">
|
||||
{{ t('plugin.versionHistory') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
<div class="plugin-market-detail-actions__downloads" v-if="props.count">
|
||||
<VIcon icon="mdi-fire" />
|
||||
{{ t('plugin.totalDownloads', { count: formatDownloadCount(props.count) }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isInstalled" class="plugin-market-detail-user-rating mt-5">
|
||||
<div class="text-body-2 font-weight-medium mb-2">
|
||||
{{ t('plugin.yourRating') }}
|
||||
</div>
|
||||
<div class="plugin-market-detail-user-rating__controls">
|
||||
<VRating
|
||||
v-model="selectedRating"
|
||||
:disabled="ratingLoading || ratingSubmitting"
|
||||
half-increments
|
||||
hover
|
||||
density="compact"
|
||||
active-color="warning"
|
||||
/>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-star-check-outline"
|
||||
:loading="ratingSubmitting"
|
||||
:disabled="ratingLoading || selectedRating <= 0"
|
||||
@click="submitPluginRating"
|
||||
>
|
||||
{{ t('plugin.submitRating') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
</div>
|
||||
<VCardText class="plugin-market-detail__content">
|
||||
<header class="plugin-market-detail__header">
|
||||
<VAvatar size="64" class="plugin-market-detail__avatar">
|
||||
<VImg :src="pluginIconPath()" aspect-ratio="4/3" cover @error="imageLoadError = true" />
|
||||
</VAvatar>
|
||||
<h2 class="plugin-market-detail__title">
|
||||
{{ props.plugin?.plugin_name }}
|
||||
</h2>
|
||||
<p v-if="props.plugin?.plugin_desc" class="plugin-market-detail__description">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<dl class="plugin-market-detail__metadata">
|
||||
<div class="plugin-market-detail__metadata-row">
|
||||
<dt>{{ t('common.version') }}:</dt>
|
||||
<dd>v{{ props.plugin?.plugin_version }}</dd>
|
||||
</div>
|
||||
</VCol>
|
||||
<div class="plugin-market-detail__metadata-row">
|
||||
<dt>{{ t('common.author') }}:</dt>
|
||||
<dd>
|
||||
<button type="button" class="plugin-market-detail__author" @click="visitPluginPage">
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="props.plugin?.system_version" class="plugin-market-detail__metadata-row">
|
||||
<dt>{{ t('plugin.systemVersion') }}:</dt>
|
||||
<dd>{{ props.plugin?.system_version }}</dd>
|
||||
</div>
|
||||
<div v-if="rating.rating_count > 0" class="plugin-market-detail__metadata-row">
|
||||
<dt>{{ t('plugin.rating') }}:</dt>
|
||||
<dd class="plugin-market-detail__rating-summary">
|
||||
<PluginRatingDisplay :rating="rating.average_rating" :count="rating.rating_count" :icon-size="18" />
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<VAlert
|
||||
v-if="props.plugin?.system_version_compatible === false"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="plugin-market-detail__warning"
|
||||
:text="props.plugin?.system_version_message || t('plugin.incompatibleSystemVersion')"
|
||||
/>
|
||||
|
||||
<div class="plugin-market-detail-actions">
|
||||
<div class="plugin-market-detail-actions__buttons">
|
||||
<VBtn
|
||||
v-if="!isInstalled"
|
||||
color="primary"
|
||||
prepend-icon="mdi-download"
|
||||
:disabled="props.plugin?.system_version_compatible === false"
|
||||
@click="installPlugin()"
|
||||
>
|
||||
{{ t('plugin.installToLocal') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else-if="props.plugin?.has_update"
|
||||
color="primary"
|
||||
prepend-icon="mdi-arrow-up-circle-outline"
|
||||
:disabled="props.plugin?.system_version_compatible === false"
|
||||
@click="installPlugin()"
|
||||
>
|
||||
{{ t('plugin.update') }}
|
||||
</VBtn>
|
||||
<VBtn variant="tonal" prepend-icon="mdi-update" @click="showUpdateHistory">
|
||||
{{ t('plugin.versionHistory') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
<div v-if="props.count" class="plugin-market-detail-actions__downloads">
|
||||
<VIcon icon="mdi-fire" size="18" />
|
||||
<span>{{ t('plugin.totalDownloads', { count: formatDownloadCount(props.count) }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="isInstalled" class="plugin-market-detail-user-rating">
|
||||
<h3 class="plugin-market-detail-user-rating__title">
|
||||
{{ t('plugin.yourRating') }}
|
||||
</h3>
|
||||
<div class="plugin-market-detail-user-rating__controls">
|
||||
<VRating
|
||||
v-model="selectedRating"
|
||||
:disabled="ratingLoading || ratingSubmitting"
|
||||
half-increments
|
||||
hover
|
||||
density="compact"
|
||||
active-color="warning"
|
||||
/>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-star-check-outline"
|
||||
:loading="ratingSubmitting"
|
||||
:disabled="ratingLoading || selectedRating <= 0"
|
||||
@click="submitPluginRating"
|
||||
>
|
||||
{{ t('plugin.submitRating') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</section>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-block-start: 1.25rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions__buttons {
|
||||
/* 窄屏换行时用统一 gap 控制按钮间距,避免第二个按钮带左边距导致视觉偏移。 */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions__downloads {
|
||||
flex-basis: 100%;
|
||||
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;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
white-space: normal;
|
||||
gap: 0.3rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
@media (width >= 960px) {
|
||||
.plugin-market-detail-actions {
|
||||
justify-content: flex-start;
|
||||
@media (width < 360px) {
|
||||
.plugin-market-detail__content {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions__buttons {
|
||||
justify-content: flex-start;
|
||||
.plugin-market-detail__metadata-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.plugin-market-detail-actions__downloads {
|
||||
.plugin-market-detail__metadata dt {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.plugin-market-detail-rating-row {
|
||||
justify-content: flex-start;
|
||||
.plugin-market-detail-actions__buttons {
|
||||
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>
|
||||
|
||||
@@ -136,6 +136,32 @@ describe('PluginMarketDetailDialog', () => {
|
||||
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 () => {
|
||||
const { emitted } = await renderDialog({ ...basePlugin, installed: false })
|
||||
|
||||
|
||||
@@ -3460,6 +3460,7 @@ export default {
|
||||
plugin: {
|
||||
sort: {
|
||||
popular: 'Popular',
|
||||
rating: 'Rating',
|
||||
name: 'Plugin Name',
|
||||
author: 'Author',
|
||||
repository: 'Plugin Repository',
|
||||
|
||||
@@ -3403,6 +3403,7 @@ export default {
|
||||
plugin: {
|
||||
sort: {
|
||||
popular: '热门',
|
||||
rating: '评分',
|
||||
name: '插件名称',
|
||||
author: '作者',
|
||||
repository: '插件仓库',
|
||||
|
||||
@@ -3402,6 +3402,7 @@ export default {
|
||||
plugin: {
|
||||
sort: {
|
||||
popular: '熱門',
|
||||
rating: '評分',
|
||||
name: '插件名稱',
|
||||
author: '作者',
|
||||
repository: '插件倉庫',
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
|
||||
import NoDataFound from '@/components/states/NoDataFound.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { getPluginTabs } from '@/router/i18n-menu'
|
||||
import { useDynamicButton, type DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -43,7 +42,7 @@ interface PluginFolderConfig {
|
||||
|
||||
type PluginFolderEntry = PluginFolderConfig | string[]
|
||||
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))
|
||||
@@ -152,6 +151,7 @@ const orderConfig = ref<PluginOrderItem[]>([])
|
||||
// 排序选项
|
||||
const sortOptions = computed<{ title: string; value: PluginSortKey }[]>(() => [
|
||||
{ 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.author'), value: 'plugin_author' },
|
||||
{ title: t('plugin.sort.repository'), value: 'repo_url' },
|
||||
@@ -1072,7 +1072,7 @@ async function refreshData(context: KeepAliveRefreshContext = {}) {
|
||||
}
|
||||
|
||||
// 对uninstalledList进行排序到sortedUninstalledList
|
||||
watch([marketList, filterForm, activeSort, PluginStatistics], () => {
|
||||
watch([marketList, filterForm, activeSort, PluginStatistics, PluginRatings], () => {
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: unknown) => {
|
||||
const text = normalizeMarketText(value).trim()
|
||||
@@ -1107,23 +1107,27 @@ watch([marketList, filterForm, activeSort, PluginStatistics], () => {
|
||||
})
|
||||
|
||||
// 排序
|
||||
if (!isNullOrEmptyObject(PluginStatistics.value)) {
|
||||
if (!activeSort.value || activeSort.value === 'count') {
|
||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||
return (PluginStatistics.value[b.id || '0'] ?? 0) - (PluginStatistics.value[a.id || '0'] ?? 0)
|
||||
})
|
||||
} else if (activeSort.value) {
|
||||
const sortKey = activeSort.value
|
||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||
if (sortKey === 'add_time') {
|
||||
return a.add_time !== undefined && b.add_time !== undefined && a.add_time > b.add_time ? 1 : -1
|
||||
}
|
||||
const sortKey = activeSort.value || 'count'
|
||||
if (sortKey === 'count') {
|
||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||
return (PluginStatistics.value[b.id || '0'] ?? 0) - (PluginStatistics.value[a.id || '0'] ?? 0)
|
||||
})
|
||||
} else if (sortKey === 'average_rating') {
|
||||
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) => {
|
||||
if (sortKey === 'add_time') {
|
||||
return a.add_time !== undefined && b.add_time !== undefined && a.add_time > b.add_time ? 1 : -1
|
||||
}
|
||||
|
||||
const aValue = a[sortKey]
|
||||
const bValue = b[sortKey]
|
||||
return aValue !== undefined && bValue !== undefined && aValue > bValue ? 1 : -1
|
||||
})
|
||||
}
|
||||
const aValue = a[sortKey]
|
||||
const bValue = b[sortKey]
|
||||
return aValue !== undefined && bValue !== undefined && aValue > bValue ? 1 : -1
|
||||
})
|
||||
}
|
||||
|
||||
// 显示前20个
|
||||
|
||||
@@ -701,6 +701,45 @@ describe('PluginCardListView market filtering and pagination', () => {
|
||||
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 () => {
|
||||
const market = Array.from({ length: 25 }, (_, index) =>
|
||||
createPlugin({
|
||||
|
||||
Reference in New Issue
Block a user