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:
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 离线收益报告弹窗(v0.5.2)
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Gem, Clock, TrendingUp, AlertCircle, Sparkles } from "lucide-react";
|
||||
import {
|
||||
getPendingOfflineReport,
|
||||
consumeOfflineReport,
|
||||
formatDuration,
|
||||
type OfflineReport,
|
||||
} from "@/lib/game/offlineReport";
|
||||
import { formatNum } from "@/lib/game/config";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
|
||||
export function OfflineReportDialog() {
|
||||
const [report, setReport] = useState<OfflineReport | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 启动时检查是否有待处理报告
|
||||
const check = () => {
|
||||
const r = getPendingOfflineReport();
|
||||
if (r && r.gain > 0.1) {
|
||||
setReport(r);
|
||||
// 延迟播放音效,避免与初始化冲突
|
||||
setTimeout(() => sfx("decodeSuccess"), 300);
|
||||
} else if (r) {
|
||||
// 收益太小直接消费掉不弹窗
|
||||
consumeOfflineReport();
|
||||
}
|
||||
};
|
||||
// 等游戏 mount + init 完成(init 现在等 persist rehydrate,可能稍晚)
|
||||
const t = setTimeout(check, 1500);
|
||||
const handler = () => check();
|
||||
window.addEventListener("echo-nexus-offline-report", handler);
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
window.removeEventListener("echo-nexus-offline-report", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const close = () => {
|
||||
consumeOfflineReport();
|
||||
setReport(null);
|
||||
};
|
||||
|
||||
if (!report) return null;
|
||||
|
||||
const cappedAtWarehouse = report.gain < report.rate * report.elapsedSec * report.eff - 0.01;
|
||||
|
||||
return (
|
||||
<Dialog open={!!report} onOpenChange={(v) => !v && close()}>
|
||||
<DialogContent className="bg-gradient-to-br from-[#0f0a2e]/95 to-[#050410]/95 border-emerald-400/30 max-w-[400px]">
|
||||
<DialogHeader>
|
||||
{/* 顶部装饰条 */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-emerald-400 via-cyan-400 to-blue-400 rounded-t-xl" />
|
||||
<DialogTitle className="flex items-center gap-2 text-emerald-100">
|
||||
<Sparkles className="h-4 w-4 text-emerald-300" />
|
||||
欢迎回来,无人机
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
在你离开的这段时间里,采矿机群持续运转…
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 py-1">
|
||||
{/* 主要收益 */}
|
||||
<div className="relative rounded-xl border border-emerald-400/30 bg-emerald-500/5 p-4 overflow-hidden">
|
||||
{/* 背景光晕 */}
|
||||
<div className="pointer-events-none absolute -top-8 -right-8 h-24 w-24 rounded-full bg-emerald-500/20 blur-2xl" />
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="h-12 w-12 rounded-xl bg-emerald-500/15 border border-emerald-400/40 flex items-center justify-center shrink-0">
|
||||
<Gem className="h-6 w-6 text-emerald-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[11px] text-muted-foreground">离线采集获得</div>
|
||||
<div className="text-2xl font-bold font-mono text-emerald-200 tabular-nums leading-tight">
|
||||
+{formatNum(report.gain)}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground/80">记忆晶体</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细统计 */}
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="rounded-lg border border-white/10 bg-black/30 px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground mb-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
离线时长
|
||||
</div>
|
||||
<div className="font-mono font-semibold text-foreground">
|
||||
{formatDuration(report.rawElapsedSec)}
|
||||
</div>
|
||||
{report.capped && (
|
||||
<div className="text-[9px] text-amber-400/80 mt-0.5">已达 8h 上限</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-lg border border-white/10 bg-black/30 px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground mb-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
采集速率
|
||||
</div>
|
||||
<div className="font-mono font-semibold text-foreground">
|
||||
{formatNum(report.rate)}/s
|
||||
</div>
|
||||
<div className="text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
效率 {Math.round(report.eff * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 仓库满仓警告 */}
|
||||
{cappedAtWarehouse && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-amber-400/30 bg-amber-500/5 px-3 py-2 text-[11px] text-amber-200">
|
||||
<AlertCircle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
|
||||
<span>
|
||||
仓库已满,部分离线产出被截断。升级仓库容量或解码晶体可避免浪费。
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 当前进度 */}
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="text-muted-foreground">当前晶体</span>
|
||||
<span className="font-mono text-foreground tabular-nums">
|
||||
{formatNum(report.crystalsAfter)} / {formatNum(report.crystalCap)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 h-1.5 rounded-full bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-emerald-500 to-cyan-400 transition-all"
|
||||
style={{
|
||||
width: `${Math.min(100, (report.crystalsAfter / report.crystalCap) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-1">
|
||||
<Button
|
||||
onClick={close}
|
||||
className="bg-emerald-500 hover:bg-emerald-600 text-white border-emerald-400/50"
|
||||
>
|
||||
<Gem className="h-3.5 w-3.5 mr-1" />
|
||||
领取收益
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user