From c0bba763d16fb5cd830a808d58329025337491cf Mon Sep 17 00:00:00 2001 From: shinya Date: Mon, 4 Aug 2025 23:19:33 +0800 Subject: [PATCH] feat: add global error indicator --- VERSION.txt | 2 +- src/app/layout.tsx | 2 + src/components/GlobalErrorIndicator.tsx | 105 ++++++++++++++++++++++++ src/lib/db.client.ts | 47 +++++++++++ src/lib/douban.client.ts | 32 ++++++++ src/lib/version.ts | 2 +- tailwind.config.ts | 5 ++ 7 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/components/GlobalErrorIndicator.tsx diff --git a/VERSION.txt b/VERSION.txt index 086a0ad..ef046ac 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -20250804223856 \ No newline at end of file +20250804231933 \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 03ef796..f1e3f72 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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({ > {children} + diff --git a/src/components/GlobalErrorIndicator.tsx b/src/components/GlobalErrorIndicator.tsx new file mode 100644 index 0000000..f05bb67 --- /dev/null +++ b/src/components/GlobalErrorIndicator.tsx @@ -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(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 ( +
+ {/* 错误卡片 */} +
+ + {currentError.message} + + +
+
+ ); +} + +// 全局错误触发函数 +export function triggerGlobalError(message: string) { + if (typeof window !== 'undefined') { + window.dispatchEvent( + new CustomEvent('globalError', { + detail: { message }, + }) + ); + } +} diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index 335d847..76ebdcd 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -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 { 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> { }) .catch((err) => { console.warn('后台同步播放记录失败:', err); + triggerGlobalError('后台同步播放记录失败'); }); return cachedData; @@ -471,6 +485,7 @@ export async function getAllPlayRecords(): Promise> { return freshData; } catch (err) { console.error('获取播放记录失败:', err); + triggerGlobalError('获取播放记录失败'); return {}; } } @@ -483,6 +498,7 @@ export async function getAllPlayRecords(): Promise> { return JSON.parse(raw) as Record; } 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 { }) .catch((err) => { console.warn('后台同步搜索历史失败:', err); + triggerGlobalError('后台同步搜索历史失败'); }); return cachedData; @@ -651,6 +672,7 @@ export async function getSearchHistory(): Promise { return freshData; } catch (err) { console.error('获取搜索历史失败:', err); + triggerGlobalError('获取搜索历史失败'); return []; } } @@ -665,6 +687,7 @@ export async function getSearchHistory(): Promise { return Array.isArray(arr) ? arr : []; } catch (err) { console.error('读取搜索历史失败:', err); + triggerGlobalError('读取搜索历史失败'); return []; } } @@ -728,6 +751,7 @@ export async function addSearchHistory(keyword: string): Promise { ); } catch (err) { console.error('保存搜索历史失败:', err); + triggerGlobalError('保存搜索历史失败'); } } @@ -819,6 +843,7 @@ export async function deleteSearchHistory(keyword: string): Promise { ); } catch (err) { console.error('删除搜索历史失败:', err); + triggerGlobalError('删除搜索历史失败'); } } @@ -856,6 +881,7 @@ export async function getAllFavorites(): Promise> { }) .catch((err) => { console.warn('后台同步收藏失败:', err); + triggerGlobalError('后台同步收藏失败'); }); return cachedData; @@ -869,6 +895,7 @@ export async function getAllFavorites(): Promise> { return freshData; } catch (err) { console.error('获取收藏失败:', err); + triggerGlobalError('获取收藏失败'); return {}; } } @@ -881,6 +908,7 @@ export async function getAllFavorites(): Promise> { return JSON.parse(raw) as Record; } 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 { }); } catch (err) { await handleDatabaseOperationFailure('playRecords', err); + triggerGlobalError('清空播放记录失败'); throw err; } return; @@ -1123,6 +1158,7 @@ export async function clearAllFavorites(): Promise { }); } catch (err) { await handleDatabaseOperationFailure('favorites', err); + triggerGlobalError('清空收藏失败'); throw err; } return; @@ -1204,6 +1240,7 @@ export async function refreshAllCache(): Promise { } } catch (err) { console.error('刷新缓存失败:', err); + triggerGlobalError('刷新缓存失败'); } } @@ -1297,6 +1334,7 @@ export async function preloadUserData(): Promise { // 后台静默预加载,不阻塞界面 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> { }) .catch((err) => { console.warn('后台同步跳过片头片尾配置失败:', err); + triggerGlobalError('后台同步跳过片头片尾配置失败'); }); return cachedData; @@ -1476,6 +1519,7 @@ export async function getAllSkipConfigs(): Promise> { return freshData; } catch (err) { console.error('获取跳过片头片尾配置失败:', err); + triggerGlobalError('获取跳过片头片尾配置失败'); return {}; } } @@ -1488,6 +1532,7 @@ export async function getAllSkipConfigs(): Promise> { return JSON.parse(raw) as Record; } 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; } } diff --git a/src/lib/douban.client.ts b/src/lib/douban.client.ts index b7fa953..bdc1ee1 100644 --- a/src/lib/douban.client.ts +++ b/src/lib/douban.client.ts @@ -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}`); } } diff --git a/src/lib/version.ts b/src/lib/version.ts index 4212f29..ff14020 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -2,7 +2,7 @@ 'use client'; -const CURRENT_VERSION = '20250804223856'; +const CURRENT_VERSION = '20250804231933'; // 版本检查结果枚举 export enum UpdateStatus { diff --git a/tailwind.config.ts b/tailwind.config.ts index 2362275..6a68fdd 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -65,6 +65,10 @@ const config: Config = { '0%': { transform: 'translateY(-10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' }, }, + slideInFromRight: { + '0%': { transform: 'translateX(100%)', opacity: '0' }, + '100%': { transform: 'translateX(0)', opacity: '1' }, + }, }, animation: { flicker: 'flicker 3s linear infinite', @@ -72,6 +76,7 @@ const config: Config = { 'fade-in': 'fadeIn 0.3s ease-in-out', 'slide-up': 'slideUp 0.3s ease-in-out', 'slide-down': 'slideDown 0.3s ease-in-out', + 'slide-in-from-right': 'slideInFromRight 0.3s ease-out', }, backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',