From dab2b9d5bdb075eff6f24ac0d218a6189b286afb Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 20 Aug 2026 07:18:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(resource):=20=E4=BF=AE=E5=A4=8D=E8=A2=AB=20?= =?UTF-8?q?keep-alive=20=E7=BC=93=E5=AD=98=E7=9A=84=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E9=A1=B5=E5=9C=A8=E5=90=8E=E5=8F=B0=E8=A7=A6=E5=8F=91=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=B9=B6=E6=B8=85=E7=A9=BA=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 路由查询参数监听增加前台页面判断,避免详情页携带的媒体身份参数触发后台资源搜索 - 搜索完成后仅在仍停留本页时清理地址栏参数,防止误删其他页面的 query - 补充回归测试 --- src/pages/__tests__/resource.spec.ts | 68 ++++++++++++++++++++++++++++ src/pages/resource.vue | 8 +++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/pages/__tests__/resource.spec.ts b/src/pages/__tests__/resource.spec.ts index 8057d719..26d8d5e6 100644 --- a/src/pages/__tests__/resource.spec.ts +++ b/src/pages/__tests__/resource.spec.ts @@ -931,6 +931,74 @@ describe('resource page search flow', () => { await latestEventSource(4) }) + it('does not run a background search when another route updates the global query while the page is cached', async () => { + const rendered = await renderResource({ + path: '/resource', + query: { keyword: '前台搜索', result_type: 'torrent' }, + }) + const source = await latestEventSource() + finishStream(source, [createTorrent({ title: '前台结果' })]) + await screen.findByText('前台结果') + expect(EventSourceFake.instances).toHaveLength(1) + + // 模拟 keep-alive 缓存页在后台时,媒体详情路由携带与资源搜索一致的媒体身份参数 + await rendered.router.push({ + path: '/media', + query: { + media_source: 'themoviedb', + media_id: '42', + title: '媒体标题', + year: '2025', + type: '电视剧', + }, + }) + await flushAsyncWork() + + expect(EventSourceFake.instances).toHaveLength(1) + expect(mocks.apiGet).not.toHaveBeenCalledWith( + 'search/media/42', + expect.objectContaining({ params: expect.objectContaining({ media_source: 'themoviedb' }) }), + ) + }) + + it('keeps the query of other routes when a search finishes after navigating away', async () => { + const rendered = await renderResource({ + path: '/resource', + query: { keyword: '待完成搜索', result_type: 'torrent' }, + }) + await latestEventSource() + const replaceSpy = vi.spyOn(rendered.router, 'replace') + + // 搜索未完成时跳转到媒体详情页,模拟详情页路由携带媒体身份参数 + await rendered.router.push({ + path: '/media', + query: { + media_source: 'themoviedb', + media_id: '42', + title: '媒体标题', + year: '2025', + type: '电视剧', + }, + }) + await flushAsyncWork() + + // 完成此刻仍处于打开状态的全部搜索流(含修复前的隐藏搜索流), + // 确保任何残留搜索都不会清理其他页面的查询参数 + for (const source of EventSourceFake.instances) { + finishStream(source, [createTorrent({ title: '后台结果' })]) + } + await flushAsyncWork() + + expect(replaceSpy).not.toHaveBeenCalled() + expect(rendered.router.currentRoute.value.query).toEqual({ + media_source: 'themoviedb', + media_id: '42', + title: '媒体标题', + year: '2025', + type: '电视剧', + }) + }) + it('does not let a late business failure from an old fallback replace the current empty-state message', async () => { let resolveOldRequest!: (value: unknown) => void mocks.apiGet.mockReturnValueOnce( diff --git a/src/pages/resource.vue b/src/pages/resource.vue index 361dd8b4..3507dbc5 100644 --- a/src/pages/resource.vue +++ b/src/pages/resource.vue @@ -1060,6 +1060,9 @@ async function fetchData(options: { force?: boolean; params?: SearchParams; sile } else { resetSearchResults() startLoadingProgress() + // 记录发起搜索时的页面路径,搜索完成后只清理本页地址栏参数, + // 避免用户搜索期间跳转其他页面(如媒体详情)时误删其 query 参数 + const searchPath = route.path try { await searchByStream(currentSearchParams, requestToken) } catch (error) { @@ -1077,7 +1080,7 @@ async function fetchData(options: { force?: boolean; params?: SearchParams; sile if (requestId !== activeSearchRequestId) return stopLoadingProgress() // 搜索完成后移除地址栏参数,避免分享/刷新残留搜索条件 - if (Object.keys(route.query).length > 0) { + if (route.path === searchPath && Object.keys(route.query).length > 0) { await router.replace({ path: route.path, query: {} }) } } @@ -1395,6 +1398,9 @@ watchEffect(() => { watch( () => route.query, query => { + // 页面被 keep-alive 缓存后仍会响应全局路由变化;只有本页处于前台时才消费查询参数, + // 否则详情页等路由携带的媒体身份参数会触发后台资源搜索 + if (route.path !== '/resource') return if (Object.keys(query).length === 0) return const nextSearchParams = createSearchParams(query)