import { useEffect, useRef, useState } from 'react' import { Link, useNavigate, useSearchParams } from 'react-router-dom' import toast from 'react-hot-toast' import { AlertTriangle, ArrowLeft, BookOpen, ChevronDown, ChevronRight, Loader2, Plus, Search } from 'lucide-react' import { readerAPI, type ReaderSearchBook, type ReaderSearchSkipped } from '../../api/reader' // 多源聚合搜索页(仿 legado SearchActivity:结果流 + 失败书源列表)。 export default function ReaderSearchPage() { const navigate = useNavigate() const [params] = useSearchParams() const [key, setKey] = useState(() => params.get('key') ?? '') const [searching, setSearching] = useState(false) const [books, setBooks] = useState(null) const [skipped, setSkipped] = useState([]) const [showSkipped, setShowSkipped] = useState(false) const [adding, setAdding] = useState('') const inputRef = useRef(null) const doSearch = async () => { const kw = key.trim() if (!kw || searching) return setSearching(true) setShowSkipped(false) try { const res = await readerAPI.search(kw) setBooks(res.books ?? []) // 后端在「没有书源失败」时会把空列表编码成 null,这里兜底成数组, // 否则下面 skipped.length 会直接抛 TypeError 把整页打崩。 setSkipped(res.skipped ?? []) if ((res.books ?? []).length === 0) toast.error('所有书源都没有找到结果') } catch (e) { const msg = (e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '搜索失败' toast.error(msg) } finally { setSearching(false) } } // 从首页顶部书搜索带 ?key= 进来时自动搜一次(只做一次,之后由用户手动搜) const autoSearchedRef = useRef(false) useEffect(() => { if (autoSearchedRef.current) return autoSearchedRef.current = true if (key.trim()) void doSearch() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const addToShelf = async (book: ReaderSearchBook) => { const origin = book.origins[0] if (!origin) return setAdding(book.book_url) try { await readerAPI.addBook({ origin, name: book.name, author: book.author, cover_url: book.cover_url }) toast.success(`《${book.name}》已加入书架`) } catch (e) { const msg = (e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '加入书架失败' toast.error(msg) } finally { setAdding('') } } const openBook = async (book: ReaderSearchBook) => { const origin = book.origins[0] if (!origin) return navigate( `/reader/book?${new URLSearchParams({ source_url: origin.origin, book_url: origin.book_url, name: book.name, author: book.author, cover_url: book.cover_url, origin_id: origin.source_id, origin_name: origin.origin_name, }).toString()}`, ) } return (
setKey(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && doSearch()} placeholder="搜索书名或作者" className="w-full bg-transparent text-sm text-[var(--app-text)] outline-none placeholder:text-[var(--app-muted)]" />
{searching && (

正在并发搜索启用的书源(单源最长 30 秒)…

)} {!searching && books !== null && (
{books.map((book) => (

{book.author || '佚名'} {book.kind ? ` · ${book.kind}` : ''} {book.word_count ? ` · ${book.word_count}` : ''}

最新:{book.latest_chapter || '未知'}

{book.intro &&

{book.intro}

}
))} {skipped.length > 0 && (
{showSkipped && (
{skipped.map((s) => (
{s.origin_name} {s.reason}
))} 去书源管理排查 →
)}
)}
)} {!searching && books === null && (
输入关键词开始搜索 · 需要 先导入书源
)}
) }