From 672bbb426585d4c040695648116c085ede5b7214 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 10 Apr 2026 16:48:29 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=B5=84=E6=BA=90=E6=B8=90?= =?UTF-8?q?=E8=BF=9B=E5=BC=8F=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/resource.vue | 409 ++++++++++++++++++++++++++++++----------- 1 file changed, 300 insertions(+), 109 deletions(-) diff --git a/src/pages/resource.vue b/src/pages/resource.vue index 22175b02..31c32b18 100644 --- a/src/pages/resource.vue +++ b/src/pages/resource.vue @@ -7,7 +7,6 @@ import TorrentCard from '@/components/cards/TorrentCard.vue' import TorrentItem from '@/components/cards/TorrentItem.vue' import TorrentFilterBar from '@/components/filter/TorrentFilterBar.vue' import { useI18n } from 'vue-i18n' -import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization' import { useGlobalSettingsStore } from '@/stores/global' import { useTorrentFilter, type FilterState } from '@/composables/useTorrentFilter' import { useInfiniteScroll } from '@/composables/useInfiniteScroll' @@ -15,7 +14,6 @@ import { useToast } from 'vue-toastification' // 国际化 const { t } = useI18n() -const { useProgressSSE } = useBackgroundOptimization() // 提示框 const toast = useToast() @@ -109,15 +107,18 @@ const progressEnabled = ref(false) // 进度是否激活 const progressActive = ref(false) +// 是否显示搜索进度 +const isSearchProgressVisible = computed( + () => progressActive.value || (!isRefreshed.value && (progressEnabled.value || progressValue.value > 0)), +) + // 是否显示搜索中的页面态 const isSearchLoading = computed( - () => !isRefreshed.value && (progressActive.value || progressEnabled.value || progressValue.value > 0), + () => !isRefreshed.value && isSearchProgressVisible.value && rawDataList.value.length === 0, ) // 归一化搜索进度,避免 SSE 异常值影响显示 -const searchProgressPercent = computed(() => - Math.min(100, Math.max(0, Math.ceil(Number(progressValue.value) || 0))), -) +const searchProgressPercent = computed(() => Math.min(100, Math.max(0, Math.ceil(Number(progressValue.value) || 0)))) // 搜索进度文案 const searchProgressLabel = computed(() => @@ -133,6 +134,16 @@ const errorTitle = ref(t('resource.noData')) // 错误描述 const errorDescription = ref(t('resource.noResourceFound')) +let searchEventSource: EventSource | null = null + +const streamPreviewLimit = 24 + +const streamTotalCount = ref(0) + +const displayResourceCount = computed(() => + progressActive.value ? streamTotalCount.value : torrentFilter.totalFilteredCount.value, +) + // 监听筛选条件变化,重新筛选数据 watch( [() => torrentFilter.filterForm, () => torrentFilter.sortField.value, () => torrentFilter.sortType.value], @@ -187,39 +198,19 @@ const watchProgressValue = watch( }, 60_000), ) -// 进度SSE消息处理函数 -function handleProgressMessage(event: MessageEvent) { - const progress = JSON.parse(event.data) - if (progress) { - progressText.value = progress.text - progressValue.value = progress.value - progressEnabled.value = progress.enable - } -} - -// 使用优化的进度SSE连接 -const progressSSE = useProgressSSE( - `${import.meta.env.VITE_API_BASE_URL}system/progress/search`, - handleProgressMessage, - 'resource-search-progress', - progressActive, -) - // 使用SSE监听加载进度 function startLoadingProgress() { watchProgressValue.resume() progressText.value = t('resource.searching') progressValue.value = 0 - progressEnabled.value = false + progressEnabled.value = true progressActive.value = true - progressSSE.start() } // 停止监听加载进度 function stopLoadingProgress() { watchProgressValue.pause() progressActive.value = false - progressSSE.stop() // 确保进度显示100%,然后再渐进清零 progressValue.value = 100 @@ -229,6 +220,203 @@ function stopLoadingProgress() { }, 1500) } +// 关闭SSE连接 +function closeSearchEventSource() { + if (searchEventSource) { + searchEventSource.close() + searchEventSource = null + } +} + +// 获取API URL +function getApiUrl(path: string) { + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL + const normalizedBaseUrl = apiBaseUrl.startsWith('http') + ? apiBaseUrl + : `${window.location.origin}${apiBaseUrl.startsWith('/') ? apiBaseUrl : `/${apiBaseUrl}`}` + + return new URL(path, normalizedBaseUrl.endsWith('/') ? normalizedBaseUrl : `${normalizedBaseUrl}/`) +} + +// 设置搜索参数 +function setSearchParam(params: URLSearchParams, key: string, value: unknown) { + if (value !== undefined && value !== null && value !== '') { + params.set(key, String(value)) + } +} + +// 构建搜索流URL +function buildSearchStreamUrl() { + const isMediaSearch = /^[a-zA-Z]+:/.test(keyword) + const url = getApiUrl(isMediaSearch ? `search/media/${encodeURIComponent(keyword)}/stream` : 'search/title/stream') + + if (isMediaSearch) { + setSearchParam(url.searchParams, 'mtype', type) + setSearchParam(url.searchParams, 'area', area) + setSearchParam(url.searchParams, 'title', title) + setSearchParam(url.searchParams, 'year', year) + setSearchParam(url.searchParams, 'season', season) + setSearchParam(url.searchParams, 'sites', sites) + } else { + setSearchParam(url.searchParams, 'keyword', keyword) + setSearchParam(url.searchParams, 'sites', sites) + } + + return url.toString() +} + +// 重置搜索结果 +function resetSearchResults() { + rawDataList.value = [] + originalDataList.value = [] + streamTotalCount.value = 0 + aiRecommended.value = false + showingAiResults.value = false + aiRecommendedList.value = [] + savedFilterState.value = null + aiStatusChecked.value = false + torrentFilter.clearAllFilters() + applyFilter() +} + +// 更新搜索进度 +function updateSearchProgress(eventData: { [key: string]: any }) { + if (eventData.text) { + progressText.value = eventData.text + } + if (typeof eventData.value === 'number') { + progressValue.value = eventData.value + } + if (typeof eventData.total_items === 'number') { + streamTotalCount.value = eventData.total_items + } + progressEnabled.value = true +} + +// 设置流式搜索结果 +function setStreamResults(items: Context[]) { + rawDataList.value = items + originalDataList.value = items + if (!progressActive.value) { + streamTotalCount.value = items.length + } + isRefreshed.value = true + applyFilter() +} + +// 追加流式搜索结果 +function appendStreamResults(items: Context[]) { + if (!items.length) return + + const nextItems = [...items, ...rawDataList.value] + setStreamResults(progressActive.value ? nextItems.slice(0, streamPreviewLimit) : nextItems) +} + +// 获取磁力链接的key +function getTorrentItemKey(item: Context, index: number) { + return ( + item.torrent_info?.page_url || + item.torrent_info?.enclosure || + `${item.torrent_info?.site_name || ''}-${item.torrent_info?.title || ''}-${item.torrent_info?.description || ''}` || + `torrent-${index}` + ) +} + +// 处理搜索流消息 +function handleSearchStreamMessage(eventData: { [key: string]: any }) { + updateSearchProgress(eventData) + + if (eventData.type === 'error') { + errorDescription.value = eventData.message || t('resource.noResourceFound') + return + } + + const items = Array.isArray(eventData.items) ? (eventData.items as Context[]) : [] + if (eventData.type === 'append') { + appendStreamResults(items) + } else if (eventData.type === 'replace' || eventData.type === 'done') { + setStreamResults(items) + } +} + +// 按请求搜索 +async function searchByRequest() { + let result: { [key: string]: any } + // 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索 + if (/^[a-zA-Z]+:/.test(keyword)) { + result = await api.get(`search/media/${keyword}`, { + params: { + mtype: type, + area, + title, + year, + season, + sites, + }, + }) + } else { + // 按标题模糊查询 + result = await api.get(`search/title`, { + params: { + keyword, + sites, + }, + }) + } + + if (result && result.success) { + streamTotalCount.value = result.data?.length || 0 + setStreamResults(result.data || []) + } else { + errorDescription.value = result?.message || t('resource.noResourceFound') + streamTotalCount.value = 0 + setStreamResults([]) + } +} + +// 按流搜索 +function searchByStream() { + return new Promise((resolve, reject) => { + closeSearchEventSource() + + let settled = false + const source = new EventSource(buildSearchStreamUrl()) + searchEventSource = source + + source.onmessage = event => { + try { + const eventData = JSON.parse(event.data) + handleSearchStreamMessage(eventData) + + if (eventData.type === 'error') { + settled = true + closeSearchEventSource() + resolve() + return + } + + if (eventData.type === 'done') { + settled = true + closeSearchEventSource() + resolve() + } + } catch (error) { + settled = true + closeSearchEventSource() + reject(error) + } + } + + source.onerror = () => { + if (settled) return + + settled = true + closeSearchEventSource() + reject(new Error(t('resource.noResourceFound'))) + } + }) +} + // 设置视图类型 function changeViewType(newType: string) { if (viewType.value !== newType) { @@ -251,38 +439,13 @@ async function fetchData() { rawDataList.value = (results as unknown as Context[]) || [] originalDataList.value = (results as unknown as Context[]) || [] } else { + resetSearchResults() startLoadingProgress() - let result: { [key: string]: any } - // 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索 - if (/^[a-zA-Z]+:/.test(keyword)) { - result = await api.get(`search/media/${keyword}`, { - params: { - mtype: type, - area, - title, - year, - season, - sites, - }, - }) - } else { - // 按标题模糊查询 - result = await api.get(`search/title`, { - params: { - keyword, - sites, - }, - }) - } - if (result && result.success) { - rawDataList.value = result.data || [] - originalDataList.value = result.data || [] - // 重置智能推荐状态 - aiRecommended.value = false - showingAiResults.value = false - aiRecommendedList.value = [] - } else if (result && result.message) { - errorDescription.value = result.message + try { + await searchByStream() + } catch (error) { + console.warn('渐进式搜索连接失败,回退到普通搜索:', error) + await searchByRequest() } stopLoadingProgress() // 从浏览器历史中删除当前搜索 @@ -294,6 +457,7 @@ async function fetchData() { isRefreshed.value = true } catch (error) { console.error(error) + closeSearchEventSource() stopLoadingProgress() isRefreshed.value = true return Promise.reject(error) @@ -560,7 +724,12 @@ const hasData = computed(() => { // 使用 watchEffect 确保计算属性变化时立即响应 watchEffect(() => { // 需要满足:AI 功能启用、数据已加载、尚未检查 - if (aiRecommendEnabled.value && originalDataList.value.length > 0 && !aiStatusChecked.value) { + if ( + aiRecommendEnabled.value && + originalDataList.value.length > 0 && + !progressActive.value && + !aiStatusChecked.value + ) { checkAiRecommendStatus() } }) @@ -572,6 +741,7 @@ onMounted(async () => { // 卸载时停止轮询 onUnmounted(() => { + closeSearchEventSource() stopLoadingProgress() stopAiRecommendPolling() }) @@ -581,7 +751,7 @@ onUnmounted(() => {
-
+
@@ -624,13 +794,13 @@ onUnmounted(() => {
-
+
- +
@@ -639,7 +809,7 @@ onUnmounted(() => { - +
{{ t('resource.searchResults') }} @@ -729,11 +899,12 @@ onUnmounted(() => {
{