diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts index 6710cadb..83ddb7bc 100644 --- a/config/defaultSettings.ts +++ b/config/defaultSettings.ts @@ -4,7 +4,6 @@ import type { ProLayoutProps } from '@ant-design/pro-components'; * @name */ const Settings: ProLayoutProps & { - pwa?: boolean; logo?: string; } = { navTheme: 'light', @@ -16,7 +15,6 @@ const Settings: ProLayoutProps & { fixSiderbar: true, colorWeak: false, title: 'Ant Design Pro', - pwa: true, logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', iconfontUrl: '', token: { diff --git a/config/md-raw-loader.cjs b/config/md-raw-loader.cjs index abb9bbc3..93ed0364 100644 --- a/config/md-raw-loader.cjs +++ b/config/md-raw-loader.cjs @@ -1,4 +1,4 @@ module.exports = function mdRawLoader(source) { this.cacheable?.(); return `export default ${JSON.stringify(String(source))};`; -}; \ No newline at end of file +}; diff --git a/src/app.tsx b/src/app.tsx index 17bf9ecb..aab3ab5e 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -13,8 +13,10 @@ dayjs.extend(relativeTime); import { AvatarDropdown, DocLink, + ErrorBoundary, Footer, LangDropdown, + OfflineBanner, VersionDropdown, } from '@/components'; import { currentUser as queryCurrentUser } from '@/services/ant-design-pro/api'; @@ -147,6 +149,7 @@ export const layout: RunTimeLayoutConfig = ({ // if (initialState?.loading) return ; return ( <> + {children} {container}; +} diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx new file mode 100644 index 00000000..cfbe5b6a --- /dev/null +++ b/src/components/ErrorBoundary/index.tsx @@ -0,0 +1,79 @@ +import { getIntl } from '@umijs/max'; +import { Button, Card, Result } from 'antd'; +import React from 'react'; + +function isChunkLoadError(error: Error): boolean { + return ( + error.name === 'ChunkLoadError' || + /loading (?:css )?chunk/i.test(error.message) || + /imported module/i.test(error.message) + ); +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +export default class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('[ErrorBoundary]', error, info.componentStack); + } + + handleRetry = () => { + if (this.state.error && isChunkLoadError(this.state.error)) { + window.location.reload(); + } else { + this.setState({ hasError: false, error: null }); + } + }; + + render() { + if (!this.state.hasError || !this.state.error) { + return this.props.children; + } + + const { error } = this.state; + const intl = getIntl(); + const isOffline = typeof navigator !== 'undefined' && !navigator.onLine; + const isChunkError = isChunkLoadError(error); + + const title = intl.formatMessage({ + id: isChunkError ? 'app.error.chunk.title' : 'app.error.render.title', + }); + const subTitle = intl.formatMessage({ + id: isChunkError + ? isOffline + ? 'app.error.chunk.description.offline' + : 'app.error.chunk.description.online' + : 'app.error.render.description', + }); + + return ( + + + {intl.formatMessage({ id: 'app.error.retry' })} + , + + + , + ]} + /> + + ); + } +} diff --git a/src/components/OfflineBanner/index.tsx b/src/components/OfflineBanner/index.tsx new file mode 100644 index 00000000..98ee995d --- /dev/null +++ b/src/components/OfflineBanner/index.tsx @@ -0,0 +1,20 @@ +import { useIntl, useModel } from '@umijs/max'; +import { Alert } from 'antd'; + +const OfflineBanner: React.FC = () => { + const { isOnline } = useModel('network'); + const intl = useIntl(); + + if (isOnline) return null; + + return ( + + ); +}; + +export default OfflineBanner; diff --git a/src/components/index.ts b/src/components/index.ts index 94092f55..ff7d559b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -14,6 +14,11 @@ import { AvatarDropdown } from './RightContent/AvatarDropdown'; */ export { default as ArticleListContent } from './ArticleListContent'; export { default as AvatarList } from './AvatarList'; +/** + * 通用组件 + */ +export { default as ErrorBoundary } from './ErrorBoundary'; +export { default as OfflineBanner } from './OfflineBanner'; export { default as StandardFormRow } from './StandardFormRow'; export { default as TagSelect } from './TagSelect'; diff --git a/src/global.tsx b/src/global.tsx index 9c7ac379..195b0524 100644 --- a/src/global.tsx +++ b/src/global.tsx @@ -1,94 +1 @@ import './tailwind.css'; -import { useIntl } from '@umijs/max'; -import { Button, message, notification } from 'antd'; -import defaultSettings from '../config/defaultSettings'; - -const { pwa } = defaultSettings; -const isHttps = document.location.protocol === 'https:'; - -const clearCache = () => { - // remove all caches - if (window.caches) { - caches - .keys() - .then((keys) => { - keys.forEach((key) => { - caches.delete(key); - }); - }) - .catch((e) => console.log(e)); - } -}; - -// if pwa is true -if (pwa) { - // Notify user if offline now - window.addEventListener('sw.offline', () => { - message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' })); - }); - - // Pop up a prompt on the page asking the user if they want to use the latest version - window.addEventListener('sw.updated', (event: Event) => { - const e = event as CustomEvent; - const reloadSW = async () => { - // Check if there is sw whose state is waiting in ServiceWorkerRegistration - // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration - const worker = e.detail?.waiting; - if (!worker) { - return true; - } - // Send skip-waiting event to waiting SW with MessageChannel - await new Promise((resolve, reject) => { - const channel = new MessageChannel(); - channel.port1.onmessage = (msgEvent) => { - if (msgEvent.data.error) { - reject(msgEvent.data.error); - } else { - resolve(msgEvent.data); - } - }; - worker.postMessage({ type: 'skip-waiting' }, [channel.port2]); - }); - - clearCache(); - window.location.reload(); - return true; - }; - const key = `open${Date.now()}`; - const btn = ( - - ); - notification.open({ - title: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }), - description: useIntl().formatMessage({ - id: 'app.pwa.serviceworker.updated.hint', - }), - btn, - key, - onClose: async () => null, - }); - }); -} else if ('serviceWorker' in navigator && isHttps) { - // unregister service worker - const { serviceWorker } = navigator; - if (serviceWorker.getRegistrations) { - serviceWorker.getRegistrations().then((sws) => { - sws.forEach((sw) => { - sw.unregister(); - }); - }); - } - serviceWorker.getRegistration().then((sw) => { - if (sw) sw.unregister(); - }); - - clearCache(); -} diff --git a/src/locales/bn-BD/pwa.ts b/src/locales/bn-BD/pwa.ts index 233fb305..364e8bf7 100644 --- a/src/locales/bn-BD/pwa.ts +++ b/src/locales/bn-BD/pwa.ts @@ -1,7 +1,15 @@ export default { - 'app.pwa.offline': 'আপনি এখন অফলাইন', - 'app.pwa.serviceworker.updated': 'নতুন সামগ্রী উপলব্ধ', - 'app.pwa.serviceworker.updated.hint': - 'বর্তমান পৃষ্ঠাটি পুনরায় লোড করতে দয়া করে "রিফ্রেশ" বোতাম টিপুন', - 'app.pwa.serviceworker.updated.ok': 'রিফ্রেশ', + 'app.network.offline': 'আপনি এখন অফলাইন। কিছু বৈশিষ্ট্য অনুপলব্ধ হতে পারে।', + 'app.error.chunk.title': 'পৃষ্ঠা লোড করতে ব্যর্থ', + 'app.error.chunk.description.offline': + 'আপনার নেটওয়ার্ক সংযোগ বিচ্ছিন্ন হয়েছে। আপনার সংযোগ পরীক্ষা করুন এবং পৃষ্ঠাটি রিফ্রেশ করুন।', + 'app.error.chunk.description.online': + 'পৃষ্ঠা সম্পদ লোড করতে ব্যর্থ। দয়া করে রিফ্রেশ করুন এবং আবার চেষ্টা করুন।', + 'app.error.render.title': 'কিছু ভুল হয়েছে', + 'app.error.render.description': + 'দুঃখিত, এই পৃষ্ঠায় একটি ত্রুটি ঘটেছে। দয়া করে রিফ্রেশ করুন বা হোম পৃষ্ঠায় ফিরে যান।', + 'app.error.retry': 'রিফ্রেশ', + 'app.error.home': 'হোমে ফিরে যান', + 'app.request.offline': + 'নেটওয়ার্ক অনুপলব্ধ। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।', }; diff --git a/src/locales/en-US/pwa.ts b/src/locales/en-US/pwa.ts index 1256c026..998b06c8 100644 --- a/src/locales/en-US/pwa.ts +++ b/src/locales/en-US/pwa.ts @@ -1,7 +1,16 @@ export default { - 'app.pwa.offline': 'You are offline now', - 'app.pwa.serviceworker.updated': 'New content is available', - 'app.pwa.serviceworker.updated.hint': - 'Please press the "Refresh" button to reload current page', - 'app.pwa.serviceworker.updated.ok': 'Refresh', + 'app.network.offline': + 'You are currently offline. Some features may be unavailable.', + 'app.error.chunk.title': 'Failed to load page', + 'app.error.chunk.description.offline': + 'Your network connection has been lost. Please check your connection and refresh.', + 'app.error.chunk.description.online': + 'Page resources failed to load. Please refresh and try again.', + 'app.error.render.title': 'Something went wrong', + 'app.error.render.description': + 'Sorry, an error occurred on this page. Please refresh or go back to the home page.', + 'app.error.retry': 'Refresh', + 'app.error.home': 'Back Home', + 'app.request.offline': + 'Network unavailable. Please check your connection and try again.', }; diff --git a/src/locales/fa-IR/pwa.ts b/src/locales/fa-IR/pwa.ts index 54831b4c..7c2473e1 100644 --- a/src/locales/fa-IR/pwa.ts +++ b/src/locales/fa-IR/pwa.ts @@ -1,7 +1,16 @@ export default { - 'app.pwa.offline': 'شما اکنون آفلاین هستید', - 'app.pwa.serviceworker.updated': 'مطالب جدید در دسترس است', - 'app.pwa.serviceworker.updated.hint': - 'لطفاً برای بارگیری مجدد صفحه فعلی ، دکمه "تازه سازی" را فشار دهید', - 'app.pwa.serviceworker.updated.ok': 'تازه سازی', + 'app.network.offline': + 'شما اکنون آفلاین هستید. برخی از قابلیت‌ها ممکن است در دسترس نباشند.', + 'app.error.chunk.title': 'بارگذاری صفحه ناموفق بود', + 'app.error.chunk.description.offline': + 'اتصال شبکه شما قطع شده است. اتصال خود را بررسی کنید و صفحه را بارگذاری مجدد کنید.', + 'app.error.chunk.description.online': + 'بارگذاری منابع صفحه ناموفق بود. لطفاً صفحه را بارگذاری مجدد کنید.', + 'app.error.render.title': 'خطایی رخ داد', + 'app.error.render.description': + 'متأسفانه، در این صفحه خطایی رخ داد. لطفاً صفحه را بارگذاری مجدد کنید یا به صفحه اصلی بازگردید.', + 'app.error.retry': 'بارگذاری مجدد', + 'app.error.home': 'بازگشت به صفحه اصلی', + 'app.request.offline': + 'شبکه در دسترس نیست. لطفاً اتصال خود را بررسی و دوباره تلاش کنید.', }; diff --git a/src/locales/id-ID/pwa.ts b/src/locales/id-ID/pwa.ts index b2cb8a12..574aad56 100644 --- a/src/locales/id-ID/pwa.ts +++ b/src/locales/id-ID/pwa.ts @@ -1,7 +1,16 @@ export default { - 'app.pwa.offline': 'Koneksi anda terputus', - 'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia', - 'app.pwa.serviceworker.updated.hint': - 'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini', - 'app.pwa.serviceworker.updated.ok': 'Memuat ulang', + 'app.network.offline': + 'Anda sedang offline. Beberapa fitur mungkin tidak tersedia.', + 'app.error.chunk.title': 'Gagal memuat halaman', + 'app.error.chunk.description.offline': + 'Koneksi jaringan terputus. Periksa koneksi Anda dan muat ulang halaman.', + 'app.error.chunk.description.online': + 'Gagal memuat sumber daya halaman. Muat ulang dan coba lagi.', + 'app.error.render.title': 'Terjadi kesalahan', + 'app.error.render.description': + 'Maaf, terjadi kesalahan pada halaman ini. Muat ulang halaman atau kembali ke beranda.', + 'app.error.retry': 'Muat Ulang', + 'app.error.home': 'Kembali ke Beranda', + 'app.request.offline': + 'Jaringan tidak tersedia. Periksa koneksi Anda dan coba lagi.', }; diff --git a/src/locales/ja-JP/pwa.ts b/src/locales/ja-JP/pwa.ts index ace23aef..5d758bff 100644 --- a/src/locales/ja-JP/pwa.ts +++ b/src/locales/ja-JP/pwa.ts @@ -1,7 +1,16 @@ export default { - 'app.pwa.offline': 'あなたは今オフラインです', - 'app.pwa.serviceworker.updated': '新しいコンテンツが利用可能です', - 'app.pwa.serviceworker.updated.hint': - '現在のページをリロードするには、「更新」ボタンを押してください', - 'app.pwa.serviceworker.updated.ok': 'リフレッシュ', + 'app.network.offline': + '現在オフラインです。一部の機能が利用できない場合があります', + 'app.error.chunk.title': 'ページの読み込みに失敗しました', + 'app.error.chunk.description.offline': + 'ネットワーク接続が切断されています。接続を確認してページを再読み込みしてください。', + 'app.error.chunk.description.online': + 'ページリソースの読み込みに失敗しました。ページを再読み込みしてください。', + 'app.error.render.title': 'エラーが発生しました', + 'app.error.render.description': + '申し訳ありません、ページでエラーが発生しました。ページを再読み込みするか、ホームに戻ってください。', + 'app.error.retry': '再読み込み', + 'app.error.home': 'ホームに戻る', + 'app.request.offline': + 'ネットワークに接続できません。接続を確認して再試行してください。', }; diff --git a/src/locales/pt-BR/pwa.ts b/src/locales/pt-BR/pwa.ts index 05cc7978..c531c1c6 100644 --- a/src/locales/pt-BR/pwa.ts +++ b/src/locales/pt-BR/pwa.ts @@ -1,7 +1,16 @@ export default { - 'app.pwa.offline': 'Você está offline agora', - 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', - 'app.pwa.serviceworker.updated.hint': - 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', - 'app.pwa.serviceworker.updated.ok': 'Atualizar', + 'app.network.offline': + 'Você está offline no momento. Alguns recursos podem estar indisponíveis.', + 'app.error.chunk.title': 'Falha ao carregar a página', + 'app.error.chunk.description.offline': + 'Sua conexão de rede foi perdida. Verifique sua conexão e atualize a página.', + 'app.error.chunk.description.online': + 'Falha ao carregar os recursos da página. Atualize e tente novamente.', + 'app.error.render.title': 'Algo deu errado', + 'app.error.render.description': + 'Desculpe, ocorreu um erro nesta página. Atualize a página ou volte para a página inicial.', + 'app.error.retry': 'Atualizar', + 'app.error.home': 'Voltar ao Início', + 'app.request.offline': + 'Rede indisponível. Verifique sua conexão e tente novamente.', }; diff --git a/src/locales/zh-CN/pwa.ts b/src/locales/zh-CN/pwa.ts index e9504849..785da416 100644 --- a/src/locales/zh-CN/pwa.ts +++ b/src/locales/zh-CN/pwa.ts @@ -1,6 +1,13 @@ export default { - 'app.pwa.offline': '当前处于离线状态', - 'app.pwa.serviceworker.updated': '有新内容', - 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', - 'app.pwa.serviceworker.updated.ok': '刷新', + 'app.network.offline': '当前处于离线状态,部分功能可能不可用', + 'app.error.chunk.title': '页面加载失败', + 'app.error.chunk.description.offline': + '网络连接已断开,请检查网络后刷新页面。', + 'app.error.chunk.description.online': '页面资源加载失败,请刷新页面重试。', + 'app.error.render.title': '页面出现错误', + 'app.error.render.description': + '抱歉,页面遇到了一些问题,请刷新页面或返回首页。', + 'app.error.retry': '刷新页面', + 'app.error.home': '返回首页', + 'app.request.offline': '网络不可用,请检查网络连接后重试。', }; diff --git a/src/locales/zh-TW/pwa.ts b/src/locales/zh-TW/pwa.ts index 108a6e48..a4265ea5 100644 --- a/src/locales/zh-TW/pwa.ts +++ b/src/locales/zh-TW/pwa.ts @@ -1,6 +1,14 @@ export default { - 'app.pwa.offline': '當前處於離線狀態', - 'app.pwa.serviceworker.updated': '有新內容', - 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', - 'app.pwa.serviceworker.updated.ok': '刷新', + 'app.network.offline': '當前處於離線狀態,部分功能可能不可用', + 'app.error.chunk.title': '頁面載入失敗', + 'app.error.chunk.description.offline': + '網路連線已中斷,請檢查網路後重新整理頁面。', + 'app.error.chunk.description.online': + '頁面資源載入失敗,請重新整理頁面重試。', + 'app.error.render.title': '頁面出現錯誤', + 'app.error.render.description': + '抱歉,頁面遇到了一些問題,請重新整理頁面或返回首頁。', + 'app.error.retry': '重新整理', + 'app.error.home': '返回首頁', + 'app.request.offline': '網路不可用,請檢查網路連線後重試。', }; diff --git a/src/models/network.ts b/src/models/network.ts new file mode 100644 index 00000000..48925cae --- /dev/null +++ b/src/models/network.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +export default function useNetwork() { + 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); + }; + }, []); + + return { isOnline }; +} diff --git a/src/requestErrorConfig.ts b/src/requestErrorConfig.ts index 56071223..2a8a05ac 100644 --- a/src/requestErrorConfig.ts +++ b/src/requestErrorConfig.ts @@ -77,7 +77,18 @@ export const errorConfig: RequestConfig = { // 请求已经成功发起,但没有收到响应 // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例, // 而在node.js中是 http.ClientRequest 的实例 - message.error('None response! Please retry.'); + if (!navigator.onLine) { + message.error( + 'Network unavailable. Please check your connection and try again.', + ); + } else { + message.error('None response! Please retry.'); + } + } else if (!navigator.onLine) { + // 发送请求时出了点问题(离线) + message.error( + 'Network unavailable. Please check your connection and try again.', + ); } else { // 发送请求时出了点问题 message.error('Request error, please retry.');