diff --git a/src/app.tsx b/src/app.tsx
index de16ba25..ca1a645b 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -14,6 +14,7 @@ import {
AvatarDropdown,
DocLink,
ErrorBoundary,
+ ErrorBoundaryClass,
Footer,
LangDropdown,
OfflineBanner,
@@ -143,7 +144,7 @@ export const layout: RunTimeLayoutConfig = ({
: [],
// Replace ProLayout's default ErrorBoundary with our offline-aware version,
// so chunk load errors show friendly messages instead of "Something went wrong."
- ErrorBoundary,
+ ErrorBoundary: ErrorBoundaryClass,
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible:
unAccessible
,
diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx
index d87b68e8..19096a7f 100644
--- a/src/components/ErrorBoundary/index.tsx
+++ b/src/components/ErrorBoundary/index.tsx
@@ -1,4 +1,4 @@
-import { getIntl } from '@umijs/max';
+import { getIntl, useModel } from '@umijs/max';
import { Button, Result } from 'antd';
import React from 'react';
@@ -10,42 +10,118 @@ function isChunkLoadError(error: Error): boolean {
);
}
+function getSubTitleId(isChunkError: boolean, isOffline: boolean): string {
+ if (!isChunkError) return 'app.error.render.description';
+ return isOffline
+ ? 'app.error.chunk.description.offline'
+ : 'app.error.chunk.description.online';
+}
+
+function renderErrorFallback(
+ error: Error,
+ isOnline: boolean,
+ onRetry: () => void,
+) {
+ const intl = getIntl();
+ const isOffline = !isOnline;
+ const isChunkError = isChunkLoadError(error);
+
+ return (
+
+ {intl.formatMessage({
+ id: 'app.error.retry',
+ defaultMessage: 'Refresh',
+ })}
+ ,
+ ,
+ ]}
+ />
+ );
+}
+
+interface ErrorBoundaryProps {
+ children: React.ReactNode;
+ /** When provided, skips internal online/offline listeners and uses this value. */
+ isOnline?: boolean;
+}
+
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
- isOnline: boolean;
}
-export default class ErrorBoundary extends React.Component<
- { children: React.ReactNode },
+/**
+ * Class-based error boundary with offline-aware error messages.
+ * Accepts optional `isOnline` prop; falls back to own navigator.onLine tracking when unset.
+ */
+export class ErrorBoundaryClass extends React.Component<
+ ErrorBoundaryProps,
ErrorBoundaryState
> {
- state: ErrorBoundaryState = {
- hasError: false,
- error: null,
- isOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,
- };
+ state: ErrorBoundaryState = { hasError: false, error: null };
+ private ownOnline =
+ typeof navigator !== 'undefined' ? navigator.onLine : true;
static getDerivedStateFromError(error: Error): Partial {
return { hasError: true, error };
}
componentDidMount() {
- window.addEventListener('online', this.handleOnline);
- window.addEventListener('offline', this.handleOffline);
+ if (this.props.isOnline === undefined) {
+ window.addEventListener('online', this.handleOnline);
+ window.addEventListener('offline', this.handleOffline);
+ }
+ }
+
+ componentDidUpdate(prev: ErrorBoundaryProps) {
+ // Auto-reload on recovery when isOnline prop transitions to true
+ if (
+ this.props.isOnline === true &&
+ prev.isOnline === false &&
+ this.state.hasError
+ ) {
+ window.location.reload();
+ }
}
componentWillUnmount() {
- window.removeEventListener('online', this.handleOnline);
- window.removeEventListener('offline', this.handleOffline);
+ if (this.props.isOnline === undefined) {
+ window.removeEventListener('online', this.handleOnline);
+ window.removeEventListener('offline', this.handleOffline);
+ }
}
handleOnline = () => {
- this.setState({ isOnline: true });
+ this.ownOnline = true;
if (this.state.hasError) window.location.reload();
};
- handleOffline = () => this.setState({ isOnline: false });
+ handleOffline = () => {
+ this.ownOnline = false;
+ };
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('[ErrorBoundary]', error, info.componentStack);
@@ -59,55 +135,29 @@ export default class ErrorBoundary extends React.Component<
}
};
- render() {
- if (!this.state.hasError || !this.state.error) {
- return this.props.children;
- }
+ getIsOnline(): boolean {
+ return this.props.isOnline ?? this.ownOnline;
+ }
- const { error } = this.state;
- const intl = getIntl();
- const isOffline = !this.state.isOnline;
- const isChunkError = isChunkLoadError(error);
-
- const subTitleId = isChunkError
- ? isOffline
- ? 'app.error.chunk.description.offline'
- : 'app.error.chunk.description.online'
- : 'app.error.render.description';
-
- return (
-
- {intl.formatMessage({
- id: 'app.error.retry',
- defaultMessage: 'Refresh',
- })}
- ,
- ,
- ]}
- />
+ render() {
+ if (!this.state.hasError || !this.state.error) return this.props.children;
+ return renderErrorFallback(
+ this.state.error,
+ this.getIsOnline(),
+ this.handleRetry,
);
}
}
+
+/** Functional wrapper providing network state via useModel('network'). */
+const ErrorBoundary: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const { isOnline } = useModel('network');
+
+ return (
+ {children}
+ );
+};
+
+export default ErrorBoundary;
diff --git a/src/components/OfflineBanner/index.tsx b/src/components/OfflineBanner/index.tsx
index 8548d97e..66ed1eec 100644
--- a/src/components/OfflineBanner/index.tsx
+++ b/src/components/OfflineBanner/index.tsx
@@ -1,22 +1,8 @@
-import { getIntl } from '@umijs/max';
+import { getIntl, useModel } from '@umijs/max';
import { Alert } from 'antd';
-import { useEffect, useState } from 'react';
const OfflineBanner: React.FC = () => {
- const [isOnline, setIsOnline] = useState(
- typeof navigator !== 'undefined' ? navigator.onLine : true,
- );
-
- useEffect(() => {
- const handleOnline = () => setIsOnline(true);
- const handleOffline = () => setIsOnline(false);
- window.addEventListener('online', handleOnline);
- window.addEventListener('offline', handleOffline);
- return () => {
- window.removeEventListener('online', handleOnline);
- window.removeEventListener('offline', handleOffline);
- };
- }, []);
+ const { isOnline } = useModel('network');
if (isOnline) return null;
@@ -25,7 +11,7 @@ const OfflineBanner: React.FC = () => {
type="warning"
banner
closable={false}
- title={getIntl().formatMessage({
+ message={getIntl().formatMessage({
id: 'app.network.offline',
defaultMessage:
'You are currently offline. Some features may be unavailable.',
diff --git a/src/components/index.ts b/src/components/index.ts
index ff7d559b..62095eaa 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -17,7 +17,7 @@ export { default as AvatarList } from './AvatarList';
/**
* 通用组件
*/
-export { default as ErrorBoundary } from './ErrorBoundary';
+export { default as ErrorBoundary, ErrorBoundaryClass } from './ErrorBoundary';
export { default as OfflineBanner } from './OfflineBanner';
export { default as StandardFormRow } from './StandardFormRow';
export { default as TagSelect } from './TagSelect';
diff --git a/src/requestErrorConfig.ts b/src/requestErrorConfig.ts
index e081bba8..e1736b97 100644
--- a/src/requestErrorConfig.ts
+++ b/src/requestErrorConfig.ts
@@ -74,21 +74,6 @@ export const errorConfig: RequestConfig = {
// Axios 的错误
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
message.error(`Response status:${error.response.status}`);
- } else if (error.request) {
- // 请求已经成功发起,但没有收到响应
- // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
- // 而在node.js中是 http.ClientRequest 的实例
- if (typeof navigator !== 'undefined' && !navigator.onLine) {
- message.error(
- getIntl().formatMessage({
- id: 'app.request.offline',
- defaultMessage:
- 'Network unavailable. Please check your connection and try again.',
- }),
- );
- } else {
- message.error('None response! Please retry.');
- }
} else if (typeof navigator !== 'undefined' && !navigator.onLine) {
message.error(
getIntl().formatMessage({
@@ -97,8 +82,9 @@ export const errorConfig: RequestConfig = {
'Network unavailable. Please check your connection and try again.',
}),
);
+ } else if (error.request) {
+ message.error('None response! Please retry.');
} else {
- // 发送请求时出了点问题
message.error('Request error, please retry.');
}
},