优化漫画展馆和电子书室的历史加载速度
This commit is contained in:
@@ -7,8 +7,9 @@ import { createPortal } from 'react-dom';
|
|||||||
|
|
||||||
import { deleteCachedBookFile, listCachedBookFiles, type CachedBookFile } from '@/lib/book-cache.client';
|
import { deleteCachedBookFile, listCachedBookFiles, type CachedBookFile } from '@/lib/book-cache.client';
|
||||||
import { buildBookReadPath, cacheBookReadRecord, cacheBookShelfItem } from '@/lib/book-route-cache.client';
|
import { buildBookReadPath, cacheBookReadRecord, cacheBookShelfItem } from '@/lib/book-route-cache.client';
|
||||||
import { deleteBookReadRecord, getAllBookReadRecords, getAllBookShelf } from '@/lib/book.db.client';
|
import { deleteBookReadRecord, getAllBookReadRecords, getAllBookShelf, getCachedBookReadRecordsSnapshot } from '@/lib/book.db.client';
|
||||||
import { BookReadRecord, BookShelfItem } from '@/lib/book.types';
|
import { BookReadRecord, BookShelfItem } from '@/lib/book.types';
|
||||||
|
import { subscribeToDataUpdates } from '@/lib/db.client';
|
||||||
|
|
||||||
function looksLikeInternalHref(value?: string) {
|
function looksLikeInternalHref(value?: string) {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
@@ -31,19 +32,62 @@ function formatBytes(size: number) {
|
|||||||
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BookHistorySkeleton() {
|
||||||
|
return (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
|
<div key={index} className='rounded-3xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<div className='h-28 w-20 animate-pulse overflow-hidden rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||||
|
<div className='min-w-0 flex-1 space-y-3'>
|
||||||
|
<div className='h-5 w-2/3 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||||
|
<div className='h-4 w-1/3 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||||
|
<div className='h-4 w-1/2 animate-pulse rounded bg-gray-200 dark:bg-gray-800' />
|
||||||
|
<div className='flex gap-2 pt-1'>
|
||||||
|
<div className='h-9 w-20 animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||||
|
<div className='h-9 w-16 animate-pulse rounded-2xl bg-gray-200 dark:bg-gray-800' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function BookHistoryPage() {
|
export default function BookHistoryPage() {
|
||||||
const [records, setRecords] = useState<Record<string, BookReadRecord>>({});
|
const [records, setRecords] = useState<Record<string, BookReadRecord>>({});
|
||||||
const [shelf, setShelf] = useState<Record<string, BookShelfItem>>({});
|
const [shelf, setShelf] = useState<Record<string, BookShelfItem>>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [cacheModalOpen, setCacheModalOpen] = useState(false);
|
const [cacheModalOpen, setCacheModalOpen] = useState(false);
|
||||||
const [cacheItems, setCacheItems] = useState<CachedBookFile[]>([]);
|
const [cacheItems, setCacheItems] = useState<CachedBookFile[]>([]);
|
||||||
const [cacheLoading, setCacheLoading] = useState(false);
|
const [cacheLoading, setCacheLoading] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [confirmAction, setConfirmAction] = useState<{ type: 'delete-one' | 'clear-all'; key?: string; title?: string } | null>(null);
|
const [confirmAction, setConfirmAction] = useState<{ type: 'delete-one' | 'clear-all'; key?: string; title?: string } | null>(null);
|
||||||
|
const [displayAll, setDisplayAll] = useState(false);
|
||||||
|
|
||||||
|
const updateRecords = (nextRecords: Record<string, BookReadRecord>) => {
|
||||||
|
const count = Object.keys(nextRecords).length;
|
||||||
|
setRecords(nextRecords);
|
||||||
|
setDisplayAll(count <= 10);
|
||||||
|
if (count > 10) {
|
||||||
|
setTimeout(() => setDisplayAll(true), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
getAllBookReadRecords().then(setRecords).catch(() => undefined);
|
const cachedRecords = getCachedBookReadRecordsSnapshot();
|
||||||
|
if (Object.keys(cachedRecords).length > 0) {
|
||||||
|
updateRecords(cachedRecords);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllBookReadRecords().then(updateRecords).catch(() => undefined).finally(() => setLoading(false));
|
||||||
getAllBookShelf().then(setShelf).catch(() => undefined);
|
getAllBookShelf().then(setShelf).catch(() => undefined);
|
||||||
|
|
||||||
|
const unsubscribeHistory = subscribeToDataUpdates<Record<string, BookReadRecord>>('bookHistoryUpdated', updateRecords);
|
||||||
|
return unsubscribeHistory;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadCacheItems = async () => {
|
const loadCacheItems = async () => {
|
||||||
@@ -79,6 +123,10 @@ export default function BookHistoryPage() {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.saveTime - a.saveTime), [records, shelf]);
|
.sort((a, b) => b.saveTime - a.saveTime), [records, shelf]);
|
||||||
|
const visibleItems = useMemo(
|
||||||
|
() => (displayAll ? items : items.slice(0, 10)),
|
||||||
|
[displayAll, items]
|
||||||
|
);
|
||||||
|
|
||||||
const cacheTotalSize = useMemo(() => cacheItems.reduce((sum, item) => sum + item.size, 0), [cacheItems]);
|
const cacheTotalSize = useMemo(() => cacheItems.reduce((sum, item) => sum + item.size, 0), [cacheItems]);
|
||||||
|
|
||||||
@@ -97,33 +145,37 @@ export default function BookHistoryPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.map((item) => (
|
{loading ? (
|
||||||
<div key={item.storageKey} className='rounded-3xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
<BookHistorySkeleton />
|
||||||
<div className='flex gap-4'>
|
) : (
|
||||||
<div className='h-28 w-20 overflow-hidden rounded-2xl bg-gray-100 dark:bg-gray-900'>{item.cover ? <img src={item.cover} alt={item.title} className='h-full w-full object-cover' /> : null}</div>
|
visibleItems.map((item) => (
|
||||||
<div className='min-w-0 flex-1'>
|
<div key={item.storageKey} className='rounded-3xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-950'>
|
||||||
<div className='truncate font-medium'>{item.title}</div>
|
<div className='flex gap-4'>
|
||||||
<div className='mt-1 text-sm text-gray-500'>{item.author || item.sourceName}</div>
|
<div className='h-28 w-20 overflow-hidden rounded-2xl bg-gray-100 dark:bg-gray-900'>{item.cover ? <img src={item.cover} alt={item.title} className='h-full w-full object-cover' /> : null}</div>
|
||||||
<div className='mt-1 text-xs text-gray-500'>已读 {Math.round(item.progressPercent || 0)}% · {getReadableChapterLabel(item)}</div>
|
<div className='min-w-0 flex-1'>
|
||||||
<div className='mt-3 flex flex-wrap gap-2'>
|
<div className='truncate font-medium'>{item.title}</div>
|
||||||
{item.sourceId ? (
|
<div className='mt-1 text-sm text-gray-500'>{item.author || item.sourceName}</div>
|
||||||
<Link
|
<div className='mt-1 text-xs text-gray-500'>已读 {Math.round(item.progressPercent || 0)}% · {getReadableChapterLabel(item)}</div>
|
||||||
href={buildBookReadPath(item.sourceId, item.bookId)}
|
<div className='mt-3 flex flex-wrap gap-2'>
|
||||||
onClick={() => { 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 }); } }}
|
{item.sourceId ? (
|
||||||
className='rounded-2xl bg-sky-600 px-3 py-2 text-xs text-white'
|
<Link
|
||||||
>
|
href={buildBookReadPath(item.sourceId, item.bookId)}
|
||||||
继续阅读
|
onClick={() => { 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 }); } }}
|
||||||
</Link>
|
className='rounded-2xl bg-sky-600 px-3 py-2 text-xs text-white'
|
||||||
) : (
|
>
|
||||||
<span className='rounded-2xl bg-gray-200 px-3 py-2 text-xs text-gray-500 dark:bg-gray-800'>历史记录缺少书源信息</span>
|
继续阅读
|
||||||
)}
|
</Link>
|
||||||
<button onClick={async () => { const [deleteSourceId = item.sourceId, deleteBookId = item.bookId] = item.storageKey.split('+'); await deleteBookReadRecord(deleteSourceId, deleteBookId); setRecords((prev) => { const next = { ...prev }; delete next[item.storageKey]; return next; }); }} className='rounded-2xl border border-gray-200 px-3 py-2 text-xs dark:border-gray-700'>删除</button>
|
) : (
|
||||||
|
<span className='rounded-2xl bg-gray-200 px-3 py-2 text-xs text-gray-500 dark:bg-gray-800'>历史记录缺少书源信息</span>
|
||||||
|
)}
|
||||||
|
<button onClick={async () => { const [deleteSourceId = item.sourceId, deleteBookId = item.bookId] = item.storageKey.split('+'); await deleteBookReadRecord(deleteSourceId, deleteBookId); updateRecords((() => { const next = { ...records }; delete next[item.storageKey]; return next; })()); }} className='rounded-2xl border border-gray-200 px-3 py-2 text-xs dark:border-gray-700'>删除</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
))}
|
)}
|
||||||
{items.length === 0 ? <div className='text-sm text-gray-500'>暂无阅读历史</div> : null}
|
{!loading && items.length === 0 ? <div className='text-sm text-gray-500'>暂无阅读历史</div> : null}
|
||||||
|
|
||||||
{cacheModalOpen && mounted && createPortal(
|
{cacheModalOpen && mounted && createPortal(
|
||||||
<div className='fixed inset-0 z-50 bg-black/40' onClick={() => setCacheModalOpen(false)}>
|
<div className='fixed inset-0 z-50 bg-black/40' onClick={() => setCacheModalOpen(false)}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { History } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from '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 { MangaReadRecord, MangaShelfItem } from '@/lib/manga.types';
|
||||||
|
|
||||||
import MangaHistoryCard from '@/components/manga/MangaHistoryCard';
|
import MangaHistoryCard from '@/components/manga/MangaHistoryCard';
|
||||||
@@ -28,17 +28,33 @@ export default function MangaHistoryPage() {
|
|||||||
const [history, setHistory] = useState<Record<string, MangaReadRecord>>({});
|
const [history, setHistory] = useState<Record<string, MangaReadRecord>>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [shelf, setShelf] = useState<Record<string, MangaShelfItem>>({});
|
const [shelf, setShelf] = useState<Record<string, MangaShelfItem>>({});
|
||||||
|
const [displayAll, setDisplayAll] = useState(false);
|
||||||
|
|
||||||
|
const updateHistory = (nextHistory: Record<string, MangaReadRecord>) => {
|
||||||
|
const sortedCount = Object.keys(nextHistory).length;
|
||||||
|
setHistory(nextHistory);
|
||||||
|
setDisplayAll(sortedCount <= 10);
|
||||||
|
if (sortedCount > 10) {
|
||||||
|
setTimeout(() => setDisplayAll(true), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const cachedHistory = getCachedMangaReadRecordsSnapshot();
|
||||||
|
if (Object.keys(cachedHistory).length > 0) {
|
||||||
|
updateHistory(cachedHistory);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all([getAllMangaReadRecords(), getAllMangaShelf()])
|
Promise.all([getAllMangaReadRecords(), getAllMangaShelf()])
|
||||||
.then(([historyData, shelfData]) => {
|
.then(([historyData, shelfData]) => {
|
||||||
setHistory(historyData);
|
updateHistory(historyData);
|
||||||
setShelf(shelfData);
|
setShelf(shelfData);
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
|
||||||
const unsubscribeHistory = subscribeToDataUpdates<Record<string, MangaReadRecord>>('mangaHistoryUpdated', setHistory);
|
const unsubscribeHistory = subscribeToDataUpdates<Record<string, MangaReadRecord>>('mangaHistoryUpdated', updateHistory);
|
||||||
const unsubscribeShelf = subscribeToDataUpdates<Record<string, MangaShelfItem>>('mangaShelfUpdated', setShelf);
|
const unsubscribeShelf = subscribeToDataUpdates<Record<string, MangaShelfItem>>('mangaShelfUpdated', setShelf);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -51,6 +67,10 @@ export default function MangaHistoryPage() {
|
|||||||
() => Object.entries(history).sort(([, a], [, b]) => b.saveTime - a.saveTime),
|
() => Object.entries(history).sort(([, a], [, b]) => b.saveTime - a.saveTime),
|
||||||
[history]
|
[history]
|
||||||
);
|
);
|
||||||
|
const visibleHistoryList = useMemo(
|
||||||
|
() => (displayAll ? historyList : historyList.slice(0, 10)),
|
||||||
|
[displayAll, historyList]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
const toggleShelf = async (item: MangaReadRecord) => {
|
const toggleShelf = async (item: MangaReadRecord) => {
|
||||||
@@ -83,11 +103,11 @@ export default function MangaHistoryPage() {
|
|||||||
const deleteHistory = async (item: MangaReadRecord) => {
|
const deleteHistory = async (item: MangaReadRecord) => {
|
||||||
const key = `${item.sourceId}+${item.mangaId}`;
|
const key = `${item.sourceId}+${item.mangaId}`;
|
||||||
await deleteMangaReadRecord(item.sourceId, item.mangaId);
|
await deleteMangaReadRecord(item.sourceId, item.mangaId);
|
||||||
setHistory((prev) => {
|
updateHistory((() => {
|
||||||
const next = { ...prev };
|
const next = { ...history };
|
||||||
delete next[key];
|
delete next[key];
|
||||||
return next;
|
return next;
|
||||||
});
|
})());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -103,7 +123,7 @@ export default function MangaHistoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-6'>
|
<div className='grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-6'>
|
||||||
{historyList.map(([key, item]) => (
|
{visibleHistoryList.map(([key, item]) => (
|
||||||
<MangaHistoryCard
|
<MangaHistoryCard
|
||||||
key={key}
|
key={key}
|
||||||
item={item}
|
item={item}
|
||||||
|
|||||||
@@ -18,6 +18,25 @@ function trimRecords(records: Record<string, BookReadRecord>) {
|
|||||||
return Object.fromEntries(entries.sort(([, a], [, b]) => b.saveTime - a.saveTime).slice(0, MAX_BOOK_HISTORY));
|
return Object.fromEntries(entries.sort(([, a], [, b]) => b.saveTime - a.saveTime).slice(0, MAX_BOOK_HISTORY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readBookHistoryCache(): Record<string, BookReadRecord> {
|
||||||
|
if (typeof window === 'undefined') return {};
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(BOOK_HISTORY_KEY);
|
||||||
|
return raw ? (JSON.parse(raw) as Record<string, BookReadRecord>) : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeBookHistoryCache(records: Record<string, BookReadRecord>) {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
localStorage.setItem(BOOK_HISTORY_KEY, JSON.stringify(trimRecords(records)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedBookReadRecordsSnapshot(): Record<string, BookReadRecord> {
|
||||||
|
return readBookHistoryCache();
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllBookShelf(): Promise<Record<string, BookShelfItem>> {
|
export async function getAllBookShelf(): Promise<Record<string, BookShelfItem>> {
|
||||||
if (typeof window === 'undefined') return {};
|
if (typeof window === 'undefined') return {};
|
||||||
if (isRemoteStorage()) {
|
if (isRemoteStorage()) {
|
||||||
@@ -56,15 +75,32 @@ export async function deleteBookShelf(sourceId: string, bookId: string): Promise
|
|||||||
export async function getAllBookReadRecords(): Promise<Record<string, BookReadRecord>> {
|
export async function getAllBookReadRecords(): Promise<Record<string, BookReadRecord>> {
|
||||||
if (typeof window === 'undefined') return {};
|
if (typeof window === 'undefined') return {};
|
||||||
if (isRemoteStorage()) {
|
if (isRemoteStorage()) {
|
||||||
return (await (await fetchWithAuth('/api/books/history')).json()) as Record<string, BookReadRecord>;
|
const cachedData = readBookHistoryCache();
|
||||||
|
if (Object.keys(cachedData).length > 0) {
|
||||||
|
fetchWithAuth('/api/books/history')
|
||||||
|
.then((response) => response.json() as Promise<Record<string, BookReadRecord>>)
|
||||||
|
.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<string, BookReadRecord>;
|
||||||
|
writeBookHistoryCache(freshData);
|
||||||
|
return freshData;
|
||||||
}
|
}
|
||||||
const raw = localStorage.getItem(BOOK_HISTORY_KEY);
|
return readBookHistoryCache();
|
||||||
return raw ? (JSON.parse(raw) as Record<string, BookReadRecord>) : {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveBookReadRecord(sourceId: string, bookId: string, record: BookReadRecord): Promise<void> {
|
export async function saveBookReadRecord(sourceId: string, bookId: string, record: BookReadRecord): Promise<void> {
|
||||||
const key = generateStorageKey(sourceId, bookId);
|
const key = generateStorageKey(sourceId, bookId);
|
||||||
if (isRemoteStorage()) {
|
if (isRemoteStorage()) {
|
||||||
|
const cached = readBookHistoryCache();
|
||||||
|
cached[key] = record;
|
||||||
|
writeBookHistoryCache(cached);
|
||||||
|
window.dispatchEvent(new CustomEvent('bookHistoryUpdated', { detail: trimRecords(cached) }));
|
||||||
await fetchWithAuth('/api/books/history', {
|
await fetchWithAuth('/api/books/history', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -74,16 +110,20 @@ export async function saveBookReadRecord(sourceId: string, bookId: string, recor
|
|||||||
}
|
}
|
||||||
const data = await getAllBookReadRecords();
|
const data = await getAllBookReadRecords();
|
||||||
data[key] = record;
|
data[key] = record;
|
||||||
localStorage.setItem(BOOK_HISTORY_KEY, JSON.stringify(trimRecords(data)));
|
writeBookHistoryCache(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteBookReadRecord(sourceId: string, bookId: string): Promise<void> {
|
export async function deleteBookReadRecord(sourceId: string, bookId: string): Promise<void> {
|
||||||
const key = generateStorageKey(sourceId, bookId);
|
const key = generateStorageKey(sourceId, bookId);
|
||||||
if (isRemoteStorage()) {
|
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' });
|
await fetchWithAuth(`/api/books/history?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await getAllBookReadRecords();
|
const data = await getAllBookReadRecords();
|
||||||
delete data[key];
|
delete data[key];
|
||||||
localStorage.setItem(BOOK_HISTORY_KEY, JSON.stringify(data));
|
writeBookHistoryCache(data);
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-1
@@ -824,6 +824,42 @@ export function getCachedPlayRecordsSnapshot(): Record<string, PlayRecord> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCachedMangaReadRecordsSnapshot(): Record<string, MangaReadRecord> {
|
||||||
|
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<string, MangaReadRecord>;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('读取本地漫画历史快照失败:', err);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存播放记录。
|
* 保存播放记录。
|
||||||
* 数据库存储模式下使用乐观更新:先更新缓存(立即生效),再异步同步到数据库。
|
* 数据库存储模式下使用乐观更新:先更新缓存(立即生效),再异步同步到数据库。
|
||||||
@@ -1965,7 +2001,8 @@ export type CacheUpdateEvent =
|
|||||||
| 'searchHistoryUpdated'
|
| 'searchHistoryUpdated'
|
||||||
| 'skipConfigsUpdated'
|
| 'skipConfigsUpdated'
|
||||||
| 'mangaShelfUpdated'
|
| 'mangaShelfUpdated'
|
||||||
| 'mangaHistoryUpdated';
|
| 'mangaHistoryUpdated'
|
||||||
|
| 'bookHistoryUpdated';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于 React 组件监听数据更新的事件监听器
|
* 用于 React 组件监听数据更新的事件监听器
|
||||||
|
|||||||
Reference in New Issue
Block a user