From 96d655155a6837a034255a14066b4713fa9c8025 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 9 May 2026 16:07:28 +0800 Subject: [PATCH] perf: virtualize management lists and make drag sorting opt-in --- src/components/cards/PluginCard.vue | 7 +- src/components/cards/PluginFolderCard.vue | 8 +- src/components/cards/PluginMixedSortCard.vue | 12 +- src/components/cards/SiteCard.vue | 26 ++- src/components/cards/SubscribeCard.vue | 7 +- src/components/misc/VirtualCardGrid.vue | 55 ++++++- src/locales/en-US.ts | 2 + src/locales/zh-CN.ts | 2 + src/locales/zh-TW.ts | 2 + src/pages/subscribe.vue | 17 ++ src/utils/siteIconCache.ts | 52 ++++++ src/views/plugin/PluginCardListView.vue | 157 +++++++++++++++---- src/views/site/SiteCardListView.vue | 87 ++++++++-- src/views/subscribe/SubscribeListView.vue | 148 ++++++++++++----- 14 files changed, 481 insertions(+), 101 deletions(-) create mode 100644 src/utils/siteIconCache.ts diff --git a/src/components/cards/PluginCard.vue b/src/components/cards/PluginCard.vue index 65409d6f..cde8bb41 100644 --- a/src/components/cards/PluginCard.vue +++ b/src/components/cards/PluginCard.vue @@ -25,6 +25,10 @@ const props = defineProps({ action: Boolean, // 动作标识 width: String, height: String, + sortable: { + type: Boolean, + default: false, + }, }) // 定义触发的自定义事件 @@ -420,6 +424,7 @@ watch( (newOpenState, _) => { if (newOpenState) openPluginDetail() }, + { immediate: true }, ) @@ -458,7 +463,7 @@ watch( {{ props.plugin?.plugin_desc }} -
+

diff --git a/src/components/cards/PluginMixedSortCard.vue b/src/components/cards/PluginMixedSortCard.vue index fc0739c0..a648bed0 100644 --- a/src/components/cards/PluginMixedSortCard.vue +++ b/src/components/cards/PluginMixedSortCard.vue @@ -14,12 +14,14 @@ interface Props { pluginStatistics?: { [key: string]: number } pluginActions?: { [key: string]: boolean } showRemoveButton?: boolean + sortable?: boolean } const props = withDefaults(defineProps(), { pluginStatistics: () => ({}), pluginActions: () => ({}), showRemoveButton: false, + sortable: false, }) const emit = defineEmits<{ @@ -36,7 +38,7 @@ const emit = defineEmits<{ // 拖拽事件处理 function handleDragOver(event: DragEvent) { // 只有当拖拽的是插件时才允许放入文件夹 - if (props.item.type === 'folder') { + if (props.sortable && props.item.type === 'folder') { event.preventDefault() event.stopPropagation() event.dataTransfer!.dropEffect = 'move' @@ -46,14 +48,14 @@ function handleDragOver(event: DragEvent) { } function handleDragEnter(event: DragEvent) { - if (props.item.type === 'folder') { + if (props.sortable && props.item.type === 'folder') { event.preventDefault() event.stopPropagation() } } function handleDragLeave(event: DragEvent) { - if (props.item.type === 'folder') { + if (props.sortable && props.item.type === 'folder') { event.preventDefault() event.stopPropagation() const target = event.currentTarget as HTMLElement @@ -62,7 +64,7 @@ function handleDragLeave(event: DragEvent) { } function handleDropToFolder(event: DragEvent) { - if (props.item.type === 'folder') { + if (props.sortable && props.item.type === 'folder') { event.preventDefault() event.stopPropagation() const target = event.currentTarget as HTMLElement @@ -89,6 +91,7 @@ function handleDropToFolder(event: DragEvent) { :folder-name="item.data.name" :plugin-count="item.data.pluginCount" :folder-config="item.data.config" + :sortable="sortable" @open="$emit('openFolder', item.id)" @delete="$emit('deleteFolder', item.id)" @rename="(oldName, newName) => $emit('renameFolder', oldName, newName)" @@ -102,6 +105,7 @@ function handleDropToFolder(event: DragEvent) { :count="pluginStatistics[item.id] || 0" :plugin="item.data" :action="pluginActions[item.id] || false" + :sortable="sortable" @remove="$emit('refreshData')" @save="$emit('refreshData')" @action-done="$emit('actionDone', item.id)" diff --git a/src/components/cards/SiteCard.vue b/src/components/cards/SiteCard.vue index 1b070716..f2d1ad27 100644 --- a/src/components/cards/SiteCard.vue +++ b/src/components/cards/SiteCard.vue @@ -12,6 +12,7 @@ import type { Site, SiteStatistic, SiteUserData } from '@/api/types' import { isNullOrEmptyObject } from '@/@core/utils' import { formatFileSize } from '@/@core/utils/formatters' import { useConfirm } from '@/composables/useConfirm' +import { getCachedSiteIcon } from '@/utils/siteIconCache' import { useDisplay } from 'vuetify' // 显示器宽度 @@ -25,6 +26,10 @@ const cardProps = defineProps({ site: Object as PropType, data: Object as PropType, stats: Object as PropType, + sortable: { + type: Boolean, + default: false, + }, }) // 定义触发的自定义事件 @@ -34,7 +39,8 @@ const emit = defineEmits(['update', 'remove', 'refresh-stats']) const createConfirm = useConfirm() // 图标 -const siteIcon = ref('') +const defaultSiteIcon = getLogoUrl('site') +const siteIcon = ref(defaultSiteIcon) // 提示框 const $toast = useToast() @@ -59,12 +65,20 @@ const siteUserDataDialog = ref(false) // 查询站点图标 async function getSiteIcon() { + const siteId = cardProps.site?.id + if (!siteId) { + siteIcon.value = defaultSiteIcon + return + } + try { - siteIcon.value = (await api.get(`site/icon/${cardProps.site?.id}`)).data.icon - if (!siteIcon.value) { - siteIcon.value = getLogoUrl('site') - } + siteIcon.value = await getCachedSiteIcon(siteId, async () => { + const response = await api.get(`site/icon/${siteId}`) + + return response?.data?.icon || defaultSiteIcon + }) } catch (error) { + siteIcon.value = defaultSiteIcon console.error(error) } } @@ -225,7 +239,7 @@ onMounted(() => { rounded="lg" size="32" class="shrink-0" - :class="{ 'cursor-move': display.mdAndUp.value }" + :class="{ 'cursor-move': cardProps.sortable && display.mdAndUp.value }" >