mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-27 23:34:48 +08:00
feat: add global error indicator
This commit is contained in:
@@ -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>
|
||||
|
||||
105
src/components/GlobalErrorIndicator.tsx
Normal file
105
src/components/GlobalErrorIndicator.tsx
Normal 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 },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
const CURRENT_VERSION = '20250804223856';
|
||||
const CURRENT_VERSION = '20250804231933';
|
||||
|
||||
// 版本检查结果枚举
|
||||
export enum UpdateStatus {
|
||||
|
||||
Reference in New Issue
Block a user