From 3890cb138e7572b170ed85264a84f83773bdfb6c Mon Sep 17 00:00:00 2001 From: mtvpls Date: Fri, 17 Apr 2026 15:24:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=90=9C=E7=B4=A2=E9=A1=B5?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E9=87=8D=E7=BD=AE=E5=92=8C=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E6=A0=8F=E6=8C=89=E9=92=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/manga/detail/page.tsx | 5 +- src/app/manga/page.tsx | 174 ++++++++++++++++++++++++--- src/app/manga/read/page.tsx | 9 +- src/components/manga/MangaLayout.tsx | 9 +- 4 files changed, 169 insertions(+), 28 deletions(-) diff --git a/src/app/manga/detail/page.tsx b/src/app/manga/detail/page.tsx index 7e472fe..c52561f 100644 --- a/src/app/manga/detail/page.tsx +++ b/src/app/manga/detail/page.tsx @@ -71,6 +71,7 @@ export default function MangaDetailPage() { const searchParams = useSearchParams(); const mangaId = searchParams.get('mangaId') || ''; const sourceId = searchParams.get('sourceId') || ''; + const returnTo = searchParams.get('returnTo') || '/manga'; const [detail, setDetail] = useState(null); const [history, setHistory] = useState>({}); const [shelf, setShelf] = useState>({}); @@ -140,7 +141,7 @@ export default function MangaDetailPage() { }; const chapterHref = (chapter: MangaChapter) => - `/manga/read?mangaId=${mangaId}&sourceId=${sourceId}&chapterId=${chapter.id}&title=${encodeURIComponent(detail?.title || '')}&cover=${encodeURIComponent(detail?.cover || '')}&sourceName=${encodeURIComponent(detail?.sourceName || '')}&chapterName=${encodeURIComponent(chapter.name)}`; + `/manga/read?mangaId=${mangaId}&sourceId=${sourceId}&chapterId=${chapter.id}&title=${encodeURIComponent(detail?.title || '')}&cover=${encodeURIComponent(detail?.cover || '')}&sourceName=${encodeURIComponent(detail?.sourceName || '')}&chapterName=${encodeURIComponent(chapter.name)}&returnTo=${encodeURIComponent(returnTo)}`; if (!detail) return ; @@ -175,7 +176,7 @@ export default function MangaDetailPage() { )} {currentRecord && ( 继续阅读 第 {currentRecord.pageIndex + 1}/{currentRecord.pageCount} 页 diff --git a/src/app/manga/page.tsx b/src/app/manga/page.tsx index 62bf3aa..fe75d4f 100644 --- a/src/app/manga/page.tsx +++ b/src/app/manga/page.tsx @@ -1,13 +1,16 @@ 'use client'; import { Search } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { deleteMangaShelf, getAllMangaShelf, saveMangaShelf } from '@/lib/db.client'; import { MangaSearchItem, MangaShelfItem, MangaSource } from '@/lib/manga.types'; import MangaCard from '@/components/MangaCard'; +const MANGA_SEARCH_STATE_KEY = 'manga_search_state'; + function MangaCardSkeleton({ withButton = false }: { withButton?: boolean }) { return (
@@ -24,6 +27,11 @@ function MangaCardSkeleton({ withButton = false }: { withButton?: boolean }) { } export default function MangaPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const urlQuery = searchParams.get('q')?.trim() || ''; + const urlSourceId = searchParams.get('sourceId') || ''; + const [query, setQuery] = useState(''); const [sources, setSources] = useState([]); const [sourceId, setSourceId] = useState(''); @@ -31,6 +39,64 @@ export default function MangaPage() { const [shelf, setShelf] = useState>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + const [hasSearched, setHasSearched] = useState(false); + const [lastSearchedQuery, setLastSearchedQuery] = useState(''); + const [lastSearchedSourceId, setLastSearchedSourceId] = useState(''); + const restoredRef = useRef(false); + + const getCacheKey = useCallback((keyword: string, selectedSourceId: string) => { + return `manga_search_cache_${selectedSourceId || 'all'}_${keyword.trim()}`; + }, []); + + const getCachedResults = useCallback( + (keyword: string, selectedSourceId: string) => { + if (typeof window === 'undefined' || !keyword.trim()) return null; + try { + const cached = sessionStorage.getItem(getCacheKey(keyword, selectedSourceId)); + return cached ? (JSON.parse(cached) as MangaSearchItem[]) : null; + } catch { + return null; + } + }, + [getCacheKey] + ); + + const setCachedResults = useCallback( + (keyword: string, selectedSourceId: string, nextResults: MangaSearchItem[]) => { + if (typeof window === 'undefined' || !keyword.trim()) return; + try { + sessionStorage.setItem(getCacheKey(keyword, selectedSourceId), JSON.stringify(nextResults)); + } catch { + // ignore session cache failures + } + }, + [getCacheKey] + ); + + const saveSearchState = useCallback((nextState: { query: string; sourceId: string; results: MangaSearchItem[] }) => { + if (typeof window === 'undefined') return; + try { + sessionStorage.setItem(MANGA_SEARCH_STATE_KEY, JSON.stringify(nextState)); + } catch { + // ignore session cache failures + } + }, []); + + const restoreSearchState = useCallback(() => { + if (typeof window === 'undefined') return null; + try { + const cached = sessionStorage.getItem(MANGA_SEARCH_STATE_KEY); + return cached + ? (JSON.parse(cached) as { + query: string; + sourceId: string; + results: MangaSearchItem[]; + }) + : null; + } catch { + return null; + } + }, []); useEffect(() => { fetch('/api/manga/sources') @@ -41,25 +107,97 @@ export default function MangaPage() { getAllMangaShelf().then(setShelf).catch(() => undefined); }, []); + const performSearch = useCallback( + async (keyword: string, selectedSourceId: string) => { + const trimmedQuery = keyword.trim(); + if (!trimmedQuery) return; + + setLoading(true); + setError(''); + setHasSearched(true); + setLastSearchedQuery(trimmedQuery); + setLastSearchedSourceId(selectedSourceId); + + const cached = getCachedResults(trimmedQuery, selectedSourceId); + if (cached) { + setResults(cached); + saveSearchState({ query: trimmedQuery, sourceId: selectedSourceId, results: cached }); + setLoading(false); + return; + } + + try { + const params = new URLSearchParams({ q: trimmedQuery }); + if (selectedSourceId) params.set('sourceId', selectedSourceId); + const res = await fetch(`/api/manga/search?${params.toString()}`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || '搜索失败'); + const nextResults = data.results || []; + setResults(nextResults); + setCachedResults(trimmedQuery, selectedSourceId, nextResults); + saveSearchState({ query: trimmedQuery, sourceId: selectedSourceId, results: nextResults }); + } catch (err) { + setError((err as Error).message); + setResults([]); + } finally { + setLoading(false); + } + }, + [getCachedResults, saveSearchState, setCachedResults] + ); + + useEffect(() => { + if (!restoredRef.current) { + restoredRef.current = true; + + if (!urlQuery) { + const cachedState = restoreSearchState(); + if (cachedState?.query?.trim()) { + setQuery(cachedState.query); + setSourceId(cachedState.sourceId || ''); + setResults(cachedState.results || []); + setHasSearched(true); + setLastSearchedQuery(cachedState.query); + setLastSearchedSourceId(cachedState.sourceId || ''); + } + return; + } + } + + setQuery(urlQuery); + setSourceId(urlSourceId); + + if (!urlQuery) { + setResults([]); + setHasSearched(false); + setLastSearchedQuery(''); + setLastSearchedSourceId(''); + setError(''); + return; + } + + void performSearch(urlQuery, urlSourceId); + }, [performSearch, restoreSearchState, urlQuery, urlSourceId]); + const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); - if (!query.trim()) return; - setLoading(true); - setError(''); - try { - const params = new URLSearchParams({ q: query.trim() }); - if (sourceId) params.set('sourceId', sourceId); - const res = await fetch(`/api/manga/search?${params.toString()}`); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || '搜索失败'); - setResults(data.results || []); - } catch (err) { - setError((err as Error).message); - } finally { - setLoading(false); - } + const trimmedQuery = query.trim(); + if (!trimmedQuery) return; + + const params = new URLSearchParams({ q: trimmedQuery }); + if (sourceId) params.set('sourceId', sourceId); + router.replace(`/manga?${params.toString()}`); + await performSearch(trimmedQuery, sourceId); }; + const returnTo = useMemo(() => { + const params = new URLSearchParams(); + if (lastSearchedQuery) params.set('q', lastSearchedQuery); + if (lastSearchedSourceId) params.set('sourceId', lastSearchedSourceId); + const queryString = params.toString(); + return queryString ? `/manga?${queryString}` : '/manga'; + }, [lastSearchedQuery, lastSearchedSourceId]); + const toggleShelf = async (item: MangaSearchItem) => { const key = `${item.sourceId}+${item.id}`; if (shelf[key]) { @@ -133,7 +271,7 @@ export default function MangaPage() {
) : results.length === 0 ? (
- 请输入关键词开始搜索漫画 + {hasSearched ? '没有找到相关漫画' : '请输入关键词开始搜索漫画'}
) : (
@@ -143,7 +281,7 @@ export default function MangaPage() {
返回详情 diff --git a/src/components/manga/MangaLayout.tsx b/src/components/manga/MangaLayout.tsx index d257fff..b42af3b 100644 --- a/src/components/manga/MangaLayout.tsx +++ b/src/components/manga/MangaLayout.tsx @@ -32,7 +32,7 @@ function getMeta(pathname: string, searchParams: ReturnType
-
+
{meta.backHref ? ( -
+