Skip to content

Commit c0bba76

Browse files
committed
feat: add global error indicator
1 parent 0bbf2b0 commit c0bba76

File tree

7 files changed

+193
-2
lines changed

7 files changed

+193
-2
lines changed

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20250804223856
1+
20250804231933

src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'sweetalert2/dist/sweetalert2.min.css';
99
import { getConfig } from '@/lib/config';
1010
import RuntimeConfig from '@/lib/runtime';
1111

12+
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
1213
import { SiteProvider } from '../components/SiteProvider';
1314
import { ThemeProvider } from '../components/ThemeProvider';
1415

@@ -113,6 +114,7 @@ export default async function RootLayout({
113114
>
114115
<SiteProvider siteName={siteName} announcement={announcement}>
115116
{children}
117+
<GlobalErrorIndicator />
116118
</SiteProvider>
117119
</ThemeProvider>
118120
</body>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
interface ErrorInfo {
6+
id: string;
7+
message: string;
8+
timestamp: number;
9+
}
10+
11+
export function GlobalErrorIndicator() {
12+
const [currentError, setCurrentError] = useState<ErrorInfo | null>(null);
13+
const [isVisible, setIsVisible] = useState(false);
14+
const [isReplacing, setIsReplacing] = useState(false);
15+
16+
useEffect(() => {
17+
// 监听自定义错误事件
18+
const handleError = (event: CustomEvent) => {
19+
const { message } = event.detail;
20+
const newError: ErrorInfo = {
21+
id: Date.now().toString(),
22+
message,
23+
timestamp: Date.now(),
24+
};
25+
26+
// 如果已有错误,开始替换动画
27+
if (currentError) {
28+
setCurrentError(newError);
29+
setIsReplacing(true);
30+
31+
// 动画完成后恢复正常
32+
setTimeout(() => {
33+
setIsReplacing(false);
34+
}, 200);
35+
} else {
36+
// 第一次显示错误
37+
setCurrentError(newError);
38+
}
39+
40+
setIsVisible(true);
41+
};
42+
43+
// 监听错误事件
44+
window.addEventListener('globalError', handleError as EventListener);
45+
46+
return () => {
47+
window.removeEventListener('globalError', handleError as EventListener);
48+
};
49+
}, [currentError]);
50+
51+
const handleClose = () => {
52+
setIsVisible(false);
53+
setCurrentError(null);
54+
setIsReplacing(false);
55+
};
56+
57+
if (!isVisible || !currentError) {
58+
return null;
59+
}
60+
61+
return (
62+
<div className='fixed top-4 right-4 z-[2000]'>
63+
{/* 错误卡片 */}
64+
<div
65+
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 ${
66+
isReplacing ? 'scale-105 bg-red-400' : 'scale-100 bg-red-500'
67+
} animate-fade-in`}
68+
>
69+
<span className='text-sm font-medium flex-1 mr-3'>
70+
{currentError.message}
71+
</span>
72+
<button
73+
onClick={handleClose}
74+
className='text-white hover:text-red-100 transition-colors flex-shrink-0'
75+
aria-label='关闭错误提示'
76+
>
77+
<svg
78+
className='w-5 h-5'
79+
fill='none'
80+
stroke='currentColor'
81+
viewBox='0 0 24 24'
82+
>
83+
<path
84+
strokeLinecap='round'
85+
strokeLinejoin='round'
86+
strokeWidth={2}
87+
d='M6 18L18 6M6 6l12 12'
88+
/>
89+
</svg>
90+
</button>
91+
</div>
92+
</div>
93+
);
94+
}
95+
96+
// 全局错误触发函数
97+
export function triggerGlobalError(message: string) {
98+
if (typeof window !== 'undefined') {
99+
window.dispatchEvent(
100+
new CustomEvent('globalError', {
101+
detail: { message },
102+
})
103+
);
104+
}
105+
}

0 commit comments

Comments
 (0)