diff --git a/frontend/components/ErrorBoundary.js b/frontend/components/ErrorBoundary.js
new file mode 100644
index 0000000..6442b1b
--- /dev/null
+++ b/frontend/components/ErrorBoundary.js
@@ -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 (
+
+
+
+
出错了
+
抱歉,应用遇到了问题。
+ {process.env.NODE_ENV === 'development' && (
+
+ {this.state.error?.toString()}
+
+ )}
+
+
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/frontend/components/LoadingSpinner.js b/frontend/components/LoadingSpinner.js
new file mode 100644
index 0000000..e4ab41f
--- /dev/null
+++ b/frontend/components/LoadingSpinner.js
@@ -0,0 +1,10 @@
+import { Icon } from './icons';
+
+export default function LoadingSpinner({ size = 24, text = '加载中...' }) {
+ return (
+
+
+ {text && {text}}
+
+ );
+}
diff --git a/frontend/pages/_app.js b/frontend/pages/_app.js
index 815a2b3..798f429 100644
--- a/frontend/pages/_app.js
+++ b/frontend/pages/_app.js
@@ -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 (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css
index b406697..7926c48 100644
--- a/frontend/styles/globals.css
+++ b/frontend/styles/globals.css
@@ -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;
+}