v0.5.2: offline report + tab UI + persist race fix

- New: Offline earnings report dialog (welcome back popup with gain/duration/rate/progress)
  - src/lib/game/offlineReport.ts: module-level store + custom event
  - src/components/game/OfflineReportDialog.tsx: Radix Dialog UI
  - gameStore.init: setPendingOfflineReport before set()
- Fix: useGameLoop waits for persist.hasHydrated() before init
  - Root cause: Zustand persist rehydrate is async (Promise.resolve wrapper)
  - Before fix: init read INITIAL_STATE (lastTick=undefined), skipped offline branch
  - After fix: init waits for rehydrate, correctly reads saved lastTick
- UI: Tab bar redesign with per-tab accent colors + flex-col layout
- Version: v0.5 -> v0.5.2
This commit is contained in:
2026-06-23 17:04:28 +00:00
parent bd28219b65
commit 296c609479
5 changed files with 273 additions and 33 deletions
+21 -4
View File
@@ -13,9 +13,22 @@ export function useGameLoop() {
const inited = useRef(false);
useEffect(() => {
if (!inited.current) {
init();
inited.current = true;
// 等待 persist rehydrate 完成后再 initv0.5.2 修复离线收益不弹窗 BUG)
// Zustand persist 即使用同步 localStoragerehydrate 也是异步(Promise.resolve 包装)
const doInit = () => {
if (!inited.current) {
init();
inited.current = true;
}
};
let unsub: (() => void) | null = null;
let fallback: ReturnType<typeof setTimeout> | null = null;
if (useGameStore.persist.hasHydrated()) {
doInit();
} else {
unsub = useGameStore.persist.onFinishHydration(doInit);
// 兜底:500ms 后强制 init(防止 hydrated 状态异常)
fallback = setTimeout(doInit, 500);
}
let counter = 0;
const id = setInterval(() => {
@@ -30,7 +43,11 @@ export function useGameLoop() {
checkAchievements();
}
}, 250);
return () => clearInterval(id);
return () => {
clearInterval(id);
if (unsub) unsub();
if (fallback) clearTimeout(fallback);
};
}, [tick, autoDecodeTick, tickTide, checkAchievements, init]);
// 页面可见性:切回时补 tick + 星潮 + 成就