feat: add error boundary and loading spinner components with SWR integration

This commit is contained in:
Developer
2026-03-18 16:18:19 +08:00
parent 1f51c330dd
commit 5dd00c310f
4 changed files with 137 additions and 6 deletions
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { Icon } from './icons';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught error:', error, errorInfo);
}
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<div className="error-boundary__content">
<Icon name="info" size={48} />
<h2>出错了</h2>
<p>抱歉应用遇到了问题</p>
{process.env.NODE_ENV === 'development' && (
<pre className="error-boundary__details">
{this.state.error?.toString()}
</pre>
)}
<div className="error-boundary__actions">
<button onClick={this.handleGoHome} className="btn btn-secondary">
返回首页
</button>
<button onClick={this.handleReload} className="btn btn-primary">
重新加载
</button>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+10
View File
@@ -0,0 +1,10 @@
import { Icon } from './icons';
export default function LoadingSpinner({ size = 24, text = '加载中...' }) {
return (
<div className="loading-spinner">
<Icon name="spinner" size={size} className="is-spinning" />
{text && <span>{text}</span>}
</div>
);
}
+8 -1
View File
@@ -1,13 +1,20 @@
import '../styles/globals.css';
import { SWRConfig } from 'swr';
import ErrorBoundary from '../components/ErrorBoundary';
import { ToastProvider } from '../components/ToastProvider';
import { ConfirmProvider } from '../components/ConfirmProvider';
import { swrConfig } from '../lib/swr-config';
import '../styles/globals.css';
export default function App({ Component, pageProps }) {
return (
<ErrorBoundary>
<SWRConfig value={swrConfig}>
<ConfirmProvider>
<ToastProvider>
<Component {...pageProps} />
</ToastProvider>
</ConfirmProvider>
</SWRConfig>
</ErrorBoundary>
);
}
+58
View File
@@ -2136,3 +2136,61 @@ textarea:focus-visible {
min-height: 54px;
}
}
/* Error Boundary */
.error-boundary {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.error-boundary__content {
text-align: center;
max-width: 400px;
}
.error-boundary__content h2 {
margin: 16px 0 8px;
color: #1e293b;
}
.error-boundary__content p {
color: #64748b;
margin-bottom: 24px;
}
.error-boundary__details {
background: #f1f5f9;
padding: 12px;
border-radius: 8px;
font-size: 12px;
color: #64748b;
margin-bottom: 24px;
overflow-x: auto;
}
.error-boundary__actions {
display: flex;
gap: 12px;
justify-content: center;
}
/* Loading Spinner */
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px;
color: #64748b;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.is-spinning {
animation: spin 1s linear infinite;
}