diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 84664a8..bc2a753 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -12,6 +12,29 @@ import { startFxLayer, spawnBurst, stopFxLayer } from './ui/fx-canvas' import { DriftLayer } from './ui/components/DriftLayer' import { seasonOf } from './game/data/season' +import { Component, type ReactNode, type ErrorInfo } from 'react' + +interface EBState { err: Error | null } +export class AppErrorBoundary extends Component<{ children: ReactNode }, EBState> { + state: EBState = { err: null } + static getDerivedStateFromError(err: Error): EBState { return { err } } + componentDidCatch(err: Error, info: ErrorInfo): void { + console.error('[ui-crash]', err, info.componentStack) + } + render(): ReactNode { + if (this.state.err) { + return ( +
+

界面出错了(游戏数据未受影响)

+
{String(this.state.err?.message ?? this.state.err)}
+ +
+ ) + } + return this.props.children + } +} + export default function App() { const screen = useGameStore((s) => s.screen) const pendingEventId = useGameStore((s) => s.pendingEventId) diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index 3ef95ad..e09b03c 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -1,13 +1,15 @@ import React from 'react' import { createRoot } from 'react-dom/client' -import App from './App' +import App, { AppErrorBoundary } from './App' import './ui/styles.css' const root = document.getElementById('root') if (root) { createRoot(root).render( - + + + ) } diff --git a/src/renderer/ui/components/MemberModal.tsx b/src/renderer/ui/components/MemberModal.tsx index 058f7d6..b109441 100644 --- a/src/renderer/ui/components/MemberModal.tsx +++ b/src/renderer/ui/components/MemberModal.tsx @@ -82,12 +82,8 @@ export function MemberModal({ member }: { member: Character }) { } > -
- {member.name} - {!member.alive && · 殁} -
-
- 第{member.generation}代 · {member.gender === 'male' ? '男' : '女'} · {w.ageOf(member)}岁 · 寿数约{lifespan} · {describeRoots(member.roots)} +
+ 寿数约 {lifespan} · {describeRoots(member.roots)}
diff --git a/src/renderer/ui/store.ts b/src/renderer/ui/store.ts index 56e09dd..b2372ac 100644 --- a/src/renderer/ui/store.ts +++ b/src/renderer/ui/store.ts @@ -300,7 +300,26 @@ export const useGameStore = create((set, get) => ({ } catch { // 保存失败时不再叠加错误 } - setNextToast('时日流转出现异常,已保存出险现场,可回档重来。') + const detail = String(e).slice(0, 120) + let safed = false + if (st.world) { + try { + const file = await getSaveSlot(st.slot) + await file.open() + st.world.syncRng() + await file.saveState(st.world.state, '出险前') + const meta = metaFromState(st.world.state, st.slot) + await file.setMeta(meta) + safed = true + } catch (serr) { + console.error('[emergency-save-failed]', serr) + try { + localStorage.setItem('cotyc-emergency', JSON.stringify({ at: new Date().toISOString(), state: st.world.state })) + } catch { /* 极端环境不管 */ } + } + } + setNextToast(safed ? `时日流转异常(${detail})——出险现场已存,可回档。` : `时日流转异常(${detail})——【存储也失败】档未落盘,请手动存档后重开。`) + console.error('[advance-crash-detail]', detail) stopTimer() } finally { set({ advancing: false }) @@ -411,7 +430,7 @@ export const useGameStore = create((set, get) => ({ // 速度被暂停/事件待决/战报展开/在途推进时静默轮空 return } - void cur.advance() + cur.advance().catch((e) => console.error('[speed-tick]', e)) }, ms) set({ speed: v, timer }) },