mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
fix(resource): 候选资源全部被过滤时显示友好提示而非 404 空态
搜索流追踪过滤前候选资源数(candidate_items 或搜索阶段 total_items),最终结果为空且存在候选时显示"找到 N 个资源,但均不符合过滤规则"。Closes #646
This commit is contained in:
@@ -1416,6 +1416,7 @@ export default {
|
|||||||
searching: 'Searching, please wait...',
|
searching: 'Searching, please wait...',
|
||||||
noData: 'No Data',
|
noData: 'No Data',
|
||||||
noResourceFound: 'No resources found',
|
noResourceFound: 'No resources found',
|
||||||
|
filteredNoResults: 'Found {count} resource(s), but none match the filter rules',
|
||||||
searchStreamTimeout: 'The search connection timed out. Please try again later.',
|
searchStreamTimeout: 'The search connection timed out. Please try again later.',
|
||||||
searchStreamDisconnected: 'The search connection was interrupted. Please try again later.',
|
searchStreamDisconnected: 'The search connection was interrupted. Please try again later.',
|
||||||
aiRecommend: 'AI Recommendation',
|
aiRecommend: 'AI Recommendation',
|
||||||
|
|||||||
@@ -1406,6 +1406,7 @@ export default {
|
|||||||
searching: '正在搜索,请稍候...',
|
searching: '正在搜索,请稍候...',
|
||||||
noData: '没有数据',
|
noData: '没有数据',
|
||||||
noResourceFound: '未搜索到任何资源',
|
noResourceFound: '未搜索到任何资源',
|
||||||
|
filteredNoResults: '找到 {count} 个资源,但均不符合过滤规则',
|
||||||
searchStreamTimeout: '搜索连接长时间无响应,请稍后重试',
|
searchStreamTimeout: '搜索连接长时间无响应,请稍后重试',
|
||||||
searchStreamDisconnected: '搜索连接已中断,请稍后重试',
|
searchStreamDisconnected: '搜索连接已中断,请稍后重试',
|
||||||
aiRecommend: '智能推荐',
|
aiRecommend: '智能推荐',
|
||||||
|
|||||||
@@ -1404,6 +1404,7 @@ export default {
|
|||||||
searching: '正在搜索,請稍候...',
|
searching: '正在搜索,請稍候...',
|
||||||
noData: '沒有數據',
|
noData: '沒有數據',
|
||||||
noResourceFound: '未搜索到任何資源',
|
noResourceFound: '未搜索到任何資源',
|
||||||
|
filteredNoResults: '找到 {count} 個資源,但均不符合過濾規則',
|
||||||
searchStreamTimeout: '搜索連線長時間無回應,請稍後重試',
|
searchStreamTimeout: '搜索連線長時間無回應,請稍後重試',
|
||||||
searchStreamDisconnected: '搜索連線已中斷,請稍後重試',
|
searchStreamDisconnected: '搜索連線已中斷,請稍後重試',
|
||||||
aiRecommend: '智能推薦',
|
aiRecommend: '智能推薦',
|
||||||
|
|||||||
@@ -549,6 +549,54 @@ describe('resource page search flow', () => {
|
|||||||
expect(screen.queryByText('搜索服务暂不可用')).not.toBeInTheDocument()
|
expect(screen.queryByText('搜索服务暂不可用')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows a friendly message when all candidate resources are filtered out', async () => {
|
||||||
|
await renderResource({
|
||||||
|
path: '/resource',
|
||||||
|
query: { keyword: '过滤空结果', result_type: 'torrent' },
|
||||||
|
})
|
||||||
|
const source = await latestEventSource()
|
||||||
|
|
||||||
|
// 搜索阶段返回候选资源,最终替换事件为空表示全部被过滤规则淘汰
|
||||||
|
source.message({
|
||||||
|
items: [createTorrent({ title: '候选资源' })],
|
||||||
|
stage: 'searching',
|
||||||
|
total_items: 19,
|
||||||
|
type: 'append',
|
||||||
|
})
|
||||||
|
source.message({
|
||||||
|
candidate_items: 19,
|
||||||
|
items: [],
|
||||||
|
stage: 'filtered',
|
||||||
|
total_items: 0,
|
||||||
|
type: 'replace',
|
||||||
|
value: 100,
|
||||||
|
})
|
||||||
|
source.message({ type: 'done' })
|
||||||
|
source.fail()
|
||||||
|
|
||||||
|
expect(await screen.findByTestId('no-data')).toHaveTextContent('找到 19 个资源,但均不符合过滤规则')
|
||||||
|
expect(screen.queryByText('未搜索到任何资源')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a friendly message when the done event reports filtered candidates for title search', async () => {
|
||||||
|
await renderResource({
|
||||||
|
path: '/resource',
|
||||||
|
query: { keyword: '标题过滤空结果', result_type: 'torrent' },
|
||||||
|
})
|
||||||
|
const source = await latestEventSource()
|
||||||
|
|
||||||
|
// 标题搜索没有 replace 事件,候选数由 done 事件的 candidate_items 携带
|
||||||
|
source.message({
|
||||||
|
candidate_items: 5,
|
||||||
|
items: [],
|
||||||
|
total_items: 0,
|
||||||
|
type: 'done',
|
||||||
|
})
|
||||||
|
source.fail()
|
||||||
|
|
||||||
|
expect(await screen.findByTestId('no-data')).toHaveTextContent('找到 5 个资源,但均不符合过滤规则')
|
||||||
|
})
|
||||||
|
|
||||||
it('restores the default empty-state message when the fallback request succeeds without results', async () => {
|
it('restores the default empty-state message when the fallback request succeeds without results', async () => {
|
||||||
mocks.apiGet.mockResolvedValueOnce({ data: [], success: true })
|
mocks.apiGet.mockResolvedValueOnce({ data: [], success: true })
|
||||||
const rendered = await renderResource({
|
const rendered = await renderResource({
|
||||||
|
|||||||
@@ -355,6 +355,9 @@ const displayResourceCount = computed(() =>
|
|||||||
// 搜索中只显示进度区域,避免结果抬头和进度条同时占用顶部空间。
|
// 搜索中只显示进度区域,避免结果抬头和进度条同时占用顶部空间。
|
||||||
const showResultHeader = computed(() => isRefreshed.value && !progressActive.value)
|
const showResultHeader = computed(() => isRefreshed.value && !progressActive.value)
|
||||||
|
|
||||||
|
// 记录过滤前的候选资源数,用于过滤后无结果时给出友好提示
|
||||||
|
let streamCandidateCount = 0
|
||||||
|
|
||||||
let pendingStreamItems: Array<Context> = []
|
let pendingStreamItems: Array<Context> = []
|
||||||
let pendingSubtitleStreamItems: Array<SubtitleInfo> = []
|
let pendingSubtitleStreamItems: Array<SubtitleInfo> = []
|
||||||
const streamReplaceBatchCollector = new SearchReplaceBatchCollector<Context>()
|
const streamReplaceBatchCollector = new SearchReplaceBatchCollector<Context>()
|
||||||
@@ -597,6 +600,7 @@ function resetSearchResults() {
|
|||||||
// 新搜索开始时先回到未完成态,避免上一轮空态在 SSE 返回前抢先显示。
|
// 新搜索开始时先回到未完成态,避免上一轮空态在 SSE 返回前抢先显示。
|
||||||
isRefreshed.value = false
|
isRefreshed.value = false
|
||||||
errorDescription.value = t('resource.noResourceFound')
|
errorDescription.value = t('resource.noResourceFound')
|
||||||
|
streamCandidateCount = 0
|
||||||
rawDataList.value = []
|
rawDataList.value = []
|
||||||
rawSubtitleDataList.value = []
|
rawSubtitleDataList.value = []
|
||||||
originalDataList.value = []
|
originalDataList.value = []
|
||||||
@@ -684,11 +688,29 @@ function appendSubtitleStreamResults(items: SubtitleInfo[]) {
|
|||||||
scheduleStreamFlush()
|
scheduleStreamFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新候选资源数:优先使用后端显式给出的 candidate_items,否则取搜索阶段事件的 total_items
|
||||||
|
function updateStreamCandidateCount(eventData: { candidate_items?: unknown; stage?: unknown; total_items?: unknown }) {
|
||||||
|
if (typeof eventData.candidate_items === 'number') {
|
||||||
|
streamCandidateCount = Math.max(streamCandidateCount, eventData.candidate_items)
|
||||||
|
} else if (eventData.stage === 'searching' && typeof eventData.total_items === 'number') {
|
||||||
|
streamCandidateCount = Math.max(streamCandidateCount, eventData.total_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤后无结果但存在候选资源时,用友好提示替代默认的“未搜索到任何资源”
|
||||||
|
function applyFilteredEmptyResultMessage(resultCount: number) {
|
||||||
|
if (resultCount === 0 && streamCandidateCount > 0) {
|
||||||
|
errorDescription.value = t('resource.filteredNoResults', { count: streamCandidateCount })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 完整最终结果到达后原子替换资源列表。
|
// 完整最终结果到达后原子替换资源列表。
|
||||||
function applyFinalStreamResults(items: Context[]) {
|
function applyFinalStreamResults(items: Context[]) {
|
||||||
streamFinalResultApplied = true
|
streamFinalResultApplied = true
|
||||||
flushBufferedStreamState()
|
flushBufferedStreamState()
|
||||||
setStreamResults(items)
|
setStreamResults(items)
|
||||||
|
// 候选全部被过滤规则淘汰时给出友好提示
|
||||||
|
applyFilteredEmptyResultMessage(items.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用最终字幕搜索结果
|
// 应用最终字幕搜索结果
|
||||||
@@ -734,6 +756,7 @@ function handleSearchStreamMessage(eventData: { [key: string]: any }) {
|
|||||||
updateSearchProgress(eventData, completedItems !== null)
|
updateSearchProgress(eventData, completedItems !== null)
|
||||||
if (completedItems) applyFinalSubtitleStreamResults(completedItems)
|
if (completedItems) applyFinalSubtitleStreamResults(completedItems)
|
||||||
} else {
|
} else {
|
||||||
|
updateStreamCandidateCount(eventData)
|
||||||
const completedItems = streamReplaceBatchCollector.append(eventData)
|
const completedItems = streamReplaceBatchCollector.append(eventData)
|
||||||
updateSearchProgress(eventData, completedItems !== null)
|
updateSearchProgress(eventData, completedItems !== null)
|
||||||
if (completedItems) applyFinalStreamResults(completedItems)
|
if (completedItems) applyFinalStreamResults(completedItems)
|
||||||
@@ -759,6 +782,7 @@ function handleSearchStreamMessage(eventData: { [key: string]: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = Array.isArray(eventData.items) ? (eventData.items as Context[]) : []
|
const items = Array.isArray(eventData.items) ? (eventData.items as Context[]) : []
|
||||||
|
updateStreamCandidateCount(eventData)
|
||||||
if (eventData.type === 'append') {
|
if (eventData.type === 'append') {
|
||||||
updateSearchProgress(eventData)
|
updateSearchProgress(eventData)
|
||||||
appendStreamResults(items)
|
appendStreamResults(items)
|
||||||
@@ -770,6 +794,10 @@ function handleSearchStreamMessage(eventData: { [key: string]: any }) {
|
|||||||
applyFinalStreamResults(items)
|
applyFinalStreamResults(items)
|
||||||
} else {
|
} else {
|
||||||
updateSearchProgress(eventData)
|
updateSearchProgress(eventData)
|
||||||
|
// 标题搜索没有 replace 事件,最终结果为空时在此给出友好提示
|
||||||
|
if (eventData.type === 'done' && !streamFinalResultApplied) {
|
||||||
|
applyFilteredEmptyResultMessage(items.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user