From 7114c63e8f75b65a911878f8c0be828a0b22ac8f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 15 May 2026 23:08:56 +0800 Subject: [PATCH] Revert "perf: optimize infinite list loading" This reverts commit 2a6f9e3cc05aa055ffafc0ec88f4323de6363286. --- src/@core/utils/image.ts | 53 +-------- .../dialog/SubscribeHistoryDialog.vue | 12 +- src/composables/usePaginatedInfiniteScroll.ts | 87 -------------- src/views/discover/MediaCardListView.vue | 100 +++++++++++----- src/views/discover/PersonCardListView.vue | 85 +++++++++++--- src/views/plugin/PluginCardListView.vue | 8 +- src/views/subscribe/SubscribePopularView.vue | 110 +++++++++++------- src/views/subscribe/SubscribeShareView.vue | 94 ++++++++++----- src/views/system/MessageView.vue | 11 +- src/views/workflow/WorkflowShareView.vue | 94 ++++++++++----- 10 files changed, 358 insertions(+), 296 deletions(-) delete mode 100644 src/composables/usePaginatedInfiniteScroll.ts diff --git a/src/@core/utils/image.ts b/src/@core/utils/image.ts index 8ecaf7d0..546c0d92 100644 --- a/src/@core/utils/image.ts +++ b/src/@core/utils/image.ts @@ -1,15 +1,5 @@ import ColorThief from 'colorthief' -const DEFAULT_DOMINANT_COLOR = '#28A9E1' -const DOMINANT_COLOR_CACHE_LIMIT = 100 -const colorThief = new ColorThief() -const dominantColorCache = new Map>() - -interface DominantColorOptions { - fallback?: string - quality?: number -} - // 将 RGB 转换为十六进制 function rgbStringToHex(rgbArray: number[]): string { if (rgbArray.length !== 3 || rgbArray.some(isNaN)) throw new Error('Invalid RGB string format') @@ -24,46 +14,11 @@ function rgbStringToHex(rgbArray: number[]): string { return `#${toHex(r)}${toHex(g)}${toHex(b)}` } -function getImageCacheKey(image: HTMLImageElement) { - return image.currentSrc || image.src || '' -} - -function rememberDominantColor(key: string, colorPromise: Promise) { - if (!key) return colorPromise - - if (dominantColorCache.size >= DOMINANT_COLOR_CACHE_LIMIT) { - const firstKey = dominantColorCache.keys().next().value - if (firstKey) dominantColorCache.delete(firstKey) - } - - dominantColorCache.set(key, colorPromise) - return colorPromise -} - // 提取主要颜色 -export async function getDominantColor( - image: HTMLImageElement | undefined | null, - options: DominantColorOptions = {}, -): Promise { - const fallback = options.fallback ?? DEFAULT_DOMINANT_COLOR - - if (!image) return fallback - - const cacheKey = getImageCacheKey(image) - const cachedColor = cacheKey ? dominantColorCache.get(cacheKey) : undefined - if (cachedColor) return cachedColor - - const colorPromise = Promise.resolve() - .then(() => { - const dominantColor = colorThief.getColor(image, options.quality ?? 20) - return rgbStringToHex(dominantColor) - }) - .catch(error => { - console.warn('Failed to extract dominant color:', error) - return fallback - }) - - return rememberDominantColor(cacheKey, colorPromise) +export async function getDominantColor(image: HTMLImageElement): Promise { + const colorThief = new ColorThief() + const dominantColor = colorThief.getColor(image) + return rgbStringToHex(dominantColor) } // 预加载图片 diff --git a/src/components/dialog/SubscribeHistoryDialog.vue b/src/components/dialog/SubscribeHistoryDialog.vue index 20fa8dd7..86b1ce17 100644 --- a/src/components/dialog/SubscribeHistoryDialog.vue +++ b/src/components/dialog/SubscribeHistoryDialog.vue @@ -6,7 +6,6 @@ import { useDisplay } from 'vuetify' import ProgressDialog from './ProgressDialog.vue' import { useI18n } from 'vue-i18n' import { mediaTypeDict } from '@/api/constants' -import type { InfiniteScrollDone } from '@/composables/usePaginatedInfiniteScroll' // 国际化 const { t } = useI18n() @@ -25,6 +24,9 @@ const emit = defineEmits(['close', 'save']) // 订阅历史列表 const historyList = ref([]) +// 当前加载数据 +const currData = ref([]) + // 当前页 const currentPage = ref(1) @@ -44,7 +46,7 @@ const progressDialog = ref(false) const progressText = ref('') // 调用API查询列表 -async function loadHistory({ done }: { done: InfiniteScrollDone }) { +async function loadHistory({ done }: { done: any }) { // 如果正在加载中,直接返回 if (loading.value) { done('ok') @@ -55,7 +57,7 @@ async function loadHistory({ done }: { done: InfiniteScrollDone }) { try { // 设置加载中 loading.value = true - const currentData: Subscribe[] = await api.get(`subscribe/history/${props.type}`, { + currData.value = await api.get(`subscribe/history/${props.type}`, { params: { page: currentPage.value, count: pageSize.value, @@ -63,12 +65,12 @@ async function loadHistory({ done }: { done: InfiniteScrollDone }) { }) // 标计为已请求完成 isRefreshed.value = true - if (currentData.length === 0) { + if (currData.value.length === 0) { // 如果没有数据,跳出 done('empty') } else { // 合并数据 - historyList.value.push(...currentData) + historyList.value = [...historyList.value, ...currData.value] // 页码+1 currentPage.value++ // 返回加载成功 diff --git a/src/composables/usePaginatedInfiniteScroll.ts b/src/composables/usePaginatedInfiniteScroll.ts deleted file mode 100644 index 82795af1..00000000 --- a/src/composables/usePaginatedInfiniteScroll.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { Ref } from 'vue' -import { nextTick } from 'vue' - -export type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error' -export type InfiniteScrollDone = (status: InfiniteScrollStatus) => void - -interface InfiniteScrollPage { - isLastPage?: boolean - items: T[] -} - -interface LoadPaginatedInfiniteScrollOptions { - advancePage: () => void - appendItems: (items: T[]) => void - done: InfiniteScrollDone - hasScroll?: () => boolean - loadPage: () => Promise> - loading: Ref - markLoaded?: () => void - maxAutoLoadPages?: number -} - -const DEFAULT_MAX_AUTO_LOAD_PAGES = 6 - -export function hasDocumentScroll() { - return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2 -} - -function normalizePageResult(result: T[] | InfiniteScrollPage): InfiniteScrollPage { - if (Array.isArray(result)) { - return { - isLastPage: result.length === 0, - items: result, - } - } - - return result -} - -export async function loadPaginatedInfiniteScroll({ - advancePage, - appendItems, - done, - hasScroll = hasDocumentScroll, - loadPage, - loading, - markLoaded, - maxAutoLoadPages = DEFAULT_MAX_AUTO_LOAD_PAGES, -}: LoadPaginatedInfiniteScrollOptions) { - if (loading.value) { - done('ok') - return - } - - loading.value = true - - let status: InfiniteScrollStatus = 'ok' - let loadedPages = 0 - - try { - do { - const { isLastPage, items } = normalizePageResult(await loadPage()) - - markLoaded?.() - - if (isLastPage) { - status = 'empty' - break - } - - if (items.length > 0) { - appendItems(items) - } - - advancePage() - loadedPages += 1 - - await nextTick() - } while (!hasScroll() && loadedPages < maxAutoLoadPages) - } catch (error) { - console.error(error) - status = 'error' - } finally { - loading.value = false - done(status) - } -} diff --git a/src/views/discover/MediaCardListView.vue b/src/views/discover/MediaCardListView.vue index e40525e8..ae3f7ca8 100644 --- a/src/views/discover/MediaCardListView.vue +++ b/src/views/discover/MediaCardListView.vue @@ -4,7 +4,6 @@ import type { MediaInfo } from '@/api/types' import MediaCard from '@/components/cards/MediaCard.vue' import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue' import NoDataFound from '@/components/NoDataFound.vue' -import { loadPaginatedInfiniteScroll, type InfiniteScrollDone } from '@/composables/usePaginatedInfiniteScroll' import { useI18n } from 'vue-i18n' const { t } = useI18n() @@ -15,6 +14,11 @@ const props = defineProps({ params: Object as PropType<{ [key: string]: any }>, }) +// 判断是否有滚动条 +function hasScroll() { + return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2 +} + // 当前页码 const page = ref(1) @@ -56,7 +60,7 @@ const dedupFields = [ function deduplicate(items: MediaInfo[]): MediaInfo[] { return items.filter(item => { - const key = getMediaDedupKey(item) + const key = dedupFields.map(field => String(item[field])).join('~') if (seenKeys.has(key)) { return false } @@ -66,16 +70,7 @@ function deduplicate(items: MediaInfo[]): MediaInfo[] { } function appendData(items: MediaInfo[]) { - dataList.value.push(...items) - triggerRef(dataList) -} - -function getMediaDedupKey(item: MediaInfo) { - return dedupFields.map(field => String(item[field] ?? '')).join('~') -} - -function getMediaItemKey(item: MediaInfo) { - return [getMediaDedupKey(item), item.title ?? ''].join('~') + dataList.value = dataList.value.concat(items) } async function loadPageData() { @@ -84,30 +79,73 @@ async function loadPageData() { }) return { - isLastPage: rawData.length === 0, - items: deduplicate(rawData), + rawCount: rawData.length, + uniqueData: deduplicate(rawData), } } // 获取列表数据 -async function fetchData({ done }: { done: InfiniteScrollDone }) { - if (!props.apipath) { - done('empty') - return - } +async function fetchData({ done }: { done: any }) { + try { + if (!props.apipath) return - await loadPaginatedInfiniteScroll({ - advancePage: () => { - page.value++ - }, - appendItems: appendData, - done, - loadPage: loadPageData, - loading, - markLoaded: () => { + // 如果正在加载中,直接返回 + if (loading.value) { + done('ok') + return + } + + // 加载到满屏或者加载出错 + if (!hasScroll()) { + // 加载多次 + while (!hasScroll()) { + // 设置加载中 + loading.value = true + // 请求API + const { rawCount, uniqueData } = await loadPageData() + // 取消加载中 + loading.value = false + // 标计为已请求完成 + isRefreshed.value = true + if (rawCount === 0) { + // 如果没有数据,跳出 + done('empty') + return + } + // 合并数据 + appendData(uniqueData) + // 页码+1 + page.value++ + // 返回加载成功 + done('ok') + } + } else { + // 加载一次 + // 设置加载中 + loading.value = true + // 请求API + const { rawCount, uniqueData } = await loadPageData() + // 标计为已请求完成 isRefreshed.value = true - }, - }) + if (rawCount === 0) { + // 如果没有数据,跳出 + done('empty') + } else { + // 合并数据 + appendData(uniqueData) + // 页码+1 + page.value++ + // 返回加载成功 + done('ok') + } + } + // 取消加载中 + loading.value = false + } catch (error) { + console.error(error) + // 返回加载失败 + done('error') + } } @@ -120,7 +158,7 @@ async function fetchData({ done }: { done: InfiniteScrollDone }) { v-if="dataList.length > 0" :items="dataList" :item-aspect-ratio="1.5" - :get-item-key="getMediaItemKey" + :get-item-key="item => item.tmdb_id || item.douban_id || item.bangumi_id || item.media_id || item.title" tabindex="0" >