feat: error boundary (#9462)

* build: 添加 react-error-boundary 依赖

添加 react-error-boundary 包以增强 React 应用的错误处理能力

* feat(组件): 添加ErrorBoundary组件用于错误边界处理

* feat(home): 为HomeTabs和Chat组件添加错误边界处理

* refactor(ErrorBoundary): 移除多余的ErrorContainer包装并优化结构

* feat(ErrorBoundary): 添加重新加载按钮并优化错误边界样式

添加重新加载功能按钮,方便用户快速恢复应用
调整错误边界容器的布局样式,使其居中显示

* style(ErrorBoundary): 移除ErrorContainer的固定高度以改善布局灵活性

* test(ErrorBoundary): 添加测试错误边界组件的功能按钮

添加一个用于测试错误边界组件功能的按钮组件,该按钮点击后会抛出错误以验证错误边界是否正常工作。此组件仅用于测试,合并前需要删除。

* feat(路由): 为路由组件添加错误边界处理

在Router组件中包裹ErrorBoundary以捕获并处理子组件中的错误

* fix(ErrorBoundary): 修复错误边界中翻译键的拼写错误

* feat(i18n): 添加边界错误处理和主题不存在错误的多语言支持

* refactor(ErrorBoundary): 移除用于测试的ThrowError组件
This commit is contained in:
Phantom
2025-08-24 18:49:14 +08:00
committed by GitHub
parent 6d102ccef8
commit 107c01913d
14 changed files with 178 additions and 25 deletions
+15 -12
View File
@@ -4,6 +4,7 @@ import { FC, useMemo } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import Sidebar from './components/app/Sidebar'
import { ErrorBoundary } from './components/ErrorBoundary'
import TabsContainer from './components/Tab/TabContainer'
import NavigationHandler from './handler/NavigationHandler'
import { useNavbarPosition } from './hooks/useSettings'
@@ -23,18 +24,20 @@ const Router: FC = () => {
const routes = useMemo(() => {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/code" element={<CodeToolsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
<ErrorBoundary>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/code" element={<CodeToolsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
</ErrorBoundary>
)
}, [])
@@ -0,0 +1,57 @@
import { formatErrorMessage } from '@renderer/utils/error'
import { Alert, Button, Space } from 'antd'
import { ComponentType, ReactNode } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): ReactNode => {
const { t } = useTranslation()
const { error } = props
const debug = async () => {
await window.api.devTools.toggle()
}
const reload = async () => {
await window.api.reload()
}
return (
<ErrorContainer>
<Alert
message={t('error.boundary.default.message')}
showIcon
description={formatErrorMessage(error)}
type="error"
action={
<Space>
<Button size="small" danger onClick={debug}>
{t('error.boundary.default.devtools')}
</Button>
<Button size="small" danger onClick={reload}>
{t('error.boundary.default.reload')}
</Button>
</Space>
}
/>
</ErrorContainer>
)
}
const ErrorBoundaryCustomized = ({
children,
fallbackComponent
}: {
children: ReactNode
fallbackComponent?: ComponentType<FallbackProps>
}) => {
return <ErrorBoundary FallbackComponent={fallbackComponent ?? DefaultFallback}>{children}</ErrorBoundary>
}
const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px;
`
export { ErrorBoundaryCustomized as ErrorBoundary }
+7
View File
@@ -808,6 +808,13 @@
"backup": {
"file_format": "Backup file format error"
},
"boundary": {
"default": {
"devtools": "Open debug panel",
"message": "It seems that something went wrong...",
"reload": "Reload"
}
},
"chat": {
"chunk": {
"non_json": "Returned an invalid data format"
+7
View File
@@ -808,6 +808,13 @@
"backup": {
"file_format": "バックアップファイルの形式エラー"
},
"boundary": {
"default": {
"devtools": "デバッグパネルを開く",
"message": "何か問題が発生したようです...",
"reload": "再読み込み"
}
},
"chat": {
"chunk": {
"non_json": "無効なデータ形式が返されました"
+7
View File
@@ -808,6 +808,13 @@
"backup": {
"file_format": "Ошибка формата файла резервной копии"
},
"boundary": {
"default": {
"devtools": "Открыть панель отладки",
"message": "Похоже, возникла какая-то проблема...",
"reload": "Перезагрузить"
}
},
"chat": {
"chunk": {
"non_json": "Вернулся недопустимый формат данных"
+7
View File
@@ -808,6 +808,13 @@
"backup": {
"file_format": "备份文件格式错误"
},
"boundary": {
"default": {
"devtools": "打开调试面板",
"message": "似乎出现了一些问题...",
"reload": "重新加载"
}
},
"chat": {
"chunk": {
"non_json": "返回了无效的数据格式"
+7
View File
@@ -808,6 +808,13 @@
"backup": {
"file_format": "備份檔案格式錯誤"
},
"boundary": {
"default": {
"devtools": "打開除錯面板",
"message": "似乎出現了一些問題...",
"reload": "重新載入"
}
},
"chat": {
"chunk": {
"non_json": "返回了無效的資料格式"
@@ -808,6 +808,13 @@
"backup": {
"file_format": "Λάθος μορφή αρχείου που επιστρέφεται"
},
"boundary": {
"default": {
"devtools": "Άνοιγμα πίνακα αποσφαλμάτωσης",
"message": "Φαίνεται ότι προέκυψε κάποιο πρόβλημα...",
"reload": "Επαναφόρτωση"
}
},
"chat": {
"chunk": {
"non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων"
@@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Συνεχίστε το συνομιλημένο",
"error": {
"topic_not_found": "Το θέμα δεν υπάρχει"
},
"locate": {
"message": "Εφαρμογή στο μήνυμα"
},
@@ -808,6 +808,13 @@
"backup": {
"file_format": "Formato de archivo de copia de seguridad incorrecto"
},
"boundary": {
"default": {
"devtools": "Abrir el panel de depuración",
"message": "Parece que ha surgido un problema...",
"reload": "Recargar"
}
},
"chat": {
"chunk": {
"non_json": "Devuelve un formato de datos no válido"
@@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Continuar chat",
"error": {
"topic_not_found": "El tema no existe"
},
"locate": {
"message": "Localizar mensaje"
},
@@ -808,6 +808,13 @@
"backup": {
"file_format": "Le format du fichier de sauvegarde est incorrect"
},
"boundary": {
"default": {
"devtools": "Ouvrir le panneau de débogage",
"message": "Il semble que quelques problèmes soient survenus...",
"reload": "Recharger"
}
},
"chat": {
"chunk": {
"non_json": "a renvoyé un format de données invalide"
@@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Continuer la conversation",
"error": {
"topic_not_found": "Le sujet n'existe pas"
},
"locate": {
"message": "Localiser le message"
},
@@ -808,6 +808,13 @@
"backup": {
"file_format": "Formato do arquivo de backup está incorreto"
},
"boundary": {
"default": {
"devtools": "Abrir o painel de depuração",
"message": "Parece que ocorreu um problema...",
"reload": "Recarregar"
}
},
"chat": {
"chunk": {
"non_json": "Devolveu um formato de dados inválido"
@@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Continuar conversando",
"error": {
"topic_not_found": "Tópico inexistente"
},
"locate": {
"message": "Localizar mensagem"
},
+18 -13
View File
@@ -1,3 +1,4 @@
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
@@ -100,20 +101,24 @@ const HomePage: FC = () => {
)}
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
<ErrorBoundary>
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
</ErrorBoundary>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
<ErrorBoundary>
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
</ErrorBoundary>
</ContentContainer>
</Container>
)