v0.9 工单修复部署

This commit is contained in:
2026-06-24 00:41:29 +00:00
commit 73720d8306
279 changed files with 39708 additions and 0 deletions
@@ -0,0 +1,754 @@
1→ 1→"use client";
2→ 2→// 回响星核 / Echo Nexus — 深空信标面板
3→ 3→// v0.5:每日挑战 + 本地排行榜
4→ 4→// v0.8:周挑战 + 信标链(连续完成奖励)
5→ 5→import { useState, useEffect, useCallback } from "react";
6→ 6→import { useGameStore } from "@/store/gameStore";
7→ 7→import { useToast } from "@/hooks/use-toast";
8→ 8→import { sfx } from "@/hooks/useAudio";
9→ 9→import {
10→ 10→ generateDailyChallenge,
11→ 11→ generateWeeklyChallenge,
12→ 12→ loadDailyProgress,
13→ 13→ loadWeeklyProgress,
14→ 14→ loadLeaderboard,
15→ 15→ loadChainState,
16→ 16→ claimBeaconReward,
17→ 17→ BEACON_DIFFICULTY,
18→ 18→ BEACON_TYPE_META,
19→ 19→ BEACON_CHAIN_MILESTONES,
20→ 20→ BEACON_CHAIN_REWARDS,
21→ 21→ getWeekKey,
22→ 22→ getTodayKey,
23→ 23→ getNextMilestone,
24→ 24→ getChainProgress,
25→ 25→ msUntilNextDay,
26→ 26→ msUntilNextWeek,
27→ 27→ formatCountdown,
28→ 28→ type BeaconDailyChallenge,
29→ 29→ type BeaconDailyProgress,
30→ 30→ type BeaconWeeklyChallenge,
31→ 31→ type BeaconWeeklyProgress,
32→ 32→ type BeaconScoreEntry,
33→ 33→ type BeaconChallengeType,
34→ 34→ type BeaconDifficulty,
35→ 35→ type BeaconChainState,
36→ 36→} from "@/lib/game/beacon";
37→ 37→import { formatNum } from "@/lib/game/config";
38→ 38→import { Button } from "@/components/ui/button";
39→ 39→import { Progress } from "@/components/ui/progress";
40→ 40→import {
41→ 41→ Radio,
42→ 42→ Clock,
43→ 43→ Trophy,
44→ 44→ Sparkles,
45→ 45→ Award,
46→ 46→ Crown,
47→ 47→ Medal,
48→ 48→ Link2,
49→ 49→ Flame,
50→ 50→ Zap,
51→ 51→} from "lucide-react";
52→ 52→
53→ 53→const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"];
54→ 54→
55→ 55→function rankBadge(rank: number) {
56→ 56→ if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" };
57→ 57→ if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" };
58→ 58→ if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" };
59→ 59→ return null;
60→ 60→}
61→ 61→
62→ 62→export function BeaconPanel() {
63→ 63→ const grantBeaconReward = useGameStore((s) => s.grantBeaconReward);
64→ 64→ const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon);
65→ 65→ const claimChainReward = useGameStore((s) => s.claimChainReward);
66→ 66→ const { toast } = useToast();
67→ 67→
68→ 68→ const [challenge, setChallenge] = useState<BeaconDailyChallenge | null>(null);
69→ 69→ const [progress, setProgress] = useState<BeaconDailyProgress | null>(null);
70→ 70→ const [weeklyChallenge, setWeeklyChallenge] =
71→ 71→ useState<BeaconWeeklyChallenge | null>(null);
72→ 72→ const [weeklyProgress, setWeeklyProgress] =
73→ 73→ useState<BeaconWeeklyProgress | null>(null);
74→ 74→ const [chainState, setChainState] = useState<BeaconChainState | null>(null);
75→ 75→ const [leaderboard, setLeaderboard] = useState<BeaconScoreEntry[]>([]);
76→ 76→ const [dayCountdown, setDayCountdown] = useState("00:00:00");
77→ 77→ const [weekCountdown, setWeekCountdown] = useState("00:00:00");
78→ 78→ const [now, setNow] = useState(Date.now());
79→ 79→
80→ 80→ // 初始化 + 每秒刷新(进度 + 倒计时)
81→ 81→ useEffect(() => {
82→ 82→ setChallenge(generateDailyChallenge());
83→ 83→ setProgress(loadDailyProgress());
84→ 84→ setWeeklyChallenge(generateWeeklyChallenge());
85→ 85→ setWeeklyProgress(loadWeeklyProgress());
86→ 86→ setChainState(loadChainState());
87→ 87→ setLeaderboard(loadLeaderboard());
88→ 88→ const id = setInterval(() => {
89→ 89→ setNow(Date.now());
90→ 90→ setProgress(loadDailyProgress());
91→ 91→ setWeeklyProgress(loadWeeklyProgress());
92→ 92→ setChainState(loadChainState());
93→ 93→ setChallenge((c) => c ?? generateDailyChallenge());
94→ 94→ setWeeklyChallenge((c) => c ?? generateWeeklyChallenge());
95→ 95→ }, 1000);
96→ 96→ return () => clearInterval(id);
97→ 97→ }, []);
98→ 98→
99→ 99→ useEffect(() => {
100→ 100→ setDayCountdown(formatCountdown(msUntilNextDay(new Date(now))));
101→ 101→ setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now))));
102→ 102→ }, [now]);
103→ 103→
104→ 104→ const handleClaimDaily = useCallback(() => {
105→ 105→ if (!challenge || !progress) return;
106→ 106→ if (progress.completedAt === null || progress.claimed) return;
107→ 107→ const res = claimBeaconReward(challenge, progress);
108→ 108→ setLeaderboard(res.leaderboard);
109→ 109→ setProgress(loadDailyProgress());
110→ 110→ // 发放奖励到游戏状态
111→ 111→ grantBeaconReward(res.rewardInsight, res.rewardContact);
112→ 112→ sfx("achievement");
113→ 113→ toast({
114→ 114→ title: "✦ 每日信标奖励已领取",
115→ 115→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
116→ 116→ 1
117→ 117→ )} 接触 · 得分 ${res.score}`,
118→ 118→ });
119→ 119→ }, [challenge, progress, toast, grantBeaconReward]);
120→ 120→
121→ 121→ const handleClaimWeekly = useCallback(() => {
122→ 122→ if (!weeklyChallenge || !weeklyProgress) return;
123→ 123→ if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return;
124→ 124→ const res = claimWeeklyBeacon();
125→ 125→ setLeaderboard(loadLeaderboard());
126→ 126→ setWeeklyProgress(loadWeeklyProgress());
127→ 127→ if (res.rewardInsight > 0 || res.rewardContact > 0) {
128→ 128→ sfx("achievement");
129→ 129→ toast({
130→ 130→ title: "✦ 周挑战奖励已领取",
131→ 131→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
132→ 132→ 1
133→ 133→ )} 接触 · 得分 ${res.score}`,
134→ 134→ });
135→ 135→ }
136→ 136→ }, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]);
137→ 137→
138→ 138→ const handleClaimChain = useCallback(
139→ 139→ (milestone: number) => {
140→ 140→ const reward = BEACON_CHAIN_REWARDS.find(
141→ 141→ (r) => r.milestone === milestone
142→ 142→ );
143→ 143→ const res = claimChainReward(milestone);
144→ 144→ setChainState(loadChainState());
145→ 145→ if (res.ok) {
146→ 146→ sfx("achievement");
147→ 147→ toast({
148→ 148→ title: `✦ ${res.label || reward?.label || "里程碑"} 已领取`,
149→ 149→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
150→ 150→ 1
151→ 151→ )} 接触`,
152→ 152→ });
153→ 153→ }
154→ 154→ },
155→ 155→ [toast, claimChainReward]
156→ 156→ );
157→ 157→
158→ 158→ if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) {
159→ 159→ return (
160→ 160→ <div className="flex items-center justify-center h-full text-xs text-muted-foreground/60">
161→ 161→ 正在校准深空信标…
162→ 162→ </div>
163→ 163→ );
164→ 164→ }
165→ 165→
166→ 166→ const diffMeta = BEACON_DIFFICULTY[challenge.difficulty];
167→ 167→ const typeMeta = BEACON_TYPE_META[challenge.type];
168→ 168→ const pct = Math.min(100, (progress.progress / challenge.goal) * 100);
169→ 169→ const isCompleted = progress.completedAt !== null;
170→ 170→ const isClaimed = progress.claimed;
171→ 171→ const canClaim = isCompleted && !isClaimed;
172→ 172→
173→ 173→ // 周挑战派生量
174→ 174→ const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty];
175→ 175→ const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type];
176→ 176→ const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100);
177→ 177→ const wCompleted = weeklyProgress.completedAt !== null;
178→ 178→ const wClaimed = weeklyProgress.claimed;
179→ 179→ const wCanClaim = wCompleted && !wClaimed;
180→ 180→
181→ 181→ // 信标链派生量
182→ 182→ const chainProgress = getChainProgress(chainState.currentStreak);
183→ 183→ const nextMilestone = getNextMilestone(chainState.currentStreak);
184→ 184→ const todayKey = getTodayKey();
185→ 185→ const completedToday = chainState.lastCompletedDateKey === todayKey;
186→ 186→
187→ 187→ return (
188→ 188→ <div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
189→ 189→ <style jsx global>{`
190→ 190→ .echo-scroll::-webkit-scrollbar { width: 4px; }
191→ 191→ .echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
192→ 192→ .echo-scroll::-webkit-scrollbar-track { background: transparent; }
193→ 193→ @keyframes beacon-pulse-ring {
194→ 194→ 0% { transform: scale(0.8); opacity: 0.8; }
195→ 195→ 100% { transform: scale(2.2); opacity: 0; }
196→ 196→ }
197→ 197→ @keyframes beacon-glow {
198→ 198→ 0%, 100% { box-shadow: 0 0 18px ${diffMeta.glow}, inset 0 0 12px ${diffMeta.glow}; }
199→ 199→ 50% { box-shadow: 0 0 32px ${diffMeta.glow}, inset 0 0 20px ${diffMeta.glow}; }
200→ 200→ }
201→ 201→ @keyframes weekly-glow {
202→ 202→ 0%, 100% { box-shadow: 0 0 18px rgba(232,121,249,0.35), inset 0 0 12px rgba(232,121,249,0.25); }
203→ 203→ 50% { box-shadow: 0 0 32px rgba(232,121,249,0.5), inset 0 0 20px rgba(232,121,249,0.35); }
204→ 204→ }
205→ 205→ @keyframes chain-milestone-pulse {
206→ 206→ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(251,113,133,0.55); }
207→ 207→ 50% { transform: scale(1.06); box-shadow: 0 0 0 8px rgba(251,113,133,0); }
208→ 208→ }
209→ 209→ .chain-milestone-reachable {
210→ 210→ animation: chain-milestone-pulse 1.8s ease-in-out infinite;
211→ 211→ }
212→ 212→ @keyframes chain-streak-flux {
213→ 213→ 0%, 100% { background-position: 0% 50%; }
214→ 214→ 50% { background-position: 100% 50%; }
215→ 215→ }
216→ 216→ .chain-streak-text {
217→ 217→ background: linear-gradient(90deg, #fbbf24, #fb7185, #fbbf24);
218→ 218→ background-size: 200% 100%;
219→ 219→ -webkit-background-clip: text;
220→ 220→ background-clip: text;
221→ 221→ -webkit-text-fill-color: transparent;
222→ 222→ animation: chain-streak-flux 4s ease-in-out infinite;
223→ 223→ }
224→ 224→ `}</style>
225→ 225→
226→ 226→ {/* 头部:信标 + 倒计时 */}
227→ 227→ <div className="flex items-center justify-between">
228→ 228→ <h3 className="text-sm font-semibold flex items-center gap-1.5">
229→ 229→ <Radio className="h-4 w-4 text-fuchsia-400" />
230→ 230→ 深空信标
231→ 231→ </h3>
232→ 232→ <div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
233→ 233→ <Clock className="h-3 w-3" />
234→ 234→ 次日重置
235→ 235→ <span className="font-mono text-fuchsia-300/90">{dayCountdown}</span>
236→ 236→ </div>
237→ 237→ </div>
238→ 238→
239→ 239→ {/* 每日挑战卡片 */}
240→ 240→ <div
241→ 241→ className="relative rounded-xl border p-3 overflow-hidden"
242→ 242→ style={{
243→ 243→ borderColor: `${diffMeta.color}55`,
244→ 244→ background: `linear-gradient(135deg, ${diffMeta.color}1f, rgba(0,0,0,0.45))`,
245→ 245→ animation: isCompleted ? "none" : "beacon-glow 3s ease-in-out infinite",
246→ 246→ }}
247→ 247→ >
248→ 248→ {/* 背景装饰:脉冲环 */}
249→ 249→ {!isCompleted && (
250→ 250→ <>
251→ 251→ <div
252→ 252→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
253→ 253→ style={{
254→ 254→ border: `1.5px solid ${diffMeta.color}`,
255→ 255→ animation: "beacon-pulse-ring 2.5s ease-out infinite",
256→ 256→ }}
257→ 257→ />
258→ 258→ <div
259→ 259→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
260→ 260→ style={{
261→ 261→ border: `1.5px solid ${diffMeta.color}`,
262→ 262→ animation: "beacon-pulse-ring 2.5s ease-out infinite 1.25s",
263→ 263→ }}
264→ 264→ />
265→ 265→ </>
266→ 266→ )}
267→ 267→
268→ 268→ <div className="relative">
269→ 269→ {/* 难度 + 类型 标签 */}
270→ 270→ <div className="flex items-center gap-1.5 mb-2">
271→ 271→ <span
272→ 272→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
273→ 273→ style={{ background: `${diffMeta.color}22`, color: diffMeta.color, border: `1px solid ${diffMeta.color}55` }}
274→ 274→ >
275→ 275→ <span>{diffMeta.icon}</span>
276→ 276→ {diffMeta.label}
277→ 277→ </span>
278→ 278→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
279→ 279→ {typeMeta.icon} {typeMeta.label}
280→ 280→ </span>
281→ 281→ {isCompleted && (
282→ 282→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
283→ 283→ <Sparkles className="h-2.5 w-2.5" /> 已完成
284→ 284→ </span>
285→ 285→ )}
286→ 286→ </div>
287→ 287→
288→ 288→ {/* 挑战标题 */}
289→ 289→ <h4 className="text-sm font-semibold mb-1" style={{ color: diffMeta.color, textShadow: `0 0 10px ${diffMeta.glow}` }}>
290→ 290→ {challenge.title}
291→ 291→ </h4>
292→ 292→ <p className="text-[11px] text-muted-foreground/80 leading-relaxed mb-2.5">
293→ 293→ {challenge.desc}
294→ 294→ </p>
295→ 295→
296→ 296→ {/* 进度条 */}
297→ 297→ <div className="mb-2">
298→ 298→ <div className="flex items-center justify-between mb-1">
299→ 299→ <span className="text-[10px] text-muted-foreground/70">进度</span>
300→ 300→ <span className="text-[11px] font-mono font-semibold" style={{ color: diffMeta.color }}>
301→ 301→ {Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit}
302→ 302→ </span>
303→ 303→ </div>
304→ 304→ <Progress
305→ 305→ value={pct}
306→ 306→ className="h-2 bg-black/40"
307→ 307→ style={{
308→ 308→ ["--progress-color" as string]: diffMeta.color,
309→ 309→ }}
310→ 310→ />
311→ 311→ </div>
312→ 312→
313→ 313→ {/* 奖励 + 领取按钮 */}
314→ 314→ <div className="flex items-center justify-between gap-2">
315→ 315→ <div className="flex items-center gap-2 text-[10px]">
316→ 316→ <span className="text-muted-foreground/60">奖励:</span>
317→ 317→ {challenge.rewardInsight > 0 && (
318→ 318→ <span className="text-amber-300 font-mono">+{formatNum(challenge.rewardInsight)}洞见</span>
319→ 319→ )}
320→ 320→ <span className="text-fuchsia-300 font-mono">+{challenge.rewardContact.toFixed(1)}接触</span>
321→ 321→ </div>
322→ 322→ <Button
323→ 323→ onClick={handleClaimDaily}
324→ 324→ disabled={!canClaim}
325→ 325→ size="sm"
326→ 326→ className="h-7 px-3 text-[11px] border-0"
327→ 327→ style={{
328→ 328→ background: canClaim
329→ 329→ ? `linear-gradient(90deg, ${diffMeta.color}, ${diffMeta.color}cc)`
330→ 330→ : `${diffMeta.color}1a`,
331→ 331→ color: canClaim ? "#022c22" : `${diffMeta.color}99`,
332→ 332→ }}
333→ 333→ >
334→ 334→ {isClaimed ? "已领取" : canClaim ? "领取奖励" : isCompleted ? "已领取" : "进行中…"}
335→ 335→ </Button>
336→ 336→ </div>
337→ 337→
338→ 338→ {/* 完成时长 */}
339→ 339→ {isCompleted && progress.durationSec > 0 && (
340→ 340→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
341→ 341→ 完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒
342→ 342→ </div>
343→ 343→ )}
344→ 344→ </div>
345→ 345→ </div>
346→ 346→
347→ 347→ {/* 周挑战 + 信标链(桌面端并排,移动端单列) */}
348→ 348→ <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
349→ 349→ {/* ===== 周挑战卡片(fuchsia 主题) ===== */}
350→ 350→ <div
351→ 351→ className="relative rounded-xl border p-3 overflow-hidden"
352→ 352→ style={{
353→ 353→ borderColor: "rgba(232,121,249,0.4)",
354→ 354→ background: `linear-gradient(135deg, rgba(232,121,249,0.12), rgba(0,0,0,0.45))`,
355→ 355→ animation: wCompleted ? "none" : "weekly-glow 3.5s ease-in-out infinite",
356→ 356→ }}
357→ 357→ >
358→ 358→ {/* 头部:标题 + weekKey + 倒计时 */}
359→ 359→ <div className="flex items-center justify-between mb-2">
360→ 360→ <div className="flex items-center gap-1.5">
361→ 361→ <Zap className="h-3.5 w-3.5 text-fuchsia-400" />
362→ 362→ <span className="text-xs font-semibold text-fuchsia-200">
363→ 363→ 周挑战 · WEEKLY
364→ 364→ </span>
365→ 365→ </div>
366→ 366→ <div className="flex items-center gap-1 text-[9px] text-muted-foreground/70">
367→ 367→ <span className="font-mono text-fuchsia-300/80">
368→ 368→ {weeklyChallenge.weekKey}
369→ 369→ </span>
370→ 370→ <Clock className="h-2.5 w-2.5" />
371→ 371→ <span className="font-mono text-fuchsia-300/90">{weekCountdown}</span>
372→ 372→ </div>
373→ 373→ </div>
374→ 374→
375→ 375→ {/* 难度 + 类型 标签 */}
376→ 376→ <div className="flex items-center gap-1.5 mb-2">
377→ 377→ <span
378→ 378→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
379→ 379→ style={{ background: `${wDiffMeta.color}22`, color: wDiffMeta.color, border: `1px solid ${wDiffMeta.color}55` }}
380→ 380→ >
381→ 381→ <span>{wDiffMeta.icon}</span>
382→ 382→ {wDiffMeta.label}
383→ 383→ </span>
384→ 384→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
385→ 385→ {wTypeMeta.icon} {wTypeMeta.label}
386→ 386→ </span>
387→ 387→ {wCompleted && (
388→ 388→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
389→ 389→ <Sparkles className="h-2.5 w-2.5" /> 已完成
390→ 390→ </span>
391→ 391→ )}
392→ 392→ </div>
393→ 393→
394→ 394→ {/* 标题 */}
395→ 395→ <h4
396→ 396→ className="text-sm font-semibold mb-1"
397→ 397→ style={{ color: wDiffMeta.color, textShadow: `0 0 10px ${wDiffMeta.glow}` }}
398→ 398→ >
399→ 399→ {weeklyChallenge.title}
400→ 400→ </h4>
401→ 401→ <p className="text-[10px] text-muted-foreground/80 leading-relaxed mb-2">
402→ 402→ {weeklyChallenge.desc}
403→ 403→ </p>
404→ 404→
405→ 405→ {/* 进度条 */}
406→ 406→ <div className="mb-2">
407→ 407→ <div className="flex items-center justify-between mb-1">
408→ 408→ <span className="text-[10px] text-muted-foreground/70">进度</span>
409→ 409→ <span className="text-[11px] font-mono font-semibold" style={{ color: wDiffMeta.color }}>
410→ 410→ {Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit}
411→ 411→ </span>
412→ 412→ </div>
413→ 413→ <Progress
414→ 414→ value={wPct}
415→ 415→ className="h-2 bg-black/40"
416→ 416→ style={{
417→ 417→ ["--progress-color" as string]: wDiffMeta.color,
418→ 418→ }}
419→ 419→ />
420→ 420→ </div>
421→ 421→
422→ 422→ {/* 奖励 + 领取按钮 */}
423→ 423→ <div className="flex items-center justify-between gap-2">
424→ 424→ <div className="flex items-center gap-1.5 text-[10px]">
425→ 425→ <span className="text-muted-foreground/60">奖励:</span>
426→ 426→ {weeklyChallenge.rewardInsight > 0 && (
427→ 427→ <span className="text-amber-300 font-mono">
428→ 428→ +{formatNum(weeklyChallenge.rewardInsight)}洞见
429→ 429→ </span>
430→ 430→ )}
431→ 431→ <span className="text-fuchsia-300 font-mono">
432→ 432→ +{weeklyChallenge.rewardContact.toFixed(1)}接触
433→ 433→ </span>
434→ 434→ </div>
435→ 435→ <Button
436→ 436→ onClick={handleClaimWeekly}
437→ 437→ disabled={!wCanClaim}
438→ 438→ size="sm"
439→ 439→ className="h-7 px-3 text-[11px] border-0"
440→ 440→ style={{
441→ 441→ background: wCanClaim
442→ 442→ ? `linear-gradient(90deg, #34d399, #34d399cc)`
443→ 443→ : "rgba(232,121,249,0.10)",
444→ 444→ color: wCanClaim
445→ 445→ ? "#022c22"
446→ 446→ : "rgba(232,121,249,0.55)",
447→ 447→ }}
448→ 448→ >
449→ 449→ {wClaimed
450→ 450→ ? "已领取"
451→ 451→ : wCanClaim
452→ 452→ ? "领取奖励"
453→ 453→ : wCompleted
454→ 454→ ? "已领取"
455→ 455→ : "进行中…"}
456→ 456→ </Button>
457→ 457→ </div>
458→ 458→
459→ 459→ {wCompleted && weeklyProgress.durationSec > 0 && (
460→ 460→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
461→ 461→ 完成用时 {Math.floor(weeklyProgress.durationSec / 60)}分{weeklyProgress.durationSec % 60}秒
462→ 462→ </div>
463→ 463→ )}
464→ 464→ </div>
465→ 465→
466→ 466→ {/* ===== 信标链卡片(amber→rose 渐变) ===== */}
467→ 467→ <div
468→ 468→ className="relative rounded-xl border p-3 overflow-hidden"
469→ 469→ style={{
470→ 470→ borderColor: "rgba(251,191,36,0.35)",
471→ 471→ background: `linear-gradient(135deg, rgba(251,191,36,0.10), rgba(251,113,133,0.10), rgba(0,0,0,0.4))`,
472→ 472→ }}
473→ 473→ >
474→ 474→ {/* 头部:标题 + 当前连续天数 */}
475→ 475→ <div className="flex items-center justify-between mb-2">
476→ 476→ <div className="flex items-center gap-1.5">
477→ 477→ <Link2 className="h-3.5 w-3.5 text-amber-400" />
478→ 478→ <span className="text-xs font-semibold text-amber-200">
479→ 479→ 信标链 · CHAIN
480→ 480→ </span>
481→ 481→ </div>
482→ 482→ <div className="flex items-baseline gap-1">
483→ 483→ <Flame className="h-3 w-3 text-rose-400" />
484→ 484→ <span className="text-[10px] text-muted-foreground/60">连续</span>
485→ 485→ <span className="chain-streak-text text-2xl font-bold font-mono leading-none">
486→ 486→ {chainState.currentStreak}
487→ 487→ </span>
488→ 488→ <span className="text-[10px] text-muted-foreground/60">天</span>
489→ 489→ </div>
490→ 490→ </div>
491→ 491→
492→ 492→ {/* 今日完成状态 */}
493→ 493→ <div className="mb-2 flex items-center justify-between">
494→ 494→ <span className="text-[10px] text-muted-foreground/70">
495→ 495→ {completedToday
496→ 496→ ? "今日已贡献"
497→ 497→ : "今日尚未完成日挑战"}
498→ 498→ </span>
499→ 499→ <span
500→ 500→ className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
501→ 501→ completedToday
502→ 502→ ? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
503→ 503→ : "bg-white/5 text-muted-foreground/70 border-white/10"
504→ 504→ }`}
505→ 505→ >
506→ 506→ {completedToday ? "✓ 已记录" : "○ 待完成"}
507→ 507→ </span>
508→ 508→ </div>
509→ 509→
510→ 510→ {/* 4 个里程碑节点横向排列 */}
511→ 511→ <div className="flex items-center justify-between mb-2 relative">
512→ 512→ {/* 节点之间的连线(背景灰) */}
513→ 513→ <div className="absolute top-6 left-[12.5%] right-[12.5%] h-[2px] bg-white/10" />
514→ 514→ {/* 已达成部分高亮(基于 prev→next 插值) */}
515→ 515→ {(() => {
516→ 516→ // 节点圆心水平位置(百分比)
517→ 517→ const MILESTONE_POS: Record<number, number> = {
518→ 518→ 0: 12.5,
519→ 519→ 3: 12.5,
520→ 520→ 7: 37.5,
521→ 521→ 14: 62.5,
522→ 522→ 30: 87.5,
523→ 523→ };
524→ 524→ const prevPos =
525→ 525→ MILESTONE_POS[chainProgress.prev] ?? 12.5;
526→ 526→ const nextPos =
527→ 527→ chainProgress.next !== null
528→ 528→ ? MILESTONE_POS[chainProgress.next] ?? 87.5
529→ 529→ : 87.5;
530→ 530→ const pct = chainProgress.progressPct / 100;
531→ 531→ const activeEndPos = prevPos + (nextPos - prevPos) * pct;
532→ 532→ const widthPct = Math.max(0, activeEndPos - 12.5);
533→ 533→ if (widthPct <= 0) return null;
534→ 534→ return (
535→ 535→ <div
536→ 536→ className="absolute top-6 h-[2px]"
537→ 537→ style={{
538→ 538→ left: "12.5%",
539→ 539→ width: `${widthPct}%`,
540→ 540→ background:
541→ 541→ "linear-gradient(90deg, #fbbf24, #fb7185)",
542→ 542→ boxShadow: "0 0 8px rgba(251,113,133,0.5)",
543→ 543→ }}
544→ 544→ />
545→ 545→ );
546→ 546→ })()}
547→ 547→
548→ 548→ {BEACON_CHAIN_MILESTONES.map((m) => {
549→ 549→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m);
550→ 550→ const claimed = chainState.milestonesClaimed.includes(m);
551→ 551→ const reachable =
552→ 552→ chainState.currentStreak >= m && !claimed;
553→ 553→ const inProgress = chainState.currentStreak > 0 && nextMilestone === m;
554→ 554→ // 节点配色
555→ 555→ let nodeBg = "rgba(255,255,255,0.05)";
556→ 556→ let nodeBorder = "rgba(255,255,255,0.15)";
557→ 557→ let textColor = "rgba(255,255,255,0.4)";
558→ 558→ if (claimed) {
559→ 559→ nodeBg = "rgba(52,211,153,0.25)";
560→ 560→ nodeBorder = "#34d399";
561→ 561→ textColor = "#34d399";
562→ 562→ } else if (reachable) {
563→ 563→ nodeBg = "rgba(251,113,133,0.20)";
564→ 564→ nodeBorder = "#fb7185";
565→ 565→ textColor = "#fb7185";
566→ 566→ } else if (inProgress) {
567→ 567→ nodeBg = "rgba(251,191,36,0.20)";
568→ 568→ nodeBorder = "#fbbf24";
569→ 569→ textColor = "#fbbf24";
570→ 570→ }
571→ 571→
572→ 572→ return (
573→ 573→ <div
574→ 574→ key={m}
575→ 575→ className="flex flex-col items-center gap-1 z-10 flex-1"
576→ 576→ >
577→ 577→ <div
578→ 578→ className={`w-12 h-12 rounded-full flex items-center justify-center font-mono font-bold text-sm border-2 ${
579→ 579→ reachable ? "chain-milestone-reachable" : ""
580→ 580→ }`}
581→ 581→ style={{
582→ 582→ background: nodeBg,
583→ 583→ borderColor: nodeBorder,
584→ 584→ color: textColor,
585→ 585→ }}
586→ 586→ >
587→ 587→ {claimed ? (
588→ 588→ <Sparkles className="h-4 w-4" />
589→ 589→ ) : (
590→ 590→ m
591→ 591→ )}
592→ 592→ </div>
593→ 593→ <span
594→ 594→ className="text-[9px] text-center leading-tight"
595→ 595→ style={{ color: textColor }}
596→ 596→ >
597→ 597→ {reward?.label}
598→ 598→ </span>
599→ 599→ {claimed ? (
600→ 600→ <span className="text-[9px] text-emerald-400/80 font-mono">
601→ 601→ 已领
602→ 602→ </span>
603→ 603→ ) : reachable ? (
604→ 604→ <Button
605→ 605→ onClick={() => handleClaimChain(m)}
606→ 606→ size="sm"
607→ 607→ className="h-5 px-2 text-[9px] py-0 border-0"
608→ 608→ style={{
609→ 609→ background:
610→ 610→ "linear-gradient(90deg, #fb7185, #f43f5e)",
611→ 611→ color: "#1c0608",
612→ 612→ }}
613→ 613→ >
614→ 614→ 领取
615→ 615→ </Button>
616→ 616→ ) : (
617→ 617→ <span className="text-[9px] text-muted-foreground/40 font-mono">
618→ 618→ +{reward?.rewardInsight}洞
619→ 619→ </span>
620→ 620→ )}
621→ 621→ </div>
622→ 622→ );
623→ 623→ })}
624→ 624→ </div>
625→ 625→
626→ 626→ {/* 进度条 */}
627→ 627→ <div className="mb-2">
628→ 628→ <div className="flex items-center justify-between mb-1">
629→ 629→ <span className="text-[10px] text-muted-foreground/70">
630→ 630→ {chainProgress.next === null
631→ 631→ ? "已通关全部里程碑"
632→ 632→ : `下一目标:${chainProgress.next} 天`}
633→ 633→ </span>
634→ 634→ <span className="text-[10px] font-mono text-amber-300">
635→ 635→ {chainProgress.current}
636→ 636→ {chainProgress.next !== null && ` / ${chainProgress.next}`} 天
637→ 637→ </span>
638→ 638→ </div>
639→ 639→ <Progress
640→ 640→ value={chainProgress.progressPct}
641→ 641→ className="h-1.5 bg-black/40"
642→ 642→ style={{
643→ 643→ ["--progress-color" as string]: "#fbbf24",
644→ 644→ }}
645→ 645→ />
646→ 646→ </div>
647→ 647→
648→ 648→ {/* 底部统计 */}
649→ 649→ <div className="flex items-center justify-between gap-2 text-[10px]">
650→ 650→ <div className="flex items-center gap-2">
651→ 651→ <span className="text-muted-foreground/60">最长</span>
652→ 652→ <span className="font-mono text-amber-300">
653→ 653→ {chainState.longestStreak}天
654→ 654→ </span>
655→ 655→ </div>
656→ 656→ <div className="flex items-center gap-2">
657→ 657→ <span className="text-muted-foreground/60">累计</span>
658→ 658→ <span className="font-mono text-rose-300">
659→ 659→ {chainState.totalCompletions}次
660→ 660→ </span>
661→ 661→ </div>
662→ 662→ <div className="flex items-center gap-1">
663→ 663→ <span className="text-muted-foreground/60">续命</span>
664→ 664→ {chainState.graceUsed >= 1 ? (
665→ 665→ <span className="font-mono text-rose-400/80">已用</span>
666→ 666→ ) : (
667→ 667→ <span className="font-mono text-emerald-400/80">可用</span>
668→ 668→ )}
669→ 669→ </div>
670→ 670→ </div>
671→ 671→ </div>
672→ 672→ </div>
673→ 673→
674→ 674→ {/* 本地排行榜 */}
675→ 675→ <div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
676→ 676→ <div className="flex items-center gap-1.5 mb-2">
677→ 677→ <Trophy className="h-3.5 w-3.5 text-amber-400" />
678→ 678→ <span className="text-xs font-semibold">深空排行榜</span>
679→ 679→ <span className="text-[10px] text-muted-foreground/50">本地 · Top {leaderboard.length || 0}</span>
680→ 680→ </div>
681→ 681→
682→ 682→ {leaderboard.length === 0 ? (
683→ 683→ <div className="text-center py-4 text-[11px] text-muted-foreground/40">
684→ 684→ <Trophy className="h-6 w-6 mx-auto mb-1 opacity-30" />
685→ 685→ 尚无记录。完成今日或本周信标即可登榜。
686→ 686→ </div>
687→ 687→ ) : (
688→ 688→ <div className="space-y-0.5 max-h-[180px] overflow-y-auto echo-scroll">
689→ 689→ {leaderboard.map((entry, i) => {
690→ 690→ const rb = rankBadge(i);
691→ 691→ const eDiff = BEACON_DIFFICULTY[entry.difficulty];
692→ 692→ const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType];
693→ 693→ const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey();
694→ 694→ return (
695→ 695→ <div
696→ 696→ key={`${entry.timestamp}-${i}`}
697→ 697→ className={`flex items-center gap-2 px-2 py-1 rounded-lg text-[11px] ${
698→ 698→ entry.isWeekly
699→ 699→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
700→ 700→ : isMine
701→ 701→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
702→ 702→ : "hover:bg-white/5"
703→ 703→ }`}
704→ 704→ >
705→ 705→ {/* 排名 */}
706→ 706→ <span className="w-6 flex items-center justify-center">
707→ 707→ {rb ? (
708→ 708→ <rb.icon className="h-3.5 w-3.5" style={{ color: rb.color }} />
709→ 709→ ) : (
710→ 710→ <span className="text-muted-foreground/50 font-mono">{i + 1}</span>
711→ 711→ )}
712→ 712→ </span>
713→ 713→ {/* 类型 + 难度 + 周挑战标记 */}
714→ 714→ <span className="flex items-center gap-1 flex-1 min-w-0">
715→ 715→ <span style={{ color: eDiff.color }} className="font-mono">{eDiff.icon}</span>
716→ 716→ <span className="text-muted-foreground/80 truncate">{eType.label}</span>
717→ 717→ {entry.isWeekly && (
718→ 718→ <span className="text-[9px] font-mono px-1 rounded bg-fuchsia-500/20 text-fuchsia-300 border border-fuchsia-500/40">
719→ 719→ WEEK
720→ 720→ </span>
721→ 721→ )}
722→ 722→ {entry.progress >= 1 && <span className="text-emerald-400/70">✓</span>}
723→ 723→ </span>
724→ 724→ {/* 用时 */}
725→ 725→ <span className="text-muted-foreground/50 font-mono text-[10px] w-16 text-right">
726→ 726→ {Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s
727→ 727→ </span>
728→ 728→ {/* 分数 */}
729→ 729→ <span className="font-mono font-semibold w-12 text-right" style={{ color: eDiff.color }}>
730→ 730→ {formatNum(entry.score)}
731→ 731→ </span>
732→ 732→ </div>
733→ 733→ );
734→ 734→ })}
735→ 735→ </div>
736→ 736→ )}
737→ 737→ </div>
738→ 738→
739→ 739→ {/* 难度图例 */}
740→ 740→ <div className="flex items-center justify-center gap-3 text-[9px] text-muted-foreground/50">
741→ 741→ {DIFFICULTY_ORDER.map((d) => {
742→ 742→ const m = BEACON_DIFFICULTY[d];
743→ 743→ return (
744→ 744→ <span key={d} className="flex items-center gap-1">
745→ 745→ <span style={{ color: m.color }}>{m.icon}</span>
746→ 746→ {m.label} ×{m.mult}
747→ 747→ </span>
748→ 748→ );
749→ 749→ })}
750→ 750→ </div>
751→ 751→ </div>
752→ 752→ );
753→ 753→}
754→ 754→