File size: 4,597 Bytes
00a912e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import { SERVER_URL } from '@/lib/config'
import type { MusicSource, Playlist, Track } from '@music-together/shared'
import { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
const PAGE_SIZE = 20
type SearchResult = Track | Playlist
/**
* 搜索逻辑 hook — 管理搜索/翻页/abort/竞态保护。
* 从 SearchDialog 中提取,使 UI 组件只关注渲染。
*/
export function useSearch(source: MusicSource, type: 'song' | 'album' | 'playlist' = 'song', roomId?: string) {
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(false)
const [hasSearched, setHasSearched] = useState(false)
const abortRef = useRef<AbortController | null>(null)
const loadMoreAbortRef = useRef<AbortController | null>(null)
const searchIdRef = useRef(0)
const lastKeywordRef = useRef('')
// Cancel any in-flight requests on unmount
useEffect(() => {
return () => {
abortRef.current?.abort()
loadMoreAbortRef.current?.abort()
}
}, [])
const fetchPage = useCallback(
async (
searchSource: MusicSource,
searchKeyword: string,
searchPage: number,
signal: AbortSignal,
searchType: 'song' | 'album' | 'playlist',
): Promise<{ tracks: SearchResult[]; hasMore: boolean }> => {
const params = new URLSearchParams({
source: searchSource,
keyword: searchKeyword,
limit: String(PAGE_SIZE),
page: String(searchPage),
type: searchType,
})
if (roomId) params.set('roomId', roomId)
const res = await fetch(`${SERVER_URL}/api/music/search?${params.toString()}`, { signal, credentials: 'include' })
if (!res.ok) throw new Error('Search failed')
const data = await res.json()
const tracks = data.tracks || []
return { tracks, hasMore: data.hasMore ?? tracks.length >= PAGE_SIZE }
},
[roomId],
)
const search = useCallback(
(keyword: string) => {
const trimmed = keyword.trim()
if (!trimmed) return
abortRef.current?.abort()
loadMoreAbortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
const currentSearchId = ++searchIdRef.current
lastKeywordRef.current = trimmed
setLoading(true)
setHasSearched(true)
fetchPage(source, trimmed, 1, controller.signal, type)
.then((data) => {
if (searchIdRef.current !== currentSearchId) return
setResults(data.tracks)
setPage(1)
setHasMore(data.hasMore)
})
.catch((err) => {
if (err instanceof DOMException && err.name === 'AbortError') return
if (searchIdRef.current !== currentSearchId) return
toast.error('搜索失败,请重试')
setResults([])
setHasMore(false)
})
.finally(() => {
if (searchIdRef.current === currentSearchId) {
setLoading(false)
}
})
},
[source, type, fetchPage],
)
const loadMore = useCallback(() => {
if (loadingMore) return
loadMoreAbortRef.current?.abort()
const controller = new AbortController()
loadMoreAbortRef.current = controller
const currentSearchId = searchIdRef.current
const nextPage = page + 1
setLoadingMore(true)
fetchPage(source, lastKeywordRef.current, nextPage, controller.signal, type)
.then((data) => {
if (searchIdRef.current !== currentSearchId) return
setResults((prev) => [...prev, ...data.tracks])
setPage(nextPage)
setHasMore(data.hasMore)
})
.catch((err) => {
if (err instanceof DOMException && err.name === 'AbortError') return
if (searchIdRef.current !== currentSearchId) return
toast.error('加载失败,请重试')
})
.finally(() => {
if (searchIdRef.current === currentSearchId) {
setLoadingMore(false)
}
})
}, [loadingMore, page, source, type, fetchPage])
const resetState = useCallback(() => {
abortRef.current?.abort()
loadMoreAbortRef.current?.abort()
searchIdRef.current++
setResults([])
setPage(1)
setHasMore(false)
setHasSearched(false)
setLoading(false)
setLoadingMore(false)
lastKeywordRef.current = ''
}, [])
return { results, loading, loadingMore, hasMore, hasSearched, page, search, loadMore, resetState }
}
|