11import type React from 'react' ;
2- import { useEffect , useRef , useState } from 'react' ;
2+ import { Component , useEffect , useRef , useState } from 'react' ;
33import { useAuth , useSystemConfig } from '../hooks' ;
44import { createParsedApiError , getParsedApiError , type ParsedApiError } from '../api/error' ;
55import { systemConfigApi } from '../api/systemConfig' ;
@@ -17,7 +17,7 @@ import {
1717 SettingsSectionCard ,
1818} from '../components/settings' ;
1919import { WEB_BUILD_INFO } from '../utils/constants' ;
20- import { getCategoryDescriptionZh } from '../utils/systemConfigI18n' ;
20+ import { getCategoryDescriptionZh , getCategoryTitleZh } from '../utils/systemConfigI18n' ;
2121import type { SystemConfigCategory } from '../types/systemConfig' ;
2222
2323type DesktopWindow = Window & {
@@ -71,6 +71,64 @@ type DesktopUpdateNotice = {
7171 actionKind ?: 'release' | 'install' ;
7272} ;
7373
74+ type SettingsContentErrorBoundaryProps = {
75+ categoryTitle : string ;
76+ resetKey : string ;
77+ children : React . ReactNode ;
78+ } ;
79+
80+ type SettingsContentErrorBoundaryState = {
81+ hasError : boolean ;
82+ message : string ;
83+ } ;
84+
85+ class SettingsContentErrorBoundary extends Component <
86+ SettingsContentErrorBoundaryProps ,
87+ SettingsContentErrorBoundaryState
88+ > {
89+ state : SettingsContentErrorBoundaryState = {
90+ hasError : false ,
91+ message : '' ,
92+ } ;
93+
94+ static getDerivedStateFromError ( error : unknown ) : SettingsContentErrorBoundaryState {
95+ return {
96+ hasError : true ,
97+ message : error instanceof Error ? error . message : 'Unknown settings render error' ,
98+ } ;
99+ }
100+
101+ componentDidUpdate ( previousProps : SettingsContentErrorBoundaryProps ) {
102+ if ( previousProps . resetKey !== this . props . resetKey && this . state . hasError ) {
103+ this . setState ( { hasError : false , message : '' } ) ;
104+ }
105+ }
106+
107+ componentDidCatch ( error : unknown , info : unknown ) {
108+ console . error ( 'Settings category render failed' , error , info ) ;
109+ }
110+
111+ render ( ) {
112+ if ( ! this . state . hasError ) {
113+ return this . props . children ;
114+ }
115+
116+ const detail = this . state . message ? `错误信息:${ this . state . message } 。` : '' ;
117+ return (
118+ < SettingsSectionCard
119+ title = "当前分类加载失败"
120+ description = "该设置分类出现运行时异常,页面已保留其它导航与操作。"
121+ >
122+ < SettingsAlert
123+ title = { `${ this . props . categoryTitle } 设置暂时无法显示` }
124+ message = { `${ detail } 请记录当前点击的设置分类,并提供 release 版本、Windows 版本和 desktop.log 继续排查。` }
125+ variant = "error"
126+ />
127+ </ SettingsSectionCard >
128+ ) ;
129+ }
130+ }
131+
74132function trimDesktopRuntimeString ( value : unknown ) {
75133 return typeof value === 'string' ? value . trim ( ) : '' ;
76134}
@@ -308,7 +366,12 @@ const SettingsPage: React.FC = () => {
308366 } ;
309367 } , [ canCheckDesktopUpdate , desktopRuntimeApi ] ) ;
310368
311- const rawActiveItems = itemsByCategory [ activeCategory ] || [ ] ;
369+ const safeCategories = Array . isArray ( categories ) ? categories : [ ] ;
370+ const safeItemsByCategory =
371+ itemsByCategory && typeof itemsByCategory === 'object' ? itemsByCategory : { } ;
372+ const rawActiveItems = Array . isArray ( safeItemsByCategory [ activeCategory ] )
373+ ? safeItemsByCategory [ activeCategory ]
374+ : [ ] ;
312375 const rawActiveItemMap = new Map ( rawActiveItems . map ( ( item ) => [ item . key , String ( item . value ?? '' ) ] ) ) ;
313376 const hasConfiguredChannels = Boolean ( ( rawActiveItemMap . get ( 'LLM_CHANNELS' ) || '' ) . trim ( ) ) ;
314377 const hasLitellmConfig = Boolean ( ( rawActiveItemMap . get ( 'LITELLM_CONFIG' ) || '' ) . trim ( ) ) ;
@@ -365,6 +428,7 @@ const SettingsPage: React.FC = () => {
365428 : rawActiveItems ;
366429 const isEnvBackupAllowed = isDesktopRuntime || authEnabled ;
367430 const envBackupActionDisabled = isLoading || isSaving || isExportingEnv || isImportingEnv || ! isEnvBackupAllowed ;
431+ const activeCategoryTitle = getCategoryTitleZh ( activeCategory as SystemConfigCategory , activeCategory ) ;
368432
369433 const downloadEnvBackup = async ( ) => {
370434 setEnvBackupActionError ( null ) ;
@@ -554,14 +618,18 @@ const SettingsPage: React.FC = () => {
554618 < div className = "grid grid-cols-1 gap-5 lg:grid-cols-[280px_1fr]" >
555619 < aside className = "lg:sticky lg:top-4 lg:self-start" >
556620 < SettingsCategoryNav
557- categories = { categories }
558- itemsByCategory = { itemsByCategory }
621+ categories = { safeCategories }
622+ itemsByCategory = { safeItemsByCategory }
559623 activeCategory = { activeCategory }
560624 onSelect = { setActiveCategory }
561625 />
562626 </ aside >
563627
564628 < section className = "space-y-4" >
629+ < SettingsContentErrorBoundary
630+ categoryTitle = { activeCategoryTitle }
631+ resetKey = { activeCategory }
632+ >
565633 { activeCategory === 'system' ? < AuthSettingsCard /> : null }
566634 { activeCategory === 'system' ? (
567635 < SettingsSectionCard
@@ -783,6 +851,7 @@ const SettingsPage: React.FC = () => {
783851 className = "settings-surface-panel settings-border-strong border-none bg-transparent shadow-none"
784852 />
785853 ) }
854+ </ SettingsContentErrorBoundary >
786855 </ section >
787856 </ div >
788857 ) }
0 commit comments