增加漫画展馆功能

This commit is contained in:
mtvpls
2026-04-16 16:01:47 +08:00
parent 4fe9314c57
commit cb225c3291
33 changed files with 3242 additions and 5 deletions
+323 -3
View File
@@ -15,6 +15,7 @@
*/
import { getAuthInfoFromBrowserCookie, clearAuthCookie } from './auth';
import { MangaReadRecord, MangaShelfItem } from './manga.types';
import { DanmakuFilterConfig, EpisodeFilterConfig,SkipConfig } from './types';
// 全局错误触发函数
@@ -81,6 +82,8 @@ interface CacheData<T> {
interface UserCacheStore {
playRecords?: CacheData<Record<string, PlayRecord>>;
favorites?: CacheData<Record<string, Favorite>>;
mangaShelf?: CacheData<Record<string, MangaShelfItem>>;
mangaReadRecords?: CacheData<Record<string, MangaReadRecord>>;
searchHistory?: CacheData<string[]>;
skipConfigs?: CacheData<Record<string, SkipConfig>>;
danmakuFilterConfig?: CacheData<DanmakuFilterConfig>;
@@ -90,6 +93,8 @@ interface UserCacheStore {
// ---- 常量 ----
const PLAY_RECORDS_KEY = 'moontv_play_records';
const FAVORITES_KEY = 'moontv_favorites';
const MANGA_SHELF_KEY = 'moontv_manga_shelf';
const MANGA_HISTORY_KEY = 'moontv_manga_history';
const SEARCH_HISTORY_KEY = 'moontv_search_history';
const MUSIC_PLAY_RECORDS_KEY = 'moontv_music_play_records';
@@ -236,6 +241,14 @@ class HybridCacheManager {
if (cache.favorites && now - cache.favorites.timestamp > maxAge) {
delete cache.favorites;
}
if (cache.mangaShelf && now - cache.mangaShelf.timestamp > maxAge) {
delete cache.mangaShelf;
}
if (cache.mangaReadRecords && now - cache.mangaReadRecords.timestamp > maxAge) {
delete cache.mangaReadRecords;
}
}
/**
@@ -330,6 +343,52 @@ class HybridCacheManager {
this.saveUserCache(username, userCache);
}
getCachedMangaShelf(): Record<string, MangaShelfItem> | null {
const username = this.getCurrentUsername();
if (!username) return null;
const userCache = this.getUserCache(username);
const cached = userCache.mangaShelf;
if (cached && this.isCacheValid(cached)) {
return cached.data;
}
return null;
}
cacheMangaShelf(data: Record<string, MangaShelfItem>): void {
const username = this.getCurrentUsername();
if (!username) return;
const userCache = this.getUserCache(username);
userCache.mangaShelf = this.createCacheData(data);
this.saveUserCache(username, userCache);
}
getCachedMangaReadRecords(): Record<string, MangaReadRecord> | null {
const username = this.getCurrentUsername();
if (!username) return null;
const userCache = this.getUserCache(username);
const cached = userCache.mangaReadRecords;
if (cached && this.isCacheValid(cached)) {
return cached.data;
}
return null;
}
cacheMangaReadRecords(data: Record<string, MangaReadRecord>): void {
const username = this.getCurrentUsername();
if (!username) return;
const userCache = this.getUserCache(username);
userCache.mangaReadRecords = this.createCacheData(data);
this.saveUserCache(username, userCache);
}
/**
* 获取缓存的搜索历史
*/
@@ -503,7 +562,7 @@ const cacheManager = HybridCacheManager.getInstance();
* 立即从数据库刷新对应类型的缓存以保持数据一致性
*/
async function handleDatabaseOperationFailure(
dataType: 'playRecords' | 'favorites' | 'searchHistory',
dataType: 'playRecords' | 'favorites' | 'searchHistory' | 'mangaShelf' | 'mangaHistory',
error: any
): Promise<void> {
console.error(`数据库操作失败 (${dataType}):`, error);
@@ -535,6 +594,16 @@ async function handleDatabaseOperationFailure(
cacheManager.cacheSearchHistory(freshData);
eventName = 'searchHistoryUpdated';
break;
case 'mangaShelf':
freshData = await fetchFromApi<Record<string, MangaShelfItem>>(`/api/manga/shelf`);
cacheManager.cacheMangaShelf(freshData);
eventName = 'mangaShelfUpdated';
break;
case 'mangaHistory':
freshData = await fetchFromApi<Record<string, MangaReadRecord>>(`/api/manga/history`);
cacheManager.cacheMangaReadRecords(freshData);
eventName = 'mangaHistoryUpdated';
break;
}
// 触发更新事件通知组件
@@ -1519,6 +1588,229 @@ export async function clearAllFavorites(): Promise<void> {
);
}
// ---------------- 漫画书架 / 历史 API ----------------
export async function getAllMangaShelf(): Promise<Record<string, MangaShelfItem>> {
if (typeof window === 'undefined') return {};
if (STORAGE_TYPE !== 'localstorage') {
const cachedData = cacheManager.getCachedMangaShelf();
if (cachedData) {
fetchFromApi<Record<string, MangaShelfItem>>('/api/manga/shelf')
.then((freshData) => {
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
cacheManager.cacheMangaShelf(freshData);
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: freshData }));
}
})
.catch((err) => {
console.warn('后台同步漫画书架失败:', err);
});
return cachedData;
}
try {
const freshData = await fetchFromApi<Record<string, MangaShelfItem>>('/api/manga/shelf');
cacheManager.cacheMangaShelf(freshData);
return freshData;
} catch (err) {
console.error('获取漫画书架失败:', err);
triggerGlobalError('获取漫画书架失败');
return {};
}
}
try {
const raw = localStorage.getItem(MANGA_SHELF_KEY);
if (!raw) return {};
return JSON.parse(raw) as Record<string, MangaShelfItem>;
} catch (err) {
console.error('读取漫画书架失败:', err);
triggerGlobalError('读取漫画书架失败');
return {};
}
}
export async function saveMangaShelf(sourceId: string, mangaId: string, item: MangaShelfItem): Promise<void> {
const key = generateStorageKey(sourceId, mangaId);
if (STORAGE_TYPE !== 'localstorage') {
const cached = cacheManager.getCachedMangaShelf() || {};
cached[key] = item;
cacheManager.cacheMangaShelf(cached);
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: cached }));
try {
await fetchWithAuth('/api/manga/shelf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, item }),
});
} catch (err) {
await handleDatabaseOperationFailure('mangaShelf', err);
throw err;
}
return;
}
const allItems = await getAllMangaShelf();
allItems[key] = item;
localStorage.setItem(MANGA_SHELF_KEY, JSON.stringify(allItems));
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: allItems }));
}
export async function deleteMangaShelf(sourceId: string, mangaId: string): Promise<void> {
const key = generateStorageKey(sourceId, mangaId);
if (STORAGE_TYPE !== 'localstorage') {
const cached = cacheManager.getCachedMangaShelf() || {};
delete cached[key];
cacheManager.cacheMangaShelf(cached);
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: cached }));
try {
await fetchWithAuth(`/api/manga/shelf?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
} catch (err) {
await handleDatabaseOperationFailure('mangaShelf', err);
throw err;
}
return;
}
const allItems = await getAllMangaShelf();
delete allItems[key];
localStorage.setItem(MANGA_SHELF_KEY, JSON.stringify(allItems));
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: allItems }));
}
export async function clearAllMangaShelf(): Promise<void> {
if (STORAGE_TYPE !== 'localstorage') {
cacheManager.cacheMangaShelf({});
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: {} }));
try {
await fetchWithAuth('/api/manga/shelf', { method: 'DELETE' });
} catch (err) {
await handleDatabaseOperationFailure('mangaShelf', err);
throw err;
}
return;
}
localStorage.removeItem(MANGA_SHELF_KEY);
window.dispatchEvent(new CustomEvent('mangaShelfUpdated', { detail: {} }));
}
export async function getAllMangaReadRecords(): Promise<Record<string, MangaReadRecord>> {
if (typeof window === 'undefined') return {};
if (STORAGE_TYPE !== 'localstorage') {
const cachedData = cacheManager.getCachedMangaReadRecords();
if (cachedData) {
fetchFromApi<Record<string, MangaReadRecord>>('/api/manga/history')
.then((freshData) => {
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
cacheManager.cacheMangaReadRecords(freshData);
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: freshData }));
}
})
.catch((err) => {
console.warn('后台同步漫画历史失败:', err);
});
return cachedData;
}
try {
const freshData = await fetchFromApi<Record<string, MangaReadRecord>>('/api/manga/history');
cacheManager.cacheMangaReadRecords(freshData);
return freshData;
} catch (err) {
console.error('获取漫画历史失败:', err);
triggerGlobalError('获取漫画历史失败');
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);
triggerGlobalError('读取漫画历史失败');
return {};
}
}
export async function saveMangaReadRecord(sourceId: string, mangaId: string, record: MangaReadRecord): Promise<void> {
const key = generateStorageKey(sourceId, mangaId);
if (STORAGE_TYPE !== 'localstorage') {
const cached = cacheManager.getCachedMangaReadRecords() || {};
cached[key] = record;
cacheManager.cacheMangaReadRecords(cached);
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: cached }));
try {
await fetchWithAuth('/api/manga/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, record }),
});
} catch (err) {
await handleDatabaseOperationFailure('mangaHistory', err);
throw err;
}
return;
}
const allRecords = await getAllMangaReadRecords();
allRecords[key] = record;
localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(allRecords));
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: allRecords }));
}
export async function deleteMangaReadRecord(sourceId: string, mangaId: string): Promise<void> {
const key = generateStorageKey(sourceId, mangaId);
if (STORAGE_TYPE !== 'localstorage') {
const cached = cacheManager.getCachedMangaReadRecords() || {};
delete cached[key];
cacheManager.cacheMangaReadRecords(cached);
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: cached }));
try {
await fetchWithAuth(`/api/manga/history?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
} catch (err) {
await handleDatabaseOperationFailure('mangaHistory', err);
throw err;
}
return;
}
const allRecords = await getAllMangaReadRecords();
delete allRecords[key];
localStorage.setItem(MANGA_HISTORY_KEY, JSON.stringify(allRecords));
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: allRecords }));
}
export async function clearAllMangaReadRecords(): Promise<void> {
if (STORAGE_TYPE !== 'localstorage') {
cacheManager.cacheMangaReadRecords({});
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: {} }));
try {
await fetchWithAuth('/api/manga/history', { method: 'DELETE' });
} catch (err) {
await handleDatabaseOperationFailure('mangaHistory', err);
throw err;
}
return;
}
localStorage.removeItem(MANGA_HISTORY_KEY);
window.dispatchEvent(new CustomEvent('mangaHistoryUpdated', { detail: {} }));
}
// ---------------- 混合缓存辅助函数 ----------------
/**
@@ -1542,10 +1834,12 @@ export async function refreshAllCache(): Promise<void> {
// 使用 Promise 缓存防止并发重复刷新
await cacheManager.getOrCreateRequest('refresh-all-cache', async () => {
// 并行刷新所有数据
const [playRecords, favorites, searchHistory, skipConfigs] =
const [playRecords, favorites, mangaShelf, mangaHistory, searchHistory, skipConfigs] =
await Promise.allSettled([
fetchFromApi<Record<string, PlayRecord>>(`/api/playrecords`),
fetchFromApi<Record<string, Favorite>>(`/api/favorites`),
fetchFromApi<Record<string, MangaShelfItem>>(`/api/manga/shelf`),
fetchFromApi<Record<string, MangaReadRecord>>(`/api/manga/history`),
fetchFromApi<string[]>(`/api/searchhistory`),
fetchFromApi<Record<string, SkipConfig>>(`/api/skipconfigs`),
]);
@@ -1568,6 +1862,24 @@ export async function refreshAllCache(): Promise<void> {
);
}
if (mangaShelf.status === 'fulfilled') {
cacheManager.cacheMangaShelf(mangaShelf.value);
window.dispatchEvent(
new CustomEvent('mangaShelfUpdated', {
detail: mangaShelf.value,
})
);
}
if (mangaHistory.status === 'fulfilled') {
cacheManager.cacheMangaReadRecords(mangaHistory.value);
window.dispatchEvent(
new CustomEvent('mangaHistoryUpdated', {
detail: mangaHistory.value,
})
);
}
if (searchHistory.status === 'fulfilled') {
cacheManager.cacheSearchHistory(searchHistory.value);
window.dispatchEvent(
@@ -1601,6 +1913,8 @@ export function getCacheStatus(): {
hasFavorites: boolean;
hasSearchHistory: boolean;
hasSkipConfigs: boolean;
hasMangaShelf: boolean;
hasMangaHistory: boolean;
username: string | null;
} {
if (STORAGE_TYPE === 'localstorage') {
@@ -1609,6 +1923,8 @@ export function getCacheStatus(): {
hasFavorites: false,
hasSearchHistory: false,
hasSkipConfigs: false,
hasMangaShelf: false,
hasMangaHistory: false,
username: null,
};
}
@@ -1619,6 +1935,8 @@ export function getCacheStatus(): {
hasFavorites: !!cacheManager.getCachedFavorites(),
hasSearchHistory: !!cacheManager.getCachedSearchHistory(),
hasSkipConfigs: !!cacheManager.getCachedSkipConfigs(),
hasMangaShelf: !!cacheManager.getCachedMangaShelf(),
hasMangaHistory: !!cacheManager.getCachedMangaReadRecords(),
username: authInfo?.username || null,
};
}
@@ -1629,7 +1947,9 @@ export type CacheUpdateEvent =
| 'playRecordsUpdated'
| 'favoritesUpdated'
| 'searchHistoryUpdated'
| 'skipConfigsUpdated';
| 'skipConfigsUpdated'
| 'mangaShelfUpdated'
| 'mangaHistoryUpdated';
/**
* 用于 React 组件监听数据更新的事件监听器