From 6360b0fa182893fa3d2e4a3367cf2577f061af93 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 18 Apr 2026 09:48:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BC=AB=E7=94=BB=E9=98=85=E8=AF=BB=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/manga/history/page.tsx | 67 +++++++-- src/components/manga/MangaHistoryCard.tsx | 158 ++++++++++++++++++++++ 2 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 src/components/manga/MangaHistoryCard.tsx diff --git a/src/app/manga/history/page.tsx b/src/app/manga/history/page.tsx index b52320f..ddd9800 100644 --- a/src/app/manga/history/page.tsx +++ b/src/app/manga/history/page.tsx @@ -3,10 +3,10 @@ import { History } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; -import { getAllMangaReadRecords } from '@/lib/db.client'; -import { MangaReadRecord } from '@/lib/manga.types'; +import { deleteMangaReadRecord, deleteMangaShelf, getAllMangaReadRecords, getAllMangaShelf, saveMangaShelf, subscribeToDataUpdates } from '@/lib/db.client'; +import { MangaReadRecord, MangaShelfItem } from '@/lib/manga.types'; -import MangaCard from '@/components/MangaCard'; +import MangaHistoryCard from '@/components/manga/MangaHistoryCard'; function MangaHistorySkeleton() { return ( @@ -27,12 +27,24 @@ function MangaHistorySkeleton() { export default function MangaHistoryPage() { const [history, setHistory] = useState>({}); const [loading, setLoading] = useState(true); + const [shelf, setShelf] = useState>({}); useEffect(() => { - getAllMangaReadRecords() - .then(setHistory) + Promise.all([getAllMangaReadRecords(), getAllMangaShelf()]) + .then(([historyData, shelfData]) => { + setHistory(historyData); + setShelf(shelfData); + }) .catch(() => undefined) .finally(() => setLoading(false)); + + const unsubscribeHistory = subscribeToDataUpdates>('mangaHistoryUpdated', setHistory); + const unsubscribeShelf = subscribeToDataUpdates>('mangaShelfUpdated', setShelf); + + return () => { + unsubscribeHistory(); + unsubscribeShelf(); + }; }, []); const historyList = useMemo( @@ -40,6 +52,44 @@ export default function MangaHistoryPage() { [history] ); + + const toggleShelf = async (item: MangaReadRecord) => { + const key = `${item.sourceId}+${item.mangaId}`; + if (shelf[key]) { + await deleteMangaShelf(item.sourceId, item.mangaId); + setShelf((prev) => { + const next = { ...prev }; + delete next[key]; + return next; + }); + return; + } + + const shelfItem: MangaShelfItem = { + title: item.title, + cover: item.cover, + sourceId: item.sourceId, + sourceName: item.sourceName, + mangaId: item.mangaId, + saveTime: Date.now(), + lastChapterId: item.chapterId, + lastChapterName: item.chapterName, + }; + + await saveMangaShelf(item.sourceId, item.mangaId, shelfItem); + setShelf((prev) => ({ ...prev, [key]: shelfItem })); + }; + + const deleteHistory = async (item: MangaReadRecord) => { + const key = `${item.sourceId}+${item.mangaId}`; + await deleteMangaReadRecord(item.sourceId, item.mangaId); + setHistory((prev) => { + const next = { ...prev }; + delete next[key]; + return next; + }); + }; + return (
@@ -54,11 +104,12 @@ export default function MangaHistoryPage() { ) : (
{historyList.map(([key, item]) => ( - ))}
diff --git a/src/components/manga/MangaHistoryCard.tsx b/src/components/manga/MangaHistoryCard.tsx new file mode 100644 index 0000000..137d769 --- /dev/null +++ b/src/components/manga/MangaHistoryCard.tsx @@ -0,0 +1,158 @@ +'use client'; + +import { BookOpen, CircleMinus, CirclePlus, Info, Trash2 } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useMemo, useState } from 'react'; + +import { useLongPress } from '@/hooks/useLongPress'; +import { MangaReadRecord } from '@/lib/manga.types'; +import { processImageUrl } from '@/lib/utils'; + +import ImageViewer from '@/components/ImageViewer'; +import MobileActionSheet from '@/components/MobileActionSheet'; +import ProxyImage from '@/components/ProxyImage'; + +interface MangaHistoryCardProps { + item: MangaReadRecord; + inShelf: boolean; + onToggleShelf: (item: MangaReadRecord) => void | Promise; + onDelete: (item: MangaReadRecord) => void | Promise; +} + +export default function MangaHistoryCard({ item, inShelf, onToggleShelf, onDelete }: MangaHistoryCardProps) { + const router = useRouter(); + const [showActions, setShowActions] = useState(false); + const [showImageViewer, setShowImageViewer] = useState(false); + + const readHref = useMemo( + () => `/manga/read?mangaId=${item.mangaId}&sourceId=${item.sourceId}&chapterId=${item.chapterId}&title=${encodeURIComponent(item.title)}&cover=${encodeURIComponent(item.cover)}&sourceName=${encodeURIComponent(item.sourceName)}&chapterName=${encodeURIComponent(item.chapterName)}&returnTo=${encodeURIComponent('/manga/history')}`, + [item] + ); + + const detailHref = useMemo( + () => `/manga/detail?mangaId=${item.mangaId}&sourceId=${item.sourceId}&title=${encodeURIComponent(item.title)}&cover=${encodeURIComponent(item.cover)}&sourceName=${encodeURIComponent(item.sourceName)}&returnTo=${encodeURIComponent('/manga/history')}`, + [item] + ); + + const subtitle = useMemo( + () => `${item.chapterName} · 第 ${item.pageIndex + 1}/${item.pageCount} 页`, + [item] + ); + + const openActions = () => setShowActions(true); + const goRead = () => router.push(readHref); + + const longPressProps = useLongPress({ + onLongPress: openActions, + onClick: goRead, + longPressDelay: 500, + }); + + const actions = useMemo( + () => [ + { + id: 'continue-reading', + label: '继续阅读', + icon: , + onClick: goRead, + color: 'primary' as const, + }, + { + id: 'detail', + label: '详情', + icon: , + onClick: () => router.push(detailHref), + }, + { + id: 'toggle-shelf', + label: inShelf ? '移除书架' : '加入书架', + icon: inShelf ? : , + onClick: () => onToggleShelf(item), + color: inShelf ? ('danger' as const) : ('default' as const), + }, + { + id: 'delete', + label: '删除', + icon: , + onClick: () => onDelete(item), + color: 'danger' as const, + }, + ], + [detailHref, goRead, inShelf, item, onDelete, onToggleShelf, router] + ); + + return ( + <> +
{ + e.preventDefault(); + e.stopPropagation(); + openActions(); + }} + onDragStart={(e) => { + e.preventDefault(); + }} + {...longPressProps} + style={{ + WebkitUserSelect: 'none', + userSelect: 'none', + WebkitTouchCallout: 'none', + WebkitTapHighlightColor: 'transparent', + touchAction: 'manipulation', + }} + > +
{ + e.preventDefault(); + e.stopPropagation(); + }} + style={{ + WebkitTouchCallout: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', + }} + > + {item.cover ? ( + + ) : ( +
暂无封面
+ )} +
+
+
+ {item.title} +
+
{item.sourceName}
+
{subtitle}
+
+
+ + setShowActions(false)} + title={item.title} + poster={processImageUrl(item.cover)} + sourceName={item.sourceName} + actions={actions} + onPosterClick={() => setShowImageViewer(true)} + /> + + {showImageViewer && ( + setShowImageViewer(false)} + imageUrl={item.cover} + alt={item.title} + /> + )} + + ); +}