Files
ChronicleOfTheImmortalClan/src/renderer/App.tsx
T
thzxx f5e49ef9bd v0.1.36a: 三问题修复——成员弹框双排名/界面白屏防线/推进异常取证+出险保障
【Bug2 双排名】MemberModal 顶部与 ModalShell 壳顶重复渲染 (title+category 各两排)
→ children 内嵌 title/cat 移除(壳已承载),改 mm-brief 一行(寿数/根骨)
【Bug3 定制白屏】App 无错误边界 → 全树崩溃只剩背景
→ AppErrorBoundary 一级防线(显示错误信息+重新加载按钮,不再白屏)
【Bug1 推进崩溃】advance 异常 toast 原文只写“已保存”——实为 Stash 静默吞错+双保险缺失
→ ①toast 显示根错前 120 字(玩家可见——此前只有 console) ②出险保存双保险:
Stash 失败时 localStorage 应急副本+明示“存储也失败”提示手动存档 ③speed timer 内 advance().catch 隔离
【验证】tsc 0 错/build 绿/7000 测试绿
2026-08-24 21:53:04 +08:00

79 lines
3.1 KiB
TypeScript

import { useEffect } from 'react'
import { useGameStore } from './ui/store'
import Boot from './ui/screens/Boot'
import NewGame from './ui/screens/NewGame'
import GameScreen from './ui/screens/GameScreen'
import { EventModal } from './ui/components/EventModal'
import { BattleModal } from './ui/components/BattleModal'
import { PaperModal } from './ui/components/PaperModal'
import { sClick } from './ui/sound'
import { ensureFx, fxVisibilityAutopause, applySeason } from './ui/fx'
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 (
<div style={{ padding: 32, color: '#e8d9b0', fontFamily: 'serif' }}>
<h2 style={{ color: '#d99a85' }}>界面出错了(游戏数据未受影响)</h2>
<pre style={{ whiteSpace: 'pre-wrap', fontSize: '0.8rem', maxHeight: '50vh', overflow: 'auto' }}>{String(this.state.err?.message ?? this.state.err)}</pre>
<button className="btn" onClick={() => location.reload()}>重新加载(回档到最近存点)</button>
</div>
)
}
return this.props.children
}
}
export default function App() {
const screen = useGameStore((s) => s.screen)
const pendingEventId = useGameStore((s) => s.pendingEventId)
const battleView = useGameStore((s) => s.battleView)
useEffect(() => {
useGameStore.getState().init()
const clicker = () => sClick()
document.addEventListener('click', clicker, true)
const fx = ensureFx()
const stopVis = fxVisibilityAutopause()
const burst = (e: Event) => {
const kind = e.type.replace('fx:', '')
if (kind === 'spark' || kind === 'blade') spawnBurst('spark', 'center')
if (kind === 'ripple') spawnBurst('ripple')
if (kind === 'pulse') spawnBurst('ripple')
}
for (const k of ['fx:spark', 'fx:blade', 'fx:ripple', 'fx:pulse']) window.addEventListener(k, burst)
const seasonInit = () => applySeason(seasonOf(useGameStore.getState().world?.state.month ?? 1))
seasonInit()
startFxLayer(fx.mode === 'soft' ? 18 : fx.mode === 'full' ? 70 : 36)
return () => {
document.removeEventListener('click', clicker, true)
stopVis()
for (const k of ['fx:spark', 'fx:blade', 'fx:ripple', 'fx:pulse']) window.removeEventListener(k, burst)
stopFxLayer()
}
}, [])
return (
<div className="app">
{screen === 'boot' && <Boot />}
{screen === 'newgame' && <NewGame />}
{screen === 'game' && <GameScreen />}
{pendingEventId && <EventModal />}
{battleView && <BattleModal />}
<PaperModal />
<DriftLayer />
</div>
)
}