From a475085d7b7ad311ed5841cbef30eefd3fe5f881 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 9 May 2026 13:22:40 +0800 Subject: [PATCH] refactor: implement buffered streaming updates and disable keep-alive for resource --- src/pages/resource.vue | 153 +++++++++++++++++++++++++++++++++++------ src/router/index.ts | 1 - 2 files changed, 133 insertions(+), 21 deletions(-) diff --git a/src/pages/resource.vue b/src/pages/resource.vue index ae2bf851..825314e3 100644 --- a/src/pages/resource.vue +++ b/src/pages/resource.vue @@ -140,13 +140,23 @@ const errorDescription = ref(t('resource.noResourceFound')) let searchEventSource: EventSource | null = null const streamPreviewLimit = 24 +const streamUiFlushDelay = 1000 +const streamPreviewBufferLimit = streamPreviewLimit * 4 const streamTotalCount = ref(0) +const streamPreviewDataList = ref>([]) const displayResourceCount = computed(() => progressActive.value ? streamTotalCount.value : torrentFilter.totalFilteredCount.value, ) +let pendingStreamItems: Array = [] +let streamFlushTimer: ReturnType | null = null +let streamFinalResultApplied = false +let pendingProgressText: string | null = null +let pendingProgressValue: number | null = null +let pendingStreamTotalCount: number | null = null + // 监听筛选条件变化,重新筛选数据 watch( [() => torrentFilter.filterForm, () => torrentFilter.sortField.value, () => torrentFilter.sortType.value], @@ -231,6 +241,58 @@ function closeSearchEventSource() { } } +// 渐进式搜索期间只保留有限预览数据,避免每个批次都触发完整筛选和分组计算。 +function clearStreamFlushTimer() { + if (streamFlushTimer) { + clearTimeout(streamFlushTimer) + streamFlushTimer = null + } +} + +function clearStreamPreviewState(resetFinalState: boolean = false) { + clearStreamFlushTimer() + pendingStreamItems = [] + pendingProgressText = null + pendingProgressValue = null + pendingStreamTotalCount = null + streamPreviewDataList.value = [] + if (resetFinalState) { + streamFinalResultApplied = false + } +} + +// 将进度和预览列表放到同一个节奏刷新,避免 SSE 到来时多处 UI 各自抖动。 +function flushBufferedStreamState() { + clearStreamFlushTimer() + + if (pendingProgressText !== null) { + progressText.value = pendingProgressText + } + if (pendingProgressValue !== null) { + progressValue.value = pendingProgressValue + } + if (pendingStreamTotalCount !== null) { + streamTotalCount.value = pendingStreamTotalCount + } + + pendingProgressText = null + pendingProgressValue = null + pendingStreamTotalCount = null + + if (!pendingStreamItems.length) return + + streamPreviewDataList.value = [...pendingStreamItems, ...streamPreviewDataList.value].slice(0, streamPreviewLimit) + pendingStreamItems = [] + isRefreshed.value = true +} + +function scheduleStreamFlush() { + if (streamFlushTimer) return + streamFlushTimer = setTimeout(() => { + flushBufferedStreamState() + }, streamUiFlushDelay) +} + // 获取API URL function getApiUrl(path: string) { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL @@ -270,6 +332,7 @@ function buildSearchStreamUrl() { // 重置搜索结果 function resetSearchResults() { + clearStreamPreviewState(true) rawDataList.value = [] originalDataList.value = [] streamTotalCount.value = 0 @@ -283,21 +346,28 @@ function resetSearchResults() { } // 更新搜索进度 -function updateSearchProgress(eventData: { [key: string]: any }) { +function updateSearchProgress(eventData: { [key: string]: any }, flushNow: boolean = false) { if (eventData.text) { - progressText.value = eventData.text + pendingProgressText = eventData.text } if (typeof eventData.value === 'number') { - progressValue.value = eventData.value + pendingProgressValue = eventData.value } if (typeof eventData.total_items === 'number') { - streamTotalCount.value = eventData.total_items + pendingStreamTotalCount = eventData.total_items } progressEnabled.value = true + + if (flushNow) { + flushBufferedStreamState() + } else { + scheduleStreamFlush() + } } // 设置流式搜索结果 function setStreamResults(items: Context[]) { + clearStreamPreviewState() rawDataList.value = items originalDataList.value = items if (!progressActive.value) { @@ -307,12 +377,21 @@ function setStreamResults(items: Context[]) { applyFilter() } -// 追加流式搜索结果 +// 追加流式搜索预览结果 function appendStreamResults(items: Context[]) { if (!items.length) return - const nextItems = [...items, ...rawDataList.value] - setStreamResults(progressActive.value ? nextItems.slice(0, streamPreviewLimit) : nextItems) + pendingStreamItems.unshift(...items) + if (pendingStreamItems.length > streamPreviewBufferLimit) { + pendingStreamItems = pendingStreamItems.slice(0, streamPreviewBufferLimit) + } + scheduleStreamFlush() +} + +function applyFinalStreamResults(items: Context[]) { + streamFinalResultApplied = true + flushBufferedStreamState() + setStreamResults(items) } // 获取磁力链接的key @@ -327,18 +406,24 @@ function getTorrentItemKey(item: Context, index: number) { // 处理搜索流消息 function handleSearchStreamMessage(eventData: { [key: string]: any }) { - updateSearchProgress(eventData) - if (eventData.type === 'error') { + updateSearchProgress(eventData, true) errorDescription.value = eventData.message || t('resource.noResourceFound') return } const items = Array.isArray(eventData.items) ? (eventData.items as Context[]) : [] if (eventData.type === 'append') { + updateSearchProgress(eventData) appendStreamResults(items) - } else if (eventData.type === 'replace' || eventData.type === 'done') { - setStreamResults(items) + } else if (eventData.type === 'replace') { + updateSearchProgress(eventData, true) + applyFinalStreamResults(items) + } else if (eventData.type === 'done' && items.length > 0 && !streamFinalResultApplied) { + updateSearchProgress(eventData, true) + applyFinalStreamResults(items) + } else { + updateSearchProgress(eventData) } } @@ -439,8 +524,7 @@ async function fetchData() { if (!keyword) { // 查询上次搜索结果 const results = await api.get('search/last') - rawDataList.value = (results as unknown as Context[]) || [] - originalDataList.value = (results as unknown as Context[]) || [] + setStreamResults((results as unknown as Context[]) || []) } else { resetSearchResults() startLoadingProgress() @@ -454,8 +538,6 @@ async function fetchData() { // 从浏览器历史中删除当前搜索 window.history.replaceState(null, '', window.location.pathname) } - // 应用筛选 - applyFilter() // 标记已刷新 isRefreshed.value = true } catch (error) { @@ -731,6 +813,10 @@ async function checkAiRecommendStatus() { // 计算当前显示的数据是否有数据 const hasData = computed(() => { + if (progressActive.value) { + return streamPreviewDataList.value.length > 0 || rawDataList.value.length > 0 + } + if (viewType.value === 'row') { return filteredRowDataList.value.length > 0 || rawDataList.value.length > 0 } else { @@ -762,6 +848,7 @@ onUnmounted(() => { closeSearchEventSource() stopLoadingProgress() stopAiRecommendPolling() + clearStreamPreviewState() }) @@ -769,7 +856,11 @@ onUnmounted(() => {
-
+
@@ -955,8 +1046,20 @@ onUnmounted(() => {
+
+ +
{
-
+
{{ t('torrent.noResults') }}
@@ -986,10 +1089,20 @@ onUnmounted(() => {
-
+
{{ t('torrent.noResults') }}
+
+
+ + +
+
{ /* 重新搜索按钮 */ .refresh-search-btn { - block-size: 44px !important; - inline-size: 44px !important; border-radius: 8px !important; background-color: rgba(var(--v-theme-surface-variant), 0.1); + block-size: 44px !important; + inline-size: 44px !important; } /* AI按钮组样式 */ diff --git a/src/router/index.ts b/src/router/index.ts index bde712cd..1499ddbc 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -48,7 +48,6 @@ const router = createRouter({ path: '/resource', component: () => import('../pages/resource.vue'), meta: { - keepAlive: true, requiresAuth: true, }, },