-
{item.title}
-
{item.author || item.sourceName}
-
已读 {Math.round(item.progressPercent || 0)}% · {getReadableChapterLabel(item)}
-
- {item.sourceId ? (
-
{ cacheBookReadRecord(item); if (item.sourceId && item.bookId) { cacheBookShelfItem({ sourceId: item.sourceId, sourceName: item.sourceName, bookId: item.bookId, title: item.title, author: item.author, cover: item.cover, format: item.format, detailHref: item.detailHref, acquisitionHref: item.acquisitionHref, saveTime: item.saveTime }); } }}
- className='rounded-2xl bg-sky-600 px-3 py-2 text-xs text-white'
- >
- 继续阅读
-
- ) : (
-
历史记录缺少书源信息
- )}
-
+ {loading ? (
+
+ ) : (
+ visibleItems.map((item) => (
+
+
+
{item.cover ?

: null}
+
+
{item.title}
+
{item.author || item.sourceName}
+
已读 {Math.round(item.progressPercent || 0)}% · {getReadableChapterLabel(item)}
+
+ {item.sourceId ? (
+ { cacheBookReadRecord(item); if (item.sourceId && item.bookId) { cacheBookShelfItem({ sourceId: item.sourceId, sourceName: item.sourceName, bookId: item.bookId, title: item.title, author: item.author, cover: item.cover, format: item.format, detailHref: item.detailHref, acquisitionHref: item.acquisitionHref, saveTime: item.saveTime }); } }}
+ className='rounded-2xl bg-sky-600 px-3 py-2 text-xs text-white'
+ >
+ 继续阅读
+
+ ) : (
+ 历史记录缺少书源信息
+ )}
+
+
-
- ))}
- {items.length === 0 ?
暂无阅读历史
: null}
+ ))
+ )}
+ {!loading && items.length === 0 ?
暂无阅读历史
: null}
{cacheModalOpen && mounted && createPortal(
setCacheModalOpen(false)}>
diff --git a/src/app/manga/history/page.tsx b/src/app/manga/history/page.tsx
index ddd9800..ba9e9aa 100644
--- a/src/app/manga/history/page.tsx
+++ b/src/app/manga/history/page.tsx
@@ -3,7 +3,7 @@
import { History } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
-import { deleteMangaReadRecord, deleteMangaShelf, getAllMangaReadRecords, getAllMangaShelf, saveMangaShelf, subscribeToDataUpdates } from '@/lib/db.client';
+import { deleteMangaReadRecord, deleteMangaShelf, getAllMangaReadRecords, getAllMangaShelf, getCachedMangaReadRecordsSnapshot, saveMangaShelf, subscribeToDataUpdates } from '@/lib/db.client';
import { MangaReadRecord, MangaShelfItem } from '@/lib/manga.types';
import MangaHistoryCard from '@/components/manga/MangaHistoryCard';
@@ -28,17 +28,33 @@ export default function MangaHistoryPage() {
const [history, setHistory] = useState>({});
const [loading, setLoading] = useState(true);
const [shelf, setShelf] = useState>({});
+ const [displayAll, setDisplayAll] = useState(false);
+
+ const updateHistory = (nextHistory: Record) => {
+ const sortedCount = Object.keys(nextHistory).length;
+ setHistory(nextHistory);
+ setDisplayAll(sortedCount <= 10);
+ if (sortedCount > 10) {
+ setTimeout(() => setDisplayAll(true), 0);
+ }
+ };
useEffect(() => {
+ const cachedHistory = getCachedMangaReadRecordsSnapshot();
+ if (Object.keys(cachedHistory).length > 0) {
+ updateHistory(cachedHistory);
+ setLoading(false);
+ }
+
Promise.all([getAllMangaReadRecords(), getAllMangaShelf()])
.then(([historyData, shelfData]) => {
- setHistory(historyData);
+ updateHistory(historyData);
setShelf(shelfData);
})
.catch(() => undefined)
.finally(() => setLoading(false));
- const unsubscribeHistory = subscribeToDataUpdates>('mangaHistoryUpdated', setHistory);
+ const unsubscribeHistory = subscribeToDataUpdates>('mangaHistoryUpdated', updateHistory);
const unsubscribeShelf = subscribeToDataUpdates>('mangaShelfUpdated', setShelf);
return () => {
@@ -51,6 +67,10 @@ export default function MangaHistoryPage() {
() => Object.entries(history).sort(([, a], [, b]) => b.saveTime - a.saveTime),
[history]
);
+ const visibleHistoryList = useMemo(
+ () => (displayAll ? historyList : historyList.slice(0, 10)),
+ [displayAll, historyList]
+ );
const toggleShelf = async (item: MangaReadRecord) => {
@@ -83,11 +103,11 @@ export default function MangaHistoryPage() {
const deleteHistory = async (item: MangaReadRecord) => {
const key = `${item.sourceId}+${item.mangaId}`;
await deleteMangaReadRecord(item.sourceId, item.mangaId);
- setHistory((prev) => {
- const next = { ...prev };
+ updateHistory((() => {
+ const next = { ...history };
delete next[key];
return next;
- });
+ })());
};
return (
@@ -103,7 +123,7 @@ export default function MangaHistoryPage() {
) : (
- {historyList.map(([key, item]) => (
+ {visibleHistoryList.map(([key, item]) => (
) {
return Object.fromEntries(entries.sort(([, a], [, b]) => b.saveTime - a.saveTime).slice(0, MAX_BOOK_HISTORY));
}
+function readBookHistoryCache(): Record {
+ if (typeof window === 'undefined') return {};
+ try {
+ const raw = localStorage.getItem(BOOK_HISTORY_KEY);
+ return raw ? (JSON.parse(raw) as Record) : {};
+ } catch {
+ return {};
+ }
+}
+
+function writeBookHistoryCache(records: Record) {
+ if (typeof window === 'undefined') return;
+ localStorage.setItem(BOOK_HISTORY_KEY, JSON.stringify(trimRecords(records)));
+}
+
+export function getCachedBookReadRecordsSnapshot(): Record {
+ return readBookHistoryCache();
+}
+
export async function getAllBookShelf(): Promise> {
if (typeof window === 'undefined') return {};
if (isRemoteStorage()) {
@@ -56,15 +75,32 @@ export async function deleteBookShelf(sourceId: string, bookId: string): Promise
export async function getAllBookReadRecords(): Promise> {
if (typeof window === 'undefined') return {};
if (isRemoteStorage()) {
- return (await (await fetchWithAuth('/api/books/history')).json()) as Record;
+ const cachedData = readBookHistoryCache();
+ if (Object.keys(cachedData).length > 0) {
+ fetchWithAuth('/api/books/history')
+ .then((response) => response.json() as Promise>)
+ .then((freshData) => {
+ writeBookHistoryCache(freshData);
+ window.dispatchEvent(new CustomEvent('bookHistoryUpdated', { detail: freshData }));
+ })
+ .catch(() => undefined);
+ return cachedData;
+ }
+
+ const freshData = (await (await fetchWithAuth('/api/books/history')).json()) as Record;
+ writeBookHistoryCache(freshData);
+ return freshData;
}
- const raw = localStorage.getItem(BOOK_HISTORY_KEY);
- return raw ? (JSON.parse(raw) as Record) : {};
+ return readBookHistoryCache();
}
export async function saveBookReadRecord(sourceId: string, bookId: string, record: BookReadRecord): Promise {
const key = generateStorageKey(sourceId, bookId);
if (isRemoteStorage()) {
+ const cached = readBookHistoryCache();
+ cached[key] = record;
+ writeBookHistoryCache(cached);
+ window.dispatchEvent(new CustomEvent('bookHistoryUpdated', { detail: trimRecords(cached) }));
await fetchWithAuth('/api/books/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -74,16 +110,20 @@ export async function saveBookReadRecord(sourceId: string, bookId: string, recor
}
const data = await getAllBookReadRecords();
data[key] = record;
- localStorage.setItem(BOOK_HISTORY_KEY, JSON.stringify(trimRecords(data)));
+ writeBookHistoryCache(data);
}
export async function deleteBookReadRecord(sourceId: string, bookId: string): Promise {
const key = generateStorageKey(sourceId, bookId);
if (isRemoteStorage()) {
+ const cached = readBookHistoryCache();
+ delete cached[key];
+ writeBookHistoryCache(cached);
+ window.dispatchEvent(new CustomEvent('bookHistoryUpdated', { detail: cached }));
await fetchWithAuth(`/api/books/history?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
return;
}
const data = await getAllBookReadRecords();
delete data[key];
- localStorage.setItem(BOOK_HISTORY_KEY, JSON.stringify(data));
+ writeBookHistoryCache(data);
}
diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts
index 119e361..0e0ccca 100644
--- a/src/lib/db.client.ts
+++ b/src/lib/db.client.ts
@@ -824,6 +824,42 @@ export function getCachedPlayRecordsSnapshot(): Record {
}
}
+export function getCachedMangaReadRecordsSnapshot(): Record {
+ if (typeof window === 'undefined') {
+ return {};
+ }
+
+ if (STORAGE_TYPE !== 'localstorage') {
+ const cachedRecords = cacheManager.getCachedMangaReadRecords();
+ if (cachedRecords) {
+ return cachedRecords;
+ }
+
+ try {
+ const username = getAuthInfoFromBrowserCookie()?.username;
+ if (!username) return {};
+
+ const raw = localStorage.getItem(`${CACHE_PREFIX}${username}`);
+ if (!raw) return {};
+
+ const userCache = JSON.parse(raw) as UserCacheStore;
+ return userCache.mangaReadRecords?.data || {};
+ } catch (err) {
+ console.error('读取用户漫画历史快照失败:', err);
+ return {};
+ }
+ }
+
+ try {
+ const raw = localStorage.getItem(MANGA_HISTORY_KEY);
+ if (!raw) return {};
+ return JSON.parse(raw) as Record;
+ } catch (err) {
+ console.error('读取本地漫画历史快照失败:', err);
+ return {};
+ }
+}
+
/**
* 保存播放记录。
* 数据库存储模式下使用乐观更新:先更新缓存(立即生效),再异步同步到数据库。
@@ -1965,7 +2001,8 @@ export type CacheUpdateEvent =
| 'searchHistoryUpdated'
| 'skipConfigsUpdated'
| 'mangaShelfUpdated'
- | 'mangaHistoryUpdated';
+ | 'mangaHistoryUpdated'
+ | 'bookHistoryUpdated';
/**
* 用于 React 组件监听数据更新的事件监听器