diff --git a/src/composables/__tests__/useTorrentFilter.spec.ts b/src/composables/__tests__/useTorrentFilter.spec.ts index 36f0167a..91fd7043 100644 --- a/src/composables/__tests__/useTorrentFilter.spec.ts +++ b/src/composables/__tests__/useTorrentFilter.spec.ts @@ -83,6 +83,27 @@ describe('useTorrentFilter', () => { expect(filter.totalFilteredCount.value).toBe(2) }) + it('ignores malformed restored contexts and preserves valid original indices', () => { + const filter = useTorrentFilter() + const malformedWithoutTorrent = { + meta_info: createTorrent().meta_info, + title: '被错误响应模型展平的缓存资源', + } as unknown as Context + const malformedWithoutMeta = { + torrent_info: createTorrent({ title: '缺少元数据的缓存资源' }).torrent_info, + } as unknown as Context + const valid = createTorrent({ title: '可恢复资源' }) + const contexts = [malformedWithoutTorrent, malformedWithoutMeta, valid] + + expect(filter.filterRowData(contexts).map(item => item.torrent_info.title)).toEqual(['可恢复资源']) + expect(filter.getFilteredIndices()).toEqual([2]) + expect(filter.totalFilteredCount.value).toBe(1) + + expect(filter.filterCardData(contexts).map(item => item.torrent_info.title)).toEqual(['可恢复资源']) + expect(filter.getFilteredIndices()).toEqual([2]) + expect(filter.totalFilteredCount.value).toBe(1) + }) + it.each([ ['default', ['默认小', '默认大']], ['site', ['站点 A', '站点 B']], diff --git a/src/composables/useTorrentFilter.ts b/src/composables/useTorrentFilter.ts index 79141a94..78b61b3f 100644 --- a/src/composables/useTorrentFilter.ts +++ b/src/composables/useTorrentFilter.ts @@ -167,6 +167,11 @@ export function useTorrentFilter() { const match = (filter: Array, value: string | undefined) => filter.length === 0 || (value && filter.includes(value)) + // 搜索结果必须同时包含可展示的元数据和种子信息。 + function isRenderableContext(data: Context | null | undefined): data is Context { + return Boolean(data?.meta_info && data?.torrent_info) + } + // 筛选列表视图数据(不分组) function filterRowData(items: Context[] | undefined): Context[] { // 重置状态 @@ -184,13 +189,17 @@ export function useTorrentFilter() { // 首先收集所有过滤选项 items.forEach(data => { - initOptions(data) + if (isRenderableContext(data)) { + initOptions(data) + } }) // 筛选数据 let filteredData: Context[] = [] items.forEach((data, index) => { + if (!isRenderableContext(data)) return + const { meta_info, torrent_info } = data if ( match(filterForm.site, torrent_info.site_name) && @@ -238,6 +247,8 @@ export function useTorrentFilter() { const groupMap = new Map() items.forEach((item, index) => { + if (!isRenderableContext(item)) return + const { torrent_info, meta_info } = item // init options initOptions(item) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 2c64ba4d..eab4f002 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -1559,7 +1559,7 @@ export default { emptySearchHint: 'Search again, or adjust the keyword and site scope before retrying.', emptyStartHint: 'Return home to start a new search.', filteredEmptyTitle: 'No resources match the current filters', - filteredEmptyHint: '{count} resource(s) were found. Clear the filters to view them.', + filteredEmptyHint: 'Clear the filters to view all search results.', searchStreamTimeout: 'The search connection timed out. Please try again later.', searchStreamDisconnected: 'The search connection was interrupted. Please try again later.', aiRecommend: 'AI Recommendation', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c031675d..c68509e9 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1538,7 +1538,7 @@ export default { emptySearchHint: '可以重新搜索,或调整关键词和站点范围后再试。', emptyStartHint: '返回首页后可通过搜索入口发起新的搜索。', filteredEmptyTitle: '当前筛选条件没有匹配项', - filteredEmptyHint: '已获取 {count} 个资源,清除筛选后即可查看。', + filteredEmptyHint: '清除筛选后可查看全部搜索结果。', searchStreamTimeout: '搜索连接长时间无响应,请稍后重试', searchStreamDisconnected: '搜索连接已中断,请稍后重试', aiRecommend: '智能推荐', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 8722a8e7..d046c6b7 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1510,7 +1510,7 @@ export default { emptySearchHint: '可以重新搜尋,或調整關鍵詞和站點範圍後再試。', emptyStartHint: '返回首頁後可通過搜索入口發起新的搜索。', filteredEmptyTitle: '當前篩選條件沒有匹配項', - filteredEmptyHint: '已獲取 {count} 個資源,清除篩選後即可查看。', + filteredEmptyHint: '清除篩選後可查看全部搜索結果。', searchStreamTimeout: '搜索連線長時間無回應,請稍後重試', searchStreamDisconnected: '搜索連線已中斷,請稍後重試', aiRecommend: '智能推薦', diff --git a/src/pages/__tests__/resource.spec.ts b/src/pages/__tests__/resource.spec.ts index ade0a80e..fffe975f 100644 --- a/src/pages/__tests__/resource.spec.ts +++ b/src/pages/__tests__/resource.spec.ts @@ -523,6 +523,47 @@ describe('resource page search flow', () => { }) }) + it('ignores malformed cached entries while restoring valid previous results', async () => { + const malformed = { + meta_info: createTorrent().meta_info, + title: '被错误响应模型展平的缓存资源', + } as unknown as Context + mocks.apiGet.mockResolvedValueOnce({ + success: true, + data: { + params: { keyword: '上次关键词', result_type: 'torrent' }, + results: [malformed, createTorrent({ title: '重启后恢复的资源' })], + }, + }) + + await renderResource() + + expect(await screen.findByText('重启后恢复的资源')).toBeInTheDocument() + expect(screen.getByTestId('torrent-filter-bar')).toBeInTheDocument() + expect(screen.queryByText('被错误响应模型展平的缓存资源')).not.toBeInTheDocument() + }) + + it('shows a clean empty state when cached results contain no renderable resources', async () => { + const malformed = { + meta_info: createTorrent().meta_info, + title: '被错误响应模型展平的缓存资源', + } as unknown as Context + mocks.apiGet.mockResolvedValueOnce({ + success: true, + data: { + params: { keyword: '上次关键词', result_type: 'torrent' }, + results: Array.from({ length: 74 }, () => malformed), + }, + }) + + await renderResource() + + const emptyState = await screen.findByRole('status', { name: '没有找到匹配的资源' }) + expect(emptyState).toHaveTextContent('可以重新搜索,或调整关键词和站点范围后再试。') + expect(emptyState).not.toHaveTextContent('74') + expect(screen.queryByTestId('torrent-filter-bar')).not.toBeInTheDocument() + }) + it('migrates a legacy composite media keyword only when restoring local search state', async () => { localStorage.setItem( 'MP_ResourceSearchParams', @@ -588,6 +629,7 @@ describe('resource page search flow', () => { const emptyState = await screen.findByRole('status', { name: '没有找到匹配的资源' }) expect(emptyState).toHaveTextContent('可以重新搜索,或调整关键词和站点范围后再试。') expect(emptyState).toHaveTextContent('冷门资源') + expect(screen.queryByTestId('torrent-filter-bar')).not.toBeInTheDocument() await fireEvent.click(within(emptyState).getByRole('button', { name: '重新搜索' })) @@ -608,12 +650,15 @@ describe('resource page search flow', () => { await fireEvent.click(screen.getByRole('button', { name: '筛选 Site A' })) const emptyState = await screen.findByRole('status', { name: '当前筛选条件没有匹配项' }) - expect(emptyState).toHaveTextContent('已获取 1 个资源,清除筛选后即可查看。') + expect(emptyState).toHaveTextContent('清除筛选后可查看全部搜索结果。') + expect(emptyState).not.toHaveTextContent('1 个资源') + expect(screen.queryByTestId('torrent-filter-bar')).not.toBeInTheDocument() expect(screen.queryByText('筛选后可恢复资源')).not.toBeInTheDocument() await fireEvent.click(within(emptyState).getByRole('button', { name: '清除筛选' })) expect(await screen.findByText('筛选后可恢复资源')).toBeInTheDocument() + expect(screen.getByTestId('torrent-filter-bar')).toBeInTheDocument() expect(screen.queryByRole('status', { name: '当前筛选条件没有匹配项' })).not.toBeInTheDocument() }) diff --git a/src/pages/resource.vue b/src/pages/resource.vue index 779f27c0..afda60b0 100644 --- a/src/pages/resource.vue +++ b/src/pages/resource.vue @@ -1389,30 +1389,18 @@ const hasData = computed(() => { } if (viewType.value === 'row') { - return filteredRowDataList.value.length > 0 || rawDataList.value.length > 0 + return filteredRowDataList.value.length > 0 } else { - return filteredCardDataList.value.length > 0 || rawDataList.value.length > 0 + return filteredCardDataList.value.length > 0 } }) -// 区分原始搜索有数据但当前视图被筛选为空的状态。 -const hasVisibleData = computed(() => { - if (isSubtitleSearch.value) { - return progressActive.value - ? streamPreviewSubtitleDataList.value.length > 0 || rawSubtitleDataList.value.length > 0 - : rawSubtitleDataList.value.length > 0 - } - - if (progressActive.value) { - return streamPreviewDataList.value.length > 0 || rawDataList.value.length > 0 - } - - return viewType.value === 'row' ? filteredRowDataList.value.length > 0 : filteredCardDataList.value.length > 0 -}) - // 空态保留用户可识别的搜索词,不展示内部媒体来源 ID。 const emptySearchQuery = computed(() => title.value.trim() || keyword.value.trim()) +// 只有用户实际选择了筛选值时,空态才提供清除筛选操作。 +const hasActiveTorrentFilters = computed(() => !isSubtitleSearch.value && torrentFilter.hasActiveFilters()) + // 只有存在可重放参数时才提供重新搜索操作。 const hasRefreshableSearch = computed( () => @@ -1428,6 +1416,14 @@ const searchEmptyDescription = computed(() => hasRefreshableSearch.value ? t('resource.emptySearchHint') : t('resource.emptyStartHint'), ) +const resultEmptyTitle = computed(() => + hasActiveTorrentFilters.value ? t('resource.filteredEmptyTitle') : searchEmptyTitle.value, +) + +const resultEmptyDescription = computed(() => + hasActiveTorrentFilters.value ? t('resource.filteredEmptyHint') : searchEmptyDescription.value, +) + // 监听 AI_RECOMMEND_ENABLED 状态和数据加载状态 // 使用 watchEffect 确保计算属性变化时立即响应 watchEffect(() => { @@ -1645,7 +1641,7 @@ onUnmounted(() => { -
+
{
- - - -
@@ -1793,13 +1773,23 @@ onUnmounted(() => {