fix(resource): 修复被 keep-alive 缓存的资源页在后台触发搜索并清空详情页参数的问题

- 路由查询参数监听增加前台页面判断,避免详情页携带的媒体身份参数触发后台资源搜索
- 搜索完成后仅在仍停留本页时清理地址栏参数,防止误删其他页面的 query
- 补充回归测试
This commit is contained in:
jxxghp
2026-08-20 07:18:44 +08:00
parent 38139d95eb
commit dab2b9d5bd
2 changed files with 75 additions and 1 deletions
+68
View File
@@ -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(
+7 -1
View File
@@ -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)