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 测试绿
This commit is contained in:
@@ -12,6 +12,29 @@ import { startFxLayer, spawnBurst, stopFxLayer } from './ui/fx-canvas'
|
|||||||
import { DriftLayer } from './ui/components/DriftLayer'
|
import { DriftLayer } from './ui/components/DriftLayer'
|
||||||
import { seasonOf } from './game/data/season'
|
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() {
|
export default function App() {
|
||||||
const screen = useGameStore((s) => s.screen)
|
const screen = useGameStore((s) => s.screen)
|
||||||
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App'
|
import App, { AppErrorBoundary } from './App'
|
||||||
import './ui/styles.css'
|
import './ui/styles.css'
|
||||||
|
|
||||||
const root = document.getElementById('root')
|
const root = document.getElementById('root')
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<AppErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
|
</AppErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,12 +82,8 @@ export function MemberModal({ member }: { member: Character }) {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="modal-title" style={{ fontSize: '1.4rem' }}>
|
<div className="mm-brief">
|
||||||
{member.name}
|
<span className={member.alive ? '' : 'bad'}>寿数约 {lifespan} · {describeRoots(member.roots)}</span>
|
||||||
{!member.alive && <span className="bad" style={{ fontSize: '1rem' }}> · 殁</span>}
|
|
||||||
</div>
|
|
||||||
<div className="modal-cat">
|
|
||||||
第{member.generation}代 · {member.gender === 'male' ? '男' : '女'} · {w.ageOf(member)}岁 · 寿数约{lifespan} · {describeRoots(member.roots)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mm-grid">
|
<div className="mm-grid">
|
||||||
|
|||||||
@@ -300,7 +300,26 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
} catch {
|
} 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()
|
stopTimer()
|
||||||
} finally {
|
} finally {
|
||||||
set({ advancing: false })
|
set({ advancing: false })
|
||||||
@@ -411,7 +430,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
// 速度被暂停/事件待决/战报展开/在途推进时静默轮空
|
// 速度被暂停/事件待决/战报展开/在途推进时静默轮空
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void cur.advance()
|
cur.advance().catch((e) => console.error('[speed-tick]', e))
|
||||||
}, ms)
|
}, ms)
|
||||||
set({ speed: v, timer })
|
set({ speed: v, timer })
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user