feat: add global error indicator

This commit is contained in:
shinya
2025-08-04 23:19:33 +08:00
parent 0bbf2b0a5b
commit c0bba763d1
7 changed files with 193 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ import 'sweetalert2/dist/sweetalert2.min.css';
import { getConfig } from '@/lib/config';
import RuntimeConfig from '@/lib/runtime';
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
import { SiteProvider } from '../components/SiteProvider';
import { ThemeProvider } from '../components/ThemeProvider';
@@ -113,6 +114,7 @@ export default async function RootLayout({
>
<SiteProvider siteName={siteName} announcement={announcement}>
{children}
<GlobalErrorIndicator />
</SiteProvider>
</ThemeProvider>
</body>

View File

@@ -0,0 +1,105 @@
'use client';
import { useEffect, useState } from 'react';
interface ErrorInfo {
id: string;
message: string;
timestamp: number;
}
export function GlobalErrorIndicator() {
const [currentError, setCurrentError] = useState<ErrorInfo | null>(null);
const [isVisible, setIsVisible] = useState(false);
const [isReplacing, setIsReplacing] = useState(false);
useEffect(() => {
// 监听自定义错误事件
const handleError = (event: CustomEvent) => {
const { message } = event.detail;
const newError: ErrorInfo = {
id: Date.now().toString(),
message,
timestamp: Date.now(),
};
// 如果已有错误,开始替换动画
if (currentError) {
setCurrentError(newError);
setIsReplacing(true);
// 动画完成后恢复正常
setTimeout(() => {
setIsReplacing(false);
}, 200);
} else {
// 第一次显示错误
setCurrentError(newError);
}
setIsVisible(true);
};
// 监听错误事件
window.addEventListener('globalError', handleError as EventListener);
return () => {
window.removeEventListener('globalError', handleError as EventListener);
};
}, [currentError]);
const handleClose = () => {
setIsVisible(false);
setCurrentError(null);
setIsReplacing(false);
};
if (!isVisible || !currentError) {
return null;
}
return (
<div className='fixed top-4 right-4 z-[2000]'>
{/* 错误卡片 */}
<div
className={`bg-red-500 text-white px-4 py-3 rounded-lg shadow-lg flex items-center justify-between min-w-[300px] max-w-[400px] transition-all duration-300 ${
isReplacing ? 'scale-105 bg-red-400' : 'scale-100 bg-red-500'
} animate-fade-in`}
>
<span className='text-sm font-medium flex-1 mr-3'>
{currentError.message}
</span>
<button
onClick={handleClose}
className='text-white hover:text-red-100 transition-colors flex-shrink-0'
aria-label='关闭错误提示'
>
<svg
className='w-5 h-5'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
</div>
);
}
// 全局错误触发函数
export function triggerGlobalError(message: string) {
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message },
})
);
}
}

View File

@@ -17,6 +17,17 @@
import { getAuthInfoFromBrowserCookie } from './auth';
import { SkipConfig } from './types';
// 全局错误触发函数
function triggerGlobalError(message: string) {
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message },
})
);
}
}
// ---- 类型 ----
export interface PlayRecord {
title: string;
@@ -346,6 +357,7 @@ async function handleDatabaseOperationFailure(
error: any
): Promise<void> {
console.error(`数据库操作失败 (${dataType}):`, error);
triggerGlobalError(`数据库操作失败`);
try {
let freshData: any;
@@ -381,6 +393,7 @@ async function handleDatabaseOperationFailure(
);
} catch (refreshErr) {
console.error(`刷新${dataType}缓存失败:`, refreshErr);
triggerGlobalError(`刷新${dataType}缓存失败`);
}
}
@@ -458,6 +471,7 @@ export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
})
.catch((err) => {
console.warn('后台同步播放记录失败:', err);
triggerGlobalError('后台同步播放记录失败');
});
return cachedData;
@@ -471,6 +485,7 @@ export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
return freshData;
} catch (err) {
console.error('获取播放记录失败:', err);
triggerGlobalError('获取播放记录失败');
return {};
}
}
@@ -483,6 +498,7 @@ export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
return JSON.parse(raw) as Record<string, PlayRecord>;
} catch (err) {
console.error('读取播放记录失败:', err);
triggerGlobalError('读取播放记录失败');
return {};
}
}
@@ -523,6 +539,7 @@ export async function savePlayRecord(
});
} catch (err) {
await handleDatabaseOperationFailure('playRecords', err);
triggerGlobalError('保存播放记录失败');
throw err;
}
return;
@@ -545,6 +562,7 @@ export async function savePlayRecord(
);
} catch (err) {
console.error('保存播放记录失败:', err);
triggerGlobalError('保存播放记录失败');
throw err;
}
}
@@ -580,6 +598,7 @@ export async function deletePlayRecord(
});
} catch (err) {
await handleDatabaseOperationFailure('playRecords', err);
triggerGlobalError('删除播放记录失败');
throw err;
}
return;
@@ -602,6 +621,7 @@ export async function deletePlayRecord(
);
} catch (err) {
console.error('删除播放记录失败:', err);
triggerGlobalError('删除播放记录失败');
throw err;
}
}
@@ -640,6 +660,7 @@ export async function getSearchHistory(): Promise<string[]> {
})
.catch((err) => {
console.warn('后台同步搜索历史失败:', err);
triggerGlobalError('后台同步搜索历史失败');
});
return cachedData;
@@ -651,6 +672,7 @@ export async function getSearchHistory(): Promise<string[]> {
return freshData;
} catch (err) {
console.error('获取搜索历史失败:', err);
triggerGlobalError('获取搜索历史失败');
return [];
}
}
@@ -665,6 +687,7 @@ export async function getSearchHistory(): Promise<string[]> {
return Array.isArray(arr) ? arr : [];
} catch (err) {
console.error('读取搜索历史失败:', err);
triggerGlobalError('读取搜索历史失败');
return [];
}
}
@@ -728,6 +751,7 @@ export async function addSearchHistory(keyword: string): Promise<void> {
);
} catch (err) {
console.error('保存搜索历史失败:', err);
triggerGlobalError('保存搜索历史失败');
}
}
@@ -819,6 +843,7 @@ export async function deleteSearchHistory(keyword: string): Promise<void> {
);
} catch (err) {
console.error('删除搜索历史失败:', err);
triggerGlobalError('删除搜索历史失败');
}
}
@@ -856,6 +881,7 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
})
.catch((err) => {
console.warn('后台同步收藏失败:', err);
triggerGlobalError('后台同步收藏失败');
});
return cachedData;
@@ -869,6 +895,7 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
return freshData;
} catch (err) {
console.error('获取收藏失败:', err);
triggerGlobalError('获取收藏失败');
return {};
}
}
@@ -881,6 +908,7 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
return JSON.parse(raw) as Record<string, Favorite>;
} catch (err) {
console.error('读取收藏失败:', err);
triggerGlobalError('读取收藏失败');
return {};
}
}
@@ -921,6 +949,7 @@ export async function saveFavorite(
});
} catch (err) {
await handleDatabaseOperationFailure('favorites', err);
triggerGlobalError('保存收藏失败');
throw err;
}
return;
@@ -943,6 +972,7 @@ export async function saveFavorite(
);
} catch (err) {
console.error('保存收藏失败:', err);
triggerGlobalError('保存收藏失败');
throw err;
}
}
@@ -978,6 +1008,7 @@ export async function deleteFavorite(
});
} catch (err) {
await handleDatabaseOperationFailure('favorites', err);
triggerGlobalError('删除收藏失败');
throw err;
}
return;
@@ -1000,6 +1031,7 @@ export async function deleteFavorite(
);
} catch (err) {
console.error('删除收藏失败:', err);
triggerGlobalError('删除收藏失败');
throw err;
}
}
@@ -1035,6 +1067,7 @@ export async function isFavorited(
})
.catch((err) => {
console.warn('后台同步收藏失败:', err);
triggerGlobalError('后台同步收藏失败');
});
return !!cachedFavorites[key];
@@ -1048,6 +1081,7 @@ export async function isFavorited(
return !!freshData[key];
} catch (err) {
console.error('检查收藏状态失败:', err);
triggerGlobalError('检查收藏状态失败');
return false;
}
}
@@ -1083,6 +1117,7 @@ export async function clearAllPlayRecords(): Promise<void> {
});
} catch (err) {
await handleDatabaseOperationFailure('playRecords', err);
triggerGlobalError('清空播放记录失败');
throw err;
}
return;
@@ -1123,6 +1158,7 @@ export async function clearAllFavorites(): Promise<void> {
});
} catch (err) {
await handleDatabaseOperationFailure('favorites', err);
triggerGlobalError('清空收藏失败');
throw err;
}
return;
@@ -1204,6 +1240,7 @@ export async function refreshAllCache(): Promise<void> {
}
} catch (err) {
console.error('刷新缓存失败:', err);
triggerGlobalError('刷新缓存失败');
}
}
@@ -1297,6 +1334,7 @@ export async function preloadUserData(): Promise<void> {
// 后台静默预加载,不阻塞界面
refreshAllCache().catch((err) => {
console.warn('预加载用户数据失败:', err);
triggerGlobalError('预加载用户数据失败');
});
}
@@ -1352,6 +1390,7 @@ export async function getSkipConfig(
return freshData[key] || null;
} catch (err) {
console.error('获取跳过片头片尾配置失败:', err);
triggerGlobalError('获取跳过片头片尾配置失败');
return null;
}
}
@@ -1365,6 +1404,7 @@ export async function getSkipConfig(
return configs[key] || null;
} catch (err) {
console.error('读取跳过片头片尾配置失败:', err);
triggerGlobalError('读取跳过片头片尾配置失败');
return null;
}
}
@@ -1405,6 +1445,7 @@ export async function saveSkipConfig(
});
} catch (err) {
console.error('保存跳过片头片尾配置失败:', err);
triggerGlobalError('保存跳过片头片尾配置失败');
}
return;
}
@@ -1427,6 +1468,7 @@ export async function saveSkipConfig(
);
} catch (err) {
console.error('保存跳过片头片尾配置失败:', err);
triggerGlobalError('保存跳过片头片尾配置失败');
throw err;
}
}
@@ -1463,6 +1505,7 @@ export async function getAllSkipConfigs(): Promise<Record<string, SkipConfig>> {
})
.catch((err) => {
console.warn('后台同步跳过片头片尾配置失败:', err);
triggerGlobalError('后台同步跳过片头片尾配置失败');
});
return cachedData;
@@ -1476,6 +1519,7 @@ export async function getAllSkipConfigs(): Promise<Record<string, SkipConfig>> {
return freshData;
} catch (err) {
console.error('获取跳过片头片尾配置失败:', err);
triggerGlobalError('获取跳过片头片尾配置失败');
return {};
}
}
@@ -1488,6 +1532,7 @@ export async function getAllSkipConfigs(): Promise<Record<string, SkipConfig>> {
return JSON.parse(raw) as Record<string, SkipConfig>;
} catch (err) {
console.error('读取跳过片头片尾配置失败:', err);
triggerGlobalError('读取跳过片头片尾配置失败');
return {};
}
}
@@ -1523,6 +1568,7 @@ export async function deleteSkipConfig(
});
} catch (err) {
console.error('删除跳过片头片尾配置失败:', err);
triggerGlobalError('删除跳过片头片尾配置失败');
}
return;
}
@@ -1547,6 +1593,7 @@ export async function deleteSkipConfig(
}
} catch (err) {
console.error('删除跳过片头片尾配置失败:', err);
triggerGlobalError('删除跳过片头片尾配置失败');
throw err;
}
}

View File

@@ -119,6 +119,14 @@ export async function fetchDoubanCategories(
list: list,
};
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣分类数据失败' },
})
);
}
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}
@@ -140,6 +148,14 @@ export async function getDoubanCategories(
);
if (!response.ok) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣分类数据失败' },
})
);
}
throw new Error('获取豆瓣分类数据失败');
}
@@ -167,6 +183,14 @@ export async function getDoubanList(
);
if (!response.ok) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣列表数据失败' },
})
);
}
throw new Error('获取豆瓣列表数据失败');
}
@@ -222,6 +246,14 @@ export async function fetchDoubanList(
list: list,
};
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣列表数据失败' },
})
);
}
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}

View File

@@ -2,7 +2,7 @@
'use client';
const CURRENT_VERSION = '20250804223856';
const CURRENT_VERSION = '20250804231933';
// 版本检查结果枚举
export enum UpdateStatus {