mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
Propagate plugin rating updates and preserve rating snapshots
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { ApiResponse, Plugin } from '@/api/types'
|
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
|
||||||
import { getLogoUrl } from '@/utils/imageUtils'
|
import { getLogoUrl } from '@/utils/imageUtils'
|
||||||
import { getCardAccentRgbFromImage } from '@/composables/useCardAccentColor'
|
import { getCardAccentRgbFromImage } from '@/composables/useCardAccentColor'
|
||||||
import { formatDownloadCount } from '@/@core/utils/formatters'
|
import { formatDownloadCount } from '@/@core/utils/formatters'
|
||||||
@@ -34,7 +34,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['remove', 'save', 'actionDone'])
|
const emit = defineEmits(['remove', 'save', 'actionDone', 'rating'])
|
||||||
|
|
||||||
// 多语言
|
// 多语言
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -392,6 +392,7 @@ async function showPluginAbout() {
|
|||||||
// 详情弹窗的安装事件只刷新父列表,动态导航由卡片补充同步。
|
// 详情弹窗的安装事件只刷新父列表,动态导航由卡片补充同步。
|
||||||
void pluginSidebarNavStore.ensureSidebarNav(true)
|
void pluginSidebarNavStore.ensureSidebarNav(true)
|
||||||
},
|
},
|
||||||
|
rating: (pluginRating: PluginRating) => emit('rating', pluginRating),
|
||||||
},
|
},
|
||||||
{ closeOn: ['close', 'install', 'update:modelValue'] },
|
{ closeOn: ['close', 'install', 'update:modelValue'] },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { PluginRating } from '@/api/types'
|
||||||
import PluginCard from './PluginCard.vue'
|
import PluginCard from './PluginCard.vue'
|
||||||
import PluginFolderCard from './PluginFolderCard.vue'
|
import PluginFolderCard from './PluginFolderCard.vue'
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ const emit = defineEmits<{
|
|||||||
renameFolder: [oldName: string, newName: string]
|
renameFolder: [oldName: string, newName: string]
|
||||||
updateFolderConfig: [folderName: string, config: any]
|
updateFolderConfig: [folderName: string, config: any]
|
||||||
refreshData: []
|
refreshData: []
|
||||||
|
rating: [pluginRating: PluginRating]
|
||||||
actionDone: [pluginId: string]
|
actionDone: [pluginId: string]
|
||||||
removeFromFolder: [pluginId: string]
|
removeFromFolder: [pluginId: string]
|
||||||
dropToFolder: [event: DragEvent, folderName: string]
|
dropToFolder: [event: DragEvent, folderName: string]
|
||||||
@@ -108,6 +110,7 @@ function handleDropToFolder(event: DragEvent) {
|
|||||||
:sortable="sortable"
|
:sortable="sortable"
|
||||||
@remove="$emit('refreshData')"
|
@remove="$emit('refreshData')"
|
||||||
@save="$emit('refreshData')"
|
@save="$emit('refreshData')"
|
||||||
|
@rating="$emit('rating', $event)"
|
||||||
@action-done="$emit('actionDone', item.id)"
|
@action-done="$emit('actionDone', item.id)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,17 @@ describe('PluginCard about menu', () => {
|
|||||||
expect(mocks.openSharedDialog.mock.calls[0][3]).toEqual({
|
expect(mocks.openSharedDialog.mock.calls[0][3]).toEqual({
|
||||||
closeOn: ['close', 'install', 'update:modelValue'],
|
closeOn: ['close', 'install', 'update:modelValue'],
|
||||||
})
|
})
|
||||||
const dialogEvents = mocks.openSharedDialog.mock.calls[0][2] as { install: () => void }
|
const dialogEvents = mocks.openSharedDialog.mock.calls[0][2] as {
|
||||||
|
install: () => void
|
||||||
|
rating: (pluginRating: { plugin_id: string; average_rating: number; rating_count: number }) => void
|
||||||
|
}
|
||||||
|
dialogEvents.rating({ plugin_id: 'DemoPlugin', average_rating: 4.5, rating_count: 13 })
|
||||||
|
expect(emitted().rating).toContainEqual([
|
||||||
|
expect.objectContaining({ plugin_id: 'DemoPlugin', average_rating: 4.5, rating_count: 13 }),
|
||||||
|
])
|
||||||
|
expect(emitted()).not.toHaveProperty('save')
|
||||||
|
expect(sidebarStore.ensureSidebarNav).not.toHaveBeenCalled()
|
||||||
|
|
||||||
dialogEvents.install()
|
dialogEvents.install()
|
||||||
expect(emitted().save).toHaveLength(1)
|
expect(emitted().save).toHaveLength(1)
|
||||||
expect(sidebarStore.ensureSidebarNav).toHaveBeenCalledWith(true)
|
expect(sidebarStore.ensureSidebarNav).toHaveBeenCalledWith(true)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['update:modelValue', 'close', 'install'])
|
const emit = defineEmits(['update:modelValue', 'close', 'install', 'rating'])
|
||||||
|
|
||||||
// 弹窗显示状态
|
// 弹窗显示状态
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
@@ -220,6 +220,7 @@ async function submitPluginRating() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
rating.value = result.data
|
rating.value = result.data
|
||||||
selectedRating.value = result.data.user_rating || selectedRating.value
|
selectedRating.value = result.data.user_rating || selectedRating.value
|
||||||
|
emit('rating', result.data)
|
||||||
$toast.success(t('plugin.ratingSuccess', { name: props.plugin?.plugin_name }))
|
$toast.success(t('plugin.ratingSuccess', { name: props.plugin?.plugin_name }))
|
||||||
} else {
|
} else {
|
||||||
$toast.error(t('plugin.ratingFailed', { message: result.message || t('common.unknown') }))
|
$toast.error(t('plugin.ratingFailed', { message: result.message || t('common.unknown') }))
|
||||||
@@ -261,6 +262,9 @@ onUnmounted(() => {
|
|||||||
<p v-if="props.plugin?.plugin_desc" class="plugin-market-detail__description">
|
<p v-if="props.plugin?.plugin_desc" class="plugin-market-detail__description">
|
||||||
{{ props.plugin?.plugin_desc }}
|
{{ props.plugin?.plugin_desc }}
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="rating.rating_count > 0" class="plugin-market-detail__header-rating">
|
||||||
|
<PluginRatingDisplay :rating="rating.average_rating" :count="rating.rating_count" :icon-size="18" />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<dl class="plugin-market-detail__metadata">
|
<dl class="plugin-market-detail__metadata">
|
||||||
@@ -276,16 +280,6 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</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>
|
</dl>
|
||||||
|
|
||||||
<VAlert
|
<VAlert
|
||||||
@@ -392,10 +386,17 @@ onUnmounted(() => {
|
|||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugin-market-detail__header-rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-block: 0.75rem 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-market-detail__metadata {
|
.plugin-market-detail__metadata {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
margin: 1.125rem auto 0;
|
margin: 1.125rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail__metadata-row {
|
.plugin-market-detail__metadata-row {
|
||||||
@@ -441,12 +442,6 @@ onUnmounted(() => {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-detail__rating-summary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-market-detail__warning {
|
.plugin-market-detail__warning {
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ const basePlugin: Plugin = {
|
|||||||
plugin_version: '1.0.0',
|
plugin_version: '1.0.0',
|
||||||
plugin_author: 'MoviePilot',
|
plugin_author: 'MoviePilot',
|
||||||
repo_url: 'https://github.com/example/plugins',
|
repo_url: 'https://github.com/example/plugins',
|
||||||
|
average_rating: 4.3,
|
||||||
|
rating_count: 12,
|
||||||
|
user_rating: 4.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratingResult: PluginRating = {
|
const ratingResult: PluginRating = {
|
||||||
@@ -102,7 +105,7 @@ describe('PluginMarketDetailDialog', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('hides install action and submits a half-star rating for an installed plugin', async () => {
|
it('hides install action and submits a half-star rating for an installed plugin', async () => {
|
||||||
await renderDialog({ ...basePlugin, installed: true })
|
const { emitted } = await renderDialog({ ...basePlugin, installed: true })
|
||||||
|
|
||||||
expect(await screen.findByText('提交评分')).toBeInTheDocument()
|
expect(await screen.findByText('提交评分')).toBeInTheDocument()
|
||||||
expect(screen.queryByText('安装到本地')).not.toBeInTheDocument()
|
expect(screen.queryByText('安装到本地')).not.toBeInTheDocument()
|
||||||
@@ -117,6 +120,9 @@ describe('PluginMarketDetailDialog', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mocks.apiPost).toHaveBeenCalledWith('plugin/rating/DemoPlugin', { rating: 4.5 })
|
expect(mocks.apiPost).toHaveBeenCalledWith('plugin/rating/DemoPlugin', { rating: 4.5 })
|
||||||
})
|
})
|
||||||
|
expect(emitted().rating).toContainEqual([
|
||||||
|
expect.objectContaining({ average_rating: 4.5, plugin_id: 'DemoPlugin', user_rating: 4.5 }),
|
||||||
|
])
|
||||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('已提交对插件 演示插件 的评分')
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('已提交对插件 演示插件 的评分')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -128,7 +134,13 @@ describe('PluginMarketDetailDialog', () => {
|
|||||||
user_rating: undefined,
|
user_rating: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
await renderDialog({ ...basePlugin, installed: true })
|
await renderDialog({
|
||||||
|
...basePlugin,
|
||||||
|
installed: true,
|
||||||
|
average_rating: 0,
|
||||||
|
rating_count: 0,
|
||||||
|
user_rating: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
expect(await screen.findByText('v1.0.0')).toBeInTheDocument()
|
expect(await screen.findByText('v1.0.0')).toBeInTheDocument()
|
||||||
expect(screen.queryByText('插件评分:')).not.toBeInTheDocument()
|
expect(screen.queryByText('插件评分:')).not.toBeInTheDocument()
|
||||||
@@ -156,12 +168,18 @@ describe('PluginMarketDetailDialog', () => {
|
|||||||
const metadata = document.querySelector('.plugin-market-detail__metadata')
|
const metadata = document.querySelector('.plugin-market-detail__metadata')
|
||||||
const metadataRows = metadata?.querySelectorAll('.plugin-market-detail__metadata-row')
|
const metadataRows = metadata?.querySelectorAll('.plugin-market-detail__metadata-row')
|
||||||
|
|
||||||
expect(metadataRows).toHaveLength(4)
|
expect(metadataRows).toHaveLength(2)
|
||||||
metadataRows?.forEach(row => {
|
metadataRows?.forEach(row => {
|
||||||
expect(row.querySelector(':scope > dt')).not.toBeNull()
|
expect(row.querySelector(':scope > dt')).not.toBeNull()
|
||||||
expect(row.querySelector(':scope > dd')).not.toBeNull()
|
expect(row.querySelector(':scope > dd')).not.toBeNull()
|
||||||
})
|
})
|
||||||
expect(metadataRows?.[3]?.querySelector('dd > .plugin-rating-display')).not.toBeNull()
|
|
||||||
|
const headerRating = document.querySelector('.plugin-market-detail__header-rating')
|
||||||
|
|
||||||
|
expect(headerRating?.previousElementSibling).toBe(description)
|
||||||
|
expect(headerRating?.querySelector('.plugin-rating-display')).not.toBeNull()
|
||||||
|
expect(screen.queryByText('插件评分:')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('v2.15.0 or later')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits installation completion only after installation succeeds', async () => {
|
it('emits installation completion only after installation succeeds', async () => {
|
||||||
@@ -351,11 +369,13 @@ describe('PluginMarketDetailDialog', () => {
|
|||||||
expect(mocks.toastError).toHaveBeenCalledWith('评分提交失败:服务器连接失败')
|
expect(mocks.toastError).toHaveBeenCalledWith('评分提交失败:服务器连接失败')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps plugin actions usable when rating loading fails', async () => {
|
it('refreshes the current plugin rating when the detail opens', async () => {
|
||||||
mocks.apiGet.mockRejectedValue(new Error('rating unavailable'))
|
|
||||||
await renderDialog({ ...basePlugin, installed: false })
|
await renderDialog({ ...basePlugin, installed: false })
|
||||||
|
|
||||||
expect(await screen.findByRole('button', { name: '安装到本地' })).toBeEnabled()
|
expect(await screen.findByRole('button', { name: '安装到本地' })).toBeEnabled()
|
||||||
|
expect(screen.getByLabelText('4.3 / 5')).toBeInTheDocument()
|
||||||
|
expect(mocks.apiGet).toHaveBeenCalledOnce()
|
||||||
|
expect(mocks.apiGet).toHaveBeenCalledWith('plugin/rating/DemoPlugin')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens installed version history without an update action and closes through the model contract', async () => {
|
it('opens installed version history without an update action and closes through the model contract', async () => {
|
||||||
|
|||||||
@@ -938,6 +938,7 @@ async function fetchInstalledPlugins(context: KeepAliveRefreshContext = {}) {
|
|||||||
})
|
})
|
||||||
if (generation !== installedWriterGeneration) return
|
if (generation !== installedWriterGeneration) return
|
||||||
|
|
||||||
|
mergeRatingsIntoPlugins(installedPlugins)
|
||||||
dataList.value = installedPlugins
|
dataList.value = installedPlugins
|
||||||
mergeMarketMetadataIntoInstalled()
|
mergeMarketMetadataIntoInstalled()
|
||||||
// 排序
|
// 排序
|
||||||
@@ -984,6 +985,7 @@ async function fetchUninstalledPlugins(force: boolean = false, context: KeepAliv
|
|||||||
})
|
})
|
||||||
if (generation !== marketWriterGeneration) return
|
if (generation !== marketWriterGeneration) return
|
||||||
|
|
||||||
|
mergeRatingsIntoPlugins(marketResponse)
|
||||||
uninstalledList.value = marketResponse
|
uninstalledList.value = marketResponse
|
||||||
mergeMarketMetadataIntoInstalled()
|
mergeMarketMetadataIntoInstalled()
|
||||||
// 更新插件市场列表
|
// 更新插件市场列表
|
||||||
@@ -1050,23 +1052,56 @@ async function getPluginRatings() {
|
|||||||
|
|
||||||
PluginRatings.value = ratings
|
PluginRatings.value = ratings
|
||||||
|
|
||||||
for (const plugin of [...dataList.value, ...uninstalledList.value, ...marketList.value]) {
|
mergeRatingsIntoPlugins([...dataList.value, ...uninstalledList.value, ...marketList.value], ratings, true)
|
||||||
const pluginRating = ratings[plugin.id]
|
|
||||||
if (!pluginRating) continue
|
|
||||||
plugin.average_rating = pluginRating.average_rating
|
|
||||||
plugin.rating_count = pluginRating.rating_count
|
|
||||||
plugin.user_rating = pluginRating.user_rating
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 下载量与评分属于同一份市场指标快照,始终在同一刷新时机加载。 */
|
||||||
|
async function getPluginMarketMetrics() {
|
||||||
|
await Promise.all([getPluginStatistics(), getPluginRatings()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新列表写入前复用最近一次评分快照,避免静默刷新期间评分闪烁。 */
|
||||||
|
function mergeRatingsIntoPlugins(
|
||||||
|
plugins: Plugin[],
|
||||||
|
ratings: Record<string, PluginRating> = PluginRatings.value,
|
||||||
|
overwrite = false,
|
||||||
|
) {
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
const pluginRating = ratings[plugin.id]
|
||||||
|
if (!pluginRating) continue
|
||||||
|
if (overwrite || plugin.average_rating === undefined) plugin.average_rating = pluginRating.average_rating
|
||||||
|
if (overwrite || plugin.rating_count === undefined) plugin.rating_count = pluginRating.rating_count
|
||||||
|
if (overwrite || plugin.user_rating === undefined) plugin.user_rating = pluginRating.user_rating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 评分提交接口已返回新值,只回写当前插件,避免重新加载全部市场指标。 */
|
||||||
|
function applyPluginRating(pluginRating: PluginRating) {
|
||||||
|
const pluginId = pluginRating.plugin_id
|
||||||
|
if (!pluginId) return
|
||||||
|
|
||||||
|
PluginRatings.value = {
|
||||||
|
...PluginRatings.value,
|
||||||
|
[pluginId]: pluginRating,
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeRatingsIntoPlugins(
|
||||||
|
[...dataList.value, ...uninstalledList.value, ...marketList.value],
|
||||||
|
{
|
||||||
|
[pluginId]: pluginRating,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载所有数据
|
// 加载所有数据
|
||||||
async function refreshData(context: KeepAliveRefreshContext = {}) {
|
async function refreshData(context: KeepAliveRefreshContext = {}) {
|
||||||
await fetchInstalledPlugins(context)
|
await fetchInstalledPlugins(context)
|
||||||
await fetchUninstalledPlugins(false, context)
|
await fetchUninstalledPlugins(false, context)
|
||||||
await Promise.all([getPluginStatistics(), getPluginRatings()])
|
await getPluginMarketMetrics()
|
||||||
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
|
// 重新加载文件夹配置,确保分身插件能正确显示在文件夹中
|
||||||
await loadPluginFolders()
|
await loadPluginFolders()
|
||||||
}
|
}
|
||||||
@@ -1154,7 +1189,7 @@ async function refreshMarket() {
|
|||||||
isMarketRefreshing.value = true
|
isMarketRefreshing.value = true
|
||||||
try {
|
try {
|
||||||
await fetchUninstalledPlugins(true, { silent: false, source: 'manual' })
|
await fetchUninstalledPlugins(true, { silent: false, source: 'manual' })
|
||||||
await Promise.all([getPluginStatistics(), getPluginRatings()])
|
await getPluginMarketMetrics()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1167,13 +1202,13 @@ async function refreshActiveTabData(context: KeepAliveRefreshContext = {}) {
|
|||||||
|
|
||||||
if (activeTab.value === 'market') {
|
if (activeTab.value === 'market') {
|
||||||
await fetchUninstalledPlugins(false, context)
|
await fetchUninstalledPlugins(false, context)
|
||||||
await Promise.all([getPluginStatistics(), getPluginRatings()])
|
await getPluginMarketMetrics()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchInstalledPlugins(context)
|
await fetchInstalledPlugins(context)
|
||||||
await fetchUninstalledPlugins(false, context)
|
await fetchUninstalledPlugins(false, context)
|
||||||
await Promise.all([getPluginStatistics(), getPluginRatings()])
|
await getPluginMarketMetrics()
|
||||||
// 文件夹配置可能在其它入口被插件操作改变,重新进入时同步一次。
|
// 文件夹配置可能在其它入口被插件操作改变,重新进入时同步一次。
|
||||||
await loadPluginFolders()
|
await loadPluginFolders()
|
||||||
}
|
}
|
||||||
@@ -1949,6 +1984,7 @@ function onDragStartPlugin(evt: { oldIndex?: number; item?: HTMLElement }) {
|
|||||||
@rename-folder="(oldName, newName) => renameFolder(oldName, newName)"
|
@rename-folder="(oldName, newName) => renameFolder(oldName, newName)"
|
||||||
@update-folder-config="(folderName, config) => updateFolderConfig(folderName, config)"
|
@update-folder-config="(folderName, config) => updateFolderConfig(folderName, config)"
|
||||||
@refresh-data="refreshData"
|
@refresh-data="refreshData"
|
||||||
|
@rating="applyPluginRating"
|
||||||
@action-done="
|
@action-done="
|
||||||
pluginId => {
|
pluginId => {
|
||||||
pluginActions[pluginId] = false
|
pluginActions[pluginId] = false
|
||||||
@@ -1977,6 +2013,7 @@ function onDragStartPlugin(evt: { oldIndex?: number; item?: HTMLElement }) {
|
|||||||
@rename-folder="(oldName, newName) => renameFolder(oldName, newName)"
|
@rename-folder="(oldName, newName) => renameFolder(oldName, newName)"
|
||||||
@update-folder-config="(folderName, config) => updateFolderConfig(folderName, config)"
|
@update-folder-config="(folderName, config) => updateFolderConfig(folderName, config)"
|
||||||
@refresh-data="refreshData"
|
@refresh-data="refreshData"
|
||||||
|
@rating="applyPluginRating"
|
||||||
@action-done="
|
@action-done="
|
||||||
pluginId => {
|
pluginId => {
|
||||||
pluginActions[pluginId] = false
|
pluginActions[pluginId] = false
|
||||||
@@ -2008,6 +2045,7 @@ function onDragStartPlugin(evt: { oldIndex?: number; item?: HTMLElement }) {
|
|||||||
:sortable="true"
|
:sortable="true"
|
||||||
:show-remove-button="true"
|
:show-remove-button="true"
|
||||||
@refresh-data="refreshData"
|
@refresh-data="refreshData"
|
||||||
|
@rating="applyPluginRating"
|
||||||
@action-done="
|
@action-done="
|
||||||
pluginId => {
|
pluginId => {
|
||||||
pluginActions[pluginId] = false
|
pluginActions[pluginId] = false
|
||||||
@@ -2032,6 +2070,7 @@ function onDragStartPlugin(evt: { oldIndex?: number; item?: HTMLElement }) {
|
|||||||
:sortable="false"
|
:sortable="false"
|
||||||
:show-remove-button="true"
|
:show-remove-button="true"
|
||||||
@refresh-data="refreshData"
|
@refresh-data="refreshData"
|
||||||
|
@rating="applyPluginRating"
|
||||||
@action-done="
|
@action-done="
|
||||||
pluginId => {
|
pluginId => {
|
||||||
pluginActions[pluginId] = false
|
pluginActions[pluginId] = false
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ const PluginMixedSortCardStub = defineComponent({
|
|||||||
'delete-folder',
|
'delete-folder',
|
||||||
'drop-to-folder',
|
'drop-to-folder',
|
||||||
'open-folder',
|
'open-folder',
|
||||||
|
'rating',
|
||||||
'refresh-data',
|
'refresh-data',
|
||||||
'rename-folder',
|
'rename-folder',
|
||||||
'remove-from-folder',
|
'remove-from-folder',
|
||||||
@@ -219,6 +220,22 @@ const PluginMixedSortCardStub = defineComponent({
|
|||||||
type === 'plugin'
|
type === 'plugin'
|
||||||
? h('button', { onClick: () => emit('remove-from-folder', id), type: 'button' }, `remove-plugin-${id}`)
|
? h('button', { onClick: () => emit('remove-from-folder', id), type: 'button' }, `remove-plugin-${id}`)
|
||||||
: null,
|
: null,
|
||||||
|
type === 'plugin'
|
||||||
|
? h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
onClick: () =>
|
||||||
|
emit('rating', {
|
||||||
|
average_rating: 4.7,
|
||||||
|
plugin_id: id,
|
||||||
|
rating_count: 10,
|
||||||
|
user_rating: 5,
|
||||||
|
} satisfies PluginRating),
|
||||||
|
type: 'button',
|
||||||
|
},
|
||||||
|
`rate-plugin-${id}`,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -542,6 +559,58 @@ describe('PluginCardListView loading and request ownership', () => {
|
|||||||
expect(screen.getByLabelText('statistic-Installed')).toHaveTextContent('99')
|
expect(screen.getByLabelText('statistic-Installed')).toHaveTextContent('99')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('updates only the submitted plugin from the POST result without reloading market metrics', async () => {
|
||||||
|
let ratingRequest = 0
|
||||||
|
let statisticRequest = 0
|
||||||
|
await renderList({
|
||||||
|
installed: () => [createPlugin({ id: 'Installed', installed: true, plugin_name: '已安装插件' })],
|
||||||
|
rating: () => {
|
||||||
|
ratingRequest += 1
|
||||||
|
return { Installed: { average_rating: 3, plugin_id: 'Installed', rating_count: 1 } }
|
||||||
|
},
|
||||||
|
statistic: () => {
|
||||||
|
statisticRequest += 1
|
||||||
|
return { Installed: 24 }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await waitForRequestsToFinish()
|
||||||
|
|
||||||
|
expect(ratingRequest).toBe(1)
|
||||||
|
expect(statisticRequest).toBe(1)
|
||||||
|
await fireEvent.click(screen.getByRole('button', { name: 'rate-plugin-Installed' }))
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('installed-rating-Installed')).toHaveTextContent('4.7')
|
||||||
|
expect(ratingRequest).toBe(1)
|
||||||
|
expect(statisticRequest).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the previous rating visible while a tab refresh loads the next rating snapshot', async () => {
|
||||||
|
const refreshedRatings = createDeferred<Record<string, PluginRating>>()
|
||||||
|
let ratingRequest = 0
|
||||||
|
await renderList({
|
||||||
|
installed: () => [createPlugin({ id: 'Installed', installed: true, plugin_name: '已安装插件' })],
|
||||||
|
rating: () => {
|
||||||
|
ratingRequest += 1
|
||||||
|
if (ratingRequest === 1) {
|
||||||
|
return { Installed: { average_rating: 4.2, plugin_id: 'Installed', rating_count: 3 } }
|
||||||
|
}
|
||||||
|
return refreshedRatings.promise
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await waitForRequestsToFinish()
|
||||||
|
|
||||||
|
if (!mocks.keepAliveHandler) throw new Error('未注册 keep-alive 刷新回调')
|
||||||
|
const refresh = mocks.keepAliveHandler({ silent: true, source: 'tab' })
|
||||||
|
await waitFor(() => expect(ratingRequest).toBe(2))
|
||||||
|
expect(screen.getByLabelText('installed-rating-Installed')).toHaveTextContent('4.2')
|
||||||
|
|
||||||
|
refreshedRatings.resolve({
|
||||||
|
Installed: { average_rating: 4.8, plugin_id: 'Installed', rating_count: 4 },
|
||||||
|
})
|
||||||
|
await refresh
|
||||||
|
expect(screen.getByLabelText('installed-rating-Installed')).toHaveTextContent('4.8')
|
||||||
|
})
|
||||||
|
|
||||||
it('rejects a rating snapshot when the current plugin ID set changes before the next rating request', async () => {
|
it('rejects a rating snapshot when the current plugin ID set changes before the next rating request', async () => {
|
||||||
const staleRatings = createDeferred<Record<string, PluginRating>>()
|
const staleRatings = createDeferred<Record<string, PluginRating>>()
|
||||||
const currentMarket = createDeferred<Plugin[]>()
|
const currentMarket = createDeferred<Plugin[]>()
|
||||||
|
|||||||
Reference in New Issue
Block a user