mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
refactor: implement search parameter state management and prevent API caching for search requests
This commit is contained in:
+103
-42
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
|
import type { LocationQuery } from 'vue-router'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
@@ -26,27 +27,60 @@ const torrentFilter = useTorrentFilter()
|
|||||||
|
|
||||||
// 路由参数
|
// 路由参数
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
interface SearchParams {
|
||||||
|
keyword: string
|
||||||
|
type: string
|
||||||
|
area: string
|
||||||
|
title: string
|
||||||
|
year: string
|
||||||
|
season: string
|
||||||
|
sites: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSearchParams(query: LocationQuery): SearchParams {
|
||||||
|
return {
|
||||||
|
keyword: query?.keyword?.toString() ?? '',
|
||||||
|
type: query?.type?.toString() ?? '',
|
||||||
|
area: query?.area?.toString() ?? '',
|
||||||
|
title: query?.title?.toString() ?? '',
|
||||||
|
year: query?.year?.toString() ?? '',
|
||||||
|
season: query?.season?.toString() ?? '',
|
||||||
|
sites: query?.sites?.toString() ?? '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchParamsKey(params: SearchParams): string {
|
||||||
|
return JSON.stringify(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSearchRequestToken(): string {
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSearchParams = ref<SearchParams>(createSearchParams(route.query))
|
||||||
|
|
||||||
// 查询TMDBID或标题
|
// 查询TMDBID或标题
|
||||||
const keyword = route.query?.keyword?.toString() ?? ''
|
const keyword = computed(() => activeSearchParams.value.keyword)
|
||||||
|
|
||||||
// 查询类型
|
// 查询类型
|
||||||
const type = route.query?.type?.toString() ?? ''
|
const type = computed(() => activeSearchParams.value.type)
|
||||||
|
|
||||||
// 搜索字段
|
// 搜索字段
|
||||||
const area = route.query?.area?.toString() ?? ''
|
const area = computed(() => activeSearchParams.value.area)
|
||||||
|
|
||||||
// 搜索标题
|
// 搜索标题
|
||||||
const title = route.query?.title?.toString() ?? ''
|
const title = computed(() => activeSearchParams.value.title)
|
||||||
|
|
||||||
// 搜索年份
|
// 搜索年份
|
||||||
const year = route.query?.year
|
const year = computed(() => activeSearchParams.value.year)
|
||||||
|
|
||||||
// 搜索季
|
// 搜索季
|
||||||
const season = route.query?.season?.toString() ?? ''
|
const season = computed(() => activeSearchParams.value.season)
|
||||||
|
|
||||||
// 搜索站点,以,分离多个
|
// 搜索站点,以,分离多个
|
||||||
const sites = route.query?.sites?.toString() ?? ''
|
const sites = computed(() => activeSearchParams.value.sites)
|
||||||
|
|
||||||
// 视图类型,从localStorage中读取
|
// 视图类型,从localStorage中读取
|
||||||
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
|
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
|
||||||
@@ -68,7 +102,7 @@ let aiStatusCheckInterval: ReturnType<typeof setInterval> | null = null // AI状
|
|||||||
|
|
||||||
// 是否有搜索标签
|
// 是否有搜索标签
|
||||||
const hasSearchTags = computed(() => {
|
const hasSearchTags = computed(() => {
|
||||||
return !!(keyword || title || year || season)
|
return !!(keyword.value || title.value || year.value || season.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否启用筛选栏动画
|
// 是否启用筛选栏动画
|
||||||
@@ -305,20 +339,24 @@ function setSearchParam(params: URLSearchParams, key: string, value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建搜索流URL
|
// 构建搜索流URL
|
||||||
function buildSearchStreamUrl() {
|
function buildSearchStreamUrl(params: SearchParams, requestToken?: string) {
|
||||||
const isMediaSearch = /^[a-zA-Z]+:/.test(keyword)
|
const isMediaSearch = /^[a-zA-Z]+:/.test(params.keyword)
|
||||||
const url = getApiUrl(isMediaSearch ? `search/media/${encodeURIComponent(keyword)}/stream` : 'search/title/stream')
|
const url = getApiUrl(isMediaSearch ? `search/media/${encodeURIComponent(params.keyword)}/stream` : 'search/title/stream')
|
||||||
|
|
||||||
if (isMediaSearch) {
|
if (isMediaSearch) {
|
||||||
setSearchParam(url.searchParams, 'mtype', type)
|
setSearchParam(url.searchParams, 'mtype', params.type)
|
||||||
setSearchParam(url.searchParams, 'area', area)
|
setSearchParam(url.searchParams, 'area', params.area)
|
||||||
setSearchParam(url.searchParams, 'title', title)
|
setSearchParam(url.searchParams, 'title', params.title)
|
||||||
setSearchParam(url.searchParams, 'year', year)
|
setSearchParam(url.searchParams, 'year', params.year)
|
||||||
setSearchParam(url.searchParams, 'season', season)
|
setSearchParam(url.searchParams, 'season', params.season)
|
||||||
setSearchParam(url.searchParams, 'sites', sites)
|
setSearchParam(url.searchParams, 'sites', params.sites)
|
||||||
} else {
|
} else {
|
||||||
setSearchParam(url.searchParams, 'keyword', keyword)
|
setSearchParam(url.searchParams, 'keyword', params.keyword)
|
||||||
setSearchParam(url.searchParams, 'sites', sites)
|
setSearchParam(url.searchParams, 'sites', params.sites)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestToken) {
|
||||||
|
setSearchParam(url.searchParams, '_ts', requestToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.toString()
|
return url.toString()
|
||||||
@@ -422,26 +460,28 @@ function handleSearchStreamMessage(eventData: { [key: string]: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 按请求搜索
|
// 按请求搜索
|
||||||
async function searchByRequest() {
|
async function searchByRequest(params: SearchParams, requestToken?: string) {
|
||||||
let result: { [key: string]: any }
|
let result: { [key: string]: any }
|
||||||
// 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索
|
// 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索
|
||||||
if (/^[a-zA-Z]+:/.test(keyword)) {
|
if (/^[a-zA-Z]+:/.test(params.keyword)) {
|
||||||
result = await api.get(`search/media/${keyword}`, {
|
result = await api.get(`search/media/${params.keyword}`, {
|
||||||
params: {
|
params: {
|
||||||
mtype: type,
|
mtype: params.type,
|
||||||
area,
|
area: params.area,
|
||||||
title,
|
title: params.title,
|
||||||
year,
|
year: params.year,
|
||||||
season,
|
season: params.season,
|
||||||
sites,
|
sites: params.sites,
|
||||||
|
_ts: requestToken,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// 按标题模糊查询
|
// 按标题模糊查询
|
||||||
result = await api.get(`search/title`, {
|
result = await api.get(`search/title`, {
|
||||||
params: {
|
params: {
|
||||||
keyword,
|
keyword: params.keyword,
|
||||||
sites,
|
sites: params.sites,
|
||||||
|
_ts: requestToken,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -457,12 +497,12 @@ async function searchByRequest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 按流搜索
|
// 按流搜索
|
||||||
function searchByStream() {
|
function searchByStream(params: SearchParams, requestToken?: string) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
closeSearchEventSource()
|
closeSearchEventSource()
|
||||||
|
|
||||||
let settled = false
|
let settled = false
|
||||||
const source = new EventSource(buildSearchStreamUrl())
|
const source = new EventSource(buildSearchStreamUrl(params, requestToken))
|
||||||
searchEventSource = source
|
searchEventSource = source
|
||||||
|
|
||||||
source.onmessage = event => {
|
source.onmessage = event => {
|
||||||
@@ -512,25 +552,32 @@ function changeViewType(newType: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取搜索列表数据
|
// 获取搜索列表数据
|
||||||
async function fetchData() {
|
async function fetchData(options: { force?: boolean } = {}) {
|
||||||
|
const currentSearchParams = { ...activeSearchParams.value }
|
||||||
|
const requestToken = options.force || Boolean(currentSearchParams.keyword) ? createSearchRequestToken() : undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enableFilterAnimation.value = true
|
enableFilterAnimation.value = true
|
||||||
if (!keyword) {
|
if (!currentSearchParams.keyword) {
|
||||||
// 查询上次搜索结果
|
// 查询上次搜索结果
|
||||||
const results = await api.get('search/last')
|
const results = await api.get('search/last', {
|
||||||
|
params: requestToken ? { _ts: requestToken } : undefined,
|
||||||
|
})
|
||||||
setStreamResults((results as unknown as Context[]) || [])
|
setStreamResults((results as unknown as Context[]) || [])
|
||||||
} else {
|
} else {
|
||||||
resetSearchResults()
|
resetSearchResults()
|
||||||
startLoadingProgress()
|
startLoadingProgress()
|
||||||
try {
|
try {
|
||||||
await searchByStream()
|
await searchByStream(currentSearchParams, requestToken)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('渐进式搜索连接失败,回退到普通搜索:', error)
|
console.warn('渐进式搜索连接失败,回退到普通搜索:', error)
|
||||||
await searchByRequest()
|
await searchByRequest(currentSearchParams, requestToken)
|
||||||
}
|
}
|
||||||
stopLoadingProgress()
|
stopLoadingProgress()
|
||||||
// 从浏览器历史中删除当前搜索
|
// 搜索完成后移除地址栏参数,避免分享/刷新残留搜索条件
|
||||||
window.history.replaceState(null, '', window.location.pathname)
|
if (Object.keys(route.query).length > 0) {
|
||||||
|
await router.replace({ path: route.path, query: {} })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 标记已刷新
|
// 标记已刷新
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
@@ -550,7 +597,7 @@ async function refreshSearch() {
|
|||||||
try {
|
try {
|
||||||
// 重新搜索时退出 AI 视图,其余状态由 fetchData 内部重置
|
// 重新搜索时退出 AI 视图,其余状态由 fetchData 内部重置
|
||||||
showingAiResults.value = false
|
showingAiResults.value = false
|
||||||
await fetchData()
|
await fetchData({ force: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重新搜索失败:', error)
|
console.error('重新搜索失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -828,13 +875,27 @@ watchEffect(() => {
|
|||||||
!progressActive.value &&
|
!progressActive.value &&
|
||||||
!aiStatusChecked.value
|
!aiStatusChecked.value
|
||||||
) {
|
) {
|
||||||
checkAiRecommendStatus()
|
void checkAiRecommendStatus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query,
|
||||||
|
query => {
|
||||||
|
if (Object.keys(query).length === 0) return
|
||||||
|
|
||||||
|
const nextSearchParams = createSearchParams(query)
|
||||||
|
if (getSearchParamsKey(nextSearchParams) === getSearchParamsKey(activeSearchParams.value)) return
|
||||||
|
|
||||||
|
activeSearchParams.value = nextSearchParams
|
||||||
|
void fetchData()
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
fetchData()
|
void fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 卸载时停止轮询
|
// 卸载时停止轮询
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ registerRoute(
|
|||||||
({ url, request }) =>
|
({ url, request }) =>
|
||||||
url.pathname.includes('/api/v1/') &&
|
url.pathname.includes('/api/v1/') &&
|
||||||
request.method === 'GET' &&
|
request.method === 'GET' &&
|
||||||
|
!url.pathname.includes('/api/v1/search/') && // 搜索接口结果动态变化,避免缓存导致重复搜索失效
|
||||||
!url.pathname.includes('/api/v1/system/message') && // SSE实时消息流
|
!url.pathname.includes('/api/v1/system/message') && // SSE实时消息流
|
||||||
!url.pathname.includes('/api/v1/system/progress/') && // SSE实时进度流
|
!url.pathname.includes('/api/v1/system/progress/') && // SSE实时进度流
|
||||||
!url.pathname.includes('/api/v1/system/logging') && // SSE实时日志流
|
!url.pathname.includes('/api/v1/system/logging') && // SSE实时日志流
|
||||||
|
|||||||
Reference in New Issue
Block a user