性能(调研 7-A 落地): - 新增 AnimatedNumber 组件(useRef + rAF lerp,60fps 平滑过渡,不触发 React re-render) - CrystalOrb 拆 OrbStats 子组件:主体(Canvas+button)不再每 tick 重渲染 crystals/crystalCap/crystalsPerSec 订阅下沉到 OrbStats,用 AnimatedNumber 平滑 - page.tsx StatsPanel: shallow → useShallow(修复 zustand v5 getSnapshot 崩溃) 数值平衡(调研 7-A 落地,防速通): - 新增 src/lib/game/softcap.ts:applySoftcap/applyCostSoftcap/effectiveDecodedForPrestige - 飞升蓝图公式改 sqrt softcap(阈值 40,power 0.5) 前 2 蓝图照常给(新手友好),3-6 蓝图需更多解码(防速通) - totalDecoded=40 → 2 蓝图(与旧一致) - totalDecoded=80 → 2 蓝图(旧=4) - totalDecoded=360 → 6 蓝图(封顶) 非商用资源落地(调研 7-B): - 安装 @fontsource/share-tech-mono + @fontsource/vt323(SIL OFL 自托管) - globals.css 加 --font-tech / --font-terminal 变量 - ResourceBar 数值用 font-tech(Share Tech Mono 星舰仪表盘感) - ArchaeoLogTicker 文本用 font-terminal(VT323 终端绿字)+ GiScrollUnfurled 图标 - 安装 react-icons,引入 game-icons.net 子集(MIT) 交互优化: - ExpeditionPanel 加探险目的说明(3 资源卡片)+ 节点类型图例(issue #3 反馈) - BeaconPanel 布局微调 验证: - lint 零错误 - VLM 首页 QA 8.5/10(字体/配色/页脚贴底均良好) - 点击脉冲交互正常(数值增加、浮动数字正确消失) - dev.log 无运行时错误
440 lines
20 KiB
TypeScript
Executable File
440 lines
20 KiB
TypeScript
Executable File
"use client";
|
||
// 回响星核 / Echo Nexus — 遗迹探险面板(v0.2 肉鸽系统,v0.9 工单修复)
|
||
import { useState, useEffect } from "react";
|
||
import { useGameStore } from "@/store/gameStore";
|
||
import { useToast } from "@/hooks/use-toast";
|
||
import { sfx } from "@/hooks/useAudio";
|
||
import { EXPEDITION_CONFIG, combatWinRate, computeEnergyRegenInterval } from "@/lib/game/expedition";
|
||
import { formatNum } from "@/lib/game/config";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Progress } from "@/components/ui/progress";
|
||
import {
|
||
Swords,
|
||
Gem,
|
||
HelpCircle,
|
||
Puzzle,
|
||
Skull,
|
||
Heart,
|
||
Zap,
|
||
ChevronRight,
|
||
LogOut,
|
||
Rocket,
|
||
Sparkles,
|
||
} from "lucide-react";
|
||
import type { ExpeditionNodeType } from "@/lib/game/types";
|
||
|
||
const NODE_META: Record<
|
||
ExpeditionNodeType,
|
||
{ icon: React.ComponentType<{ className?: string }>; color: string; label: string; bg: string }
|
||
> = {
|
||
combat: { icon: Swords, color: "#fb7185", label: "战斗", bg: "rgba(251,113,133,0.15)" },
|
||
treasure: { icon: Gem, color: "#fbbf24", label: "宝藏", bg: "rgba(251,191,36,0.15)" },
|
||
choice: { icon: HelpCircle, color: "#a5f3fc", label: "抉择", bg: "rgba(165,243,252,0.15)" },
|
||
puzzle: { icon: Puzzle, color: "#e879f9", label: "解谜", bg: "rgba(232,121,249,0.15)" },
|
||
rest: { icon: Heart, color: "#34d399", label: "休整", bg: "rgba(52,211,153,0.15)" },
|
||
boss: { icon: Skull, color: "#f43f5e", label: "BOSS", bg: "rgba(244,63,94,0.2)" },
|
||
};
|
||
|
||
export function ExpeditionPanel() {
|
||
const activeExpedition = useGameStore((s) => s.activeExpedition);
|
||
const energy = useGameStore((s) => s.energy);
|
||
const energyMax = useGameStore((s) => s.energyMax);
|
||
const lastEnergyTick = useGameStore((s) => s.lastEnergyTick);
|
||
const expeditionLog = useGameStore((s) => s.expeditionLog);
|
||
const totalExpeditions = useGameStore((s) => s.totalExpeditions);
|
||
const lastExpeditionSummary = useGameStore((s) => s.lastExpeditionSummary);
|
||
// v0.8.1 动态能量恢复间隔(技术 + 探索力属性缩短)
|
||
const regenIntervalSec = useGameStore((s) => computeEnergyRegenInterval(s));
|
||
const startExpedition = useGameStore((s) => s.startExpedition);
|
||
const resolveCurrentNode = useGameStore((s) => s.resolveCurrentNode);
|
||
const advanceNode = useGameStore((s) => s.advanceNode);
|
||
const abortExpedition = useGameStore((s) => s.abortExpedition);
|
||
const { toast } = useToast();
|
||
const [lastLog, setLastLog] = useState<string | null>(null);
|
||
const [now, setNow] = useState(() => Date.now());
|
||
// v0.9 工单修复:能量恢复实时倒计时(每秒刷新)
|
||
useEffect(() => {
|
||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||
return () => clearInterval(id);
|
||
}, []);
|
||
|
||
// 能量恢复进度(v0.8.1 动态间隔,v0.9 实时刷新)
|
||
const regenMs = regenIntervalSec * 1000;
|
||
const elapsed = now - lastEnergyTick;
|
||
const regenProgress = energy >= energyMax ? 100 : Math.min(100, (elapsed / regenMs) * 100);
|
||
const regenBoosted = regenIntervalSec < EXPEDITION_CONFIG.energyRegenSec;
|
||
const nextEnergyInSec = energy >= energyMax ? 0 : Math.max(0, Math.ceil((regenMs - elapsed) / 1000));
|
||
const canStart = energy >= EXPEDITION_CONFIG.energyCost;
|
||
|
||
const handleStart = () => {
|
||
const res = startExpedition();
|
||
if (!res.ok) {
|
||
toast({ title: "无法出发", description: res.reason, variant: "destructive" });
|
||
} else {
|
||
sfx("expeditionStart");
|
||
toast({ title: "探险队出发", description: "深入遗迹,谨慎前行。" });
|
||
setLastLog(null);
|
||
}
|
||
};
|
||
|
||
const handleResolve = () => {
|
||
const result = resolveCurrentNode();
|
||
if (!result) return;
|
||
setLastLog(result.log);
|
||
if (result.ended) {
|
||
if (result.endReason === "victory") sfx("expeditionVictory");
|
||
else sfx("expeditionDefeat");
|
||
} else {
|
||
sfx("expeditionNode");
|
||
}
|
||
toast({
|
||
title: result.ended
|
||
? result.endReason === "victory" ? "✦ 探险胜利!" : "探险失败"
|
||
: "节点结算",
|
||
description: result.log,
|
||
variant: result.endReason === "defeat" ? "destructive" : "default",
|
||
});
|
||
};
|
||
|
||
const handleAdvance = () => {
|
||
advanceNode();
|
||
setLastLog(null);
|
||
};
|
||
|
||
const handleAbort = () => {
|
||
abortExpedition();
|
||
toast({ title: "探险队撤退", description: "保留已获奖励,安全返回。" });
|
||
setLastLog(null);
|
||
};
|
||
|
||
// ===== 无活跃探险:展示入口 =====
|
||
// v0.10 工单 #2 修复:探险结束自动清空 activeExpedition,无需 justFinished 判断
|
||
if (!activeExpedition || activeExpedition.finished) {
|
||
return (
|
||
<div className="h-full overflow-y-auto echo-scroll space-y-3 pr-0.5">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
||
<Rocket className="h-4 w-4 text-amber-400" />
|
||
遗迹探险
|
||
</h3>
|
||
<span className="text-[10px] text-muted-foreground">已探索 {totalExpeditions} 次</span>
|
||
</div>
|
||
|
||
{/* v0.9 工单修复:上次探险结果摘要(不限时间,常驻展示) */}
|
||
{lastExpeditionSummary && (
|
||
<div
|
||
className={`rounded-xl border p-2.5 relative overflow-hidden ${
|
||
lastExpeditionSummary.result === "victory"
|
||
? "border-emerald-400/40 bg-emerald-950/20"
|
||
: lastExpeditionSummary.result === "defeat"
|
||
? "border-rose-400/40 bg-rose-950/20"
|
||
: "border-amber-400/30 bg-amber-950/10"
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="text-[11px] font-semibold flex items-center gap-1">
|
||
{lastExpeditionSummary.result === "victory" ? "✦ 上次探险:胜利" : lastExpeditionSummary.result === "defeat" ? "✕ 上次探险:失败" : "→ 上次探险:撤退"}
|
||
</span>
|
||
<span className="text-[9px] text-muted-foreground font-mono">{lastExpeditionSummary.nodeCount} 节点</span>
|
||
</div>
|
||
<div className="flex items-center gap-2.5 text-[10px]">
|
||
{lastExpeditionSummary.rewards.crystals > 0 && (
|
||
<span className="text-emerald-300">+{formatNum(lastExpeditionSummary.rewards.crystals)} 晶体</span>
|
||
)}
|
||
{lastExpeditionSummary.rewards.insights > 0 && (
|
||
<span className="text-amber-300">+{lastExpeditionSummary.rewards.insights} 洞见</span>
|
||
)}
|
||
{lastExpeditionSummary.rewards.contact > 0 && (
|
||
<span className="text-fuchsia-300">+{lastExpeditionSummary.rewards.contact.toFixed(1)} 接触</span>
|
||
)}
|
||
{lastExpeditionSummary.rewards.crystals === 0 && lastExpeditionSummary.rewards.insights === 0 && (
|
||
<span className="text-muted-foreground/60">无奖励</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 能量条 */}
|
||
<div className="rounded-xl border border-amber-400/20 bg-amber-950/20 p-3">
|
||
<div className="flex items-center justify-between mb-1.5">
|
||
<div className="flex items-center gap-1.5">
|
||
<Zap className="h-3.5 w-3.5 text-amber-400" />
|
||
<span className="text-xs text-amber-200">探险能量</span>
|
||
</div>
|
||
<span className="text-xs font-mono text-amber-100">
|
||
{energy} / {energyMax}
|
||
</span>
|
||
</div>
|
||
{energy < energyMax ? (
|
||
<>
|
||
<Progress
|
||
value={regenProgress}
|
||
className="h-1.5 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-amber-500 [&>div]:to-yellow-300"
|
||
/>
|
||
<p className="text-[10px] text-muted-foreground/70 mt-1 flex items-center gap-1">
|
||
<span>下一点能量</span>
|
||
<span className="text-amber-300 font-mono font-semibold">{nextEnergyInSec}s</span>
|
||
<span>后恢复</span>
|
||
{regenBoosted && (
|
||
<span className="text-emerald-400/80 ml-1">⚡ {regenIntervalSec.toFixed(0)}s/点 (已加速)</span>
|
||
)}
|
||
</p>
|
||
</>
|
||
) : (
|
||
<p className="text-[10px] text-amber-300/80 mt-0.5">能量已满,可立即出发</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 出发按钮 */}
|
||
<div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-amber-400/20 bg-black/30 p-4 py-5 relative">
|
||
<div className="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-amber-500/10 blur-2xl" />
|
||
<div className="relative">
|
||
<div className={`h-14 w-14 rounded-full bg-gradient-to-br from-amber-500/30 to-rose-500/20 flex items-center justify-center mx-auto ${canStart ? "animate-pulse" : ""}`} style={{ boxShadow: "0 0 30px rgba(251,191,36,0.3)" }}>
|
||
<Rocket className="h-6 w-6 text-amber-300" />
|
||
</div>
|
||
</div>
|
||
<div className="text-center space-y-1">
|
||
<p className="text-sm font-medium text-amber-100">深入以太遗迹</p>
|
||
<p className="text-[11px] text-muted-foreground/80 max-w-[260px] leading-relaxed">
|
||
程序化生成的 5-7 节点路径,每节点触发战斗/宝藏/抉择/解谜/休整事件,终点 BOSS 必给记忆碎片。
|
||
</p>
|
||
</div>
|
||
|
||
{/* v0.13 探险目的说明(issue #3 反馈"不知道有啥用") */}
|
||
<div className="w-full grid grid-cols-3 gap-1.5 text-[10px]">
|
||
<div className="rounded-md border border-emerald-400/20 bg-emerald-950/15 px-1.5 py-1 text-center">
|
||
<div className="text-emerald-300 font-semibold">晶体/洞见</div>
|
||
<div className="text-muted-foreground/60 text-[9px] mt-0.5">主线成长资源</div>
|
||
</div>
|
||
<div className="rounded-md border border-fuchsia-400/20 bg-fuchsia-950/15 px-1.5 py-1 text-center">
|
||
<div className="text-fuchsia-300 font-semibold">接触进度</div>
|
||
<div className="text-muted-foreground/60 text-[9px] mt-0.5">飞升必需</div>
|
||
</div>
|
||
<div className="rounded-md border border-amber-400/20 bg-amber-950/15 px-1.5 py-1 text-center">
|
||
<div className="text-amber-300 font-semibold">记忆碎片</div>
|
||
<div className="text-muted-foreground/60 text-[9px] mt-0.5">BOSS 必给</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Button
|
||
onClick={handleStart}
|
||
disabled={!canStart}
|
||
className="bg-gradient-to-r from-amber-600 to-rose-500 hover:from-amber-500 hover:to-rose-400 text-white border-0 w-full"
|
||
>
|
||
<Rocket className="h-4 w-4 mr-1.5" />
|
||
{canStart ? `出发(消耗 ${EXPEDITION_CONFIG.energyCost} 能量)` : `能量不足(${nextEnergyInSec}s 后恢复)`}
|
||
</Button>
|
||
|
||
{/* v0.13 节点类型图例(让玩家知道 6 种节点) */}
|
||
<div className="w-full flex items-center justify-center gap-2 flex-wrap text-[9px] text-muted-foreground/60 pt-1 border-t border-white/5">
|
||
{Object.entries(NODE_META).map(([key, meta]) => {
|
||
const Icon = meta.icon;
|
||
return (
|
||
<span key={key} className="inline-flex items-center gap-0.5">
|
||
<Icon className="h-2.5 w-2.5" style={{ color: meta.color }} />
|
||
{meta.label}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 探险日志 */}
|
||
{expeditionLog.length > 0 && (
|
||
<div className="rounded-xl border border-white/5 bg-black/20 p-2 max-h-28 overflow-y-auto echo-scroll">
|
||
<div className="text-[10px] text-muted-foreground/60 mb-1 px-1">最近探险记录</div>
|
||
{expeditionLog.slice(0, 4).map((entry, i) => (
|
||
<div key={i} className="text-[10px] text-muted-foreground/70 px-1 py-0.5 border-b border-white/5 last:border-0">
|
||
<span className="text-amber-300/60">›</span> {entry.result}
|
||
{entry.rewards && <span className="text-emerald-300/70 ml-1">{entry.rewards}</span>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ===== 活跃探险:展示地图 + 事件 =====
|
||
const exp = activeExpedition;
|
||
const currentNode = exp.nodes[exp.currentNode];
|
||
const nodeMeta = NODE_META[currentNode.type];
|
||
const NodeIcon = nodeMeta.icon;
|
||
const hpPct = (exp.hp / exp.maxHp) * 100;
|
||
const pathProgress = ((exp.currentNode + (currentNode.cleared ? 1 : 0)) / exp.nodes.length) * 100;
|
||
|
||
return (
|
||
<div className="h-full overflow-y-auto echo-scroll space-y-2.5 pr-0.5">
|
||
{/* 头部:生命 + 路径进度 */}
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-1.5">
|
||
<Rocket className="h-3.5 w-3.5 text-amber-400" />
|
||
<span className="text-xs font-semibold">遗迹探险</span>
|
||
<span className="text-[10px] text-muted-foreground">第 {exp.currentNode + 1}/{exp.nodes.length} 节点</span>
|
||
</div>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="text-[10px] text-muted-foreground">探险力</span>
|
||
<span className="text-xs font-mono font-semibold text-amber-300">{exp.power}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 生命条 */}
|
||
<div>
|
||
<div className="flex items-center justify-between mb-0.5">
|
||
<div className="flex items-center gap-1">
|
||
<Heart className="h-3 w-3 text-rose-400" />
|
||
<span className="text-[10px] text-muted-foreground">探险队生命</span>
|
||
</div>
|
||
<span className={`text-[10px] font-mono ${hpPct < 30 ? "text-rose-400" : "text-foreground"}`}>
|
||
{exp.hp} / {exp.maxHp}
|
||
</span>
|
||
</div>
|
||
<Progress
|
||
value={hpPct}
|
||
className="h-1.5 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-rose-500 [&>div]:to-emerald-400"
|
||
/>
|
||
</div>
|
||
|
||
{/* 节点路径图 */}
|
||
<div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
|
||
<div className="flex items-center gap-1 overflow-x-auto echo-scroll pb-1">
|
||
{exp.nodes.map((node, i) => {
|
||
const meta = NODE_META[node.type];
|
||
const Icon = meta.icon;
|
||
const isCurrent = i === exp.currentNode;
|
||
const isCleared = node.cleared;
|
||
const isPast = i < exp.currentNode;
|
||
return (
|
||
<div key={node.id} className="flex items-center shrink-0">
|
||
<div
|
||
className={`relative h-9 w-9 rounded-lg flex items-center justify-center transition-all duration-300 ${
|
||
isCurrent ? "scale-125" : ""
|
||
}`}
|
||
style={{
|
||
background: isCleared || isPast ? "rgba(255,255,255,0.04)" : meta.bg,
|
||
border: isCurrent ? `1.5px solid ${meta.color}` : "1px solid rgba(255,255,255,0.08)",
|
||
boxShadow: isCurrent ? `0 0 12px ${meta.color}66` : "none",
|
||
}}
|
||
title={`${meta.label} · ${node.title}`}
|
||
>
|
||
<Icon className={`h-4 w-4 ${isCleared || isPast ? "opacity-30" : ""}`} style={{ color: isCleared || isPast ? "#666" : meta.color }} />
|
||
{isCleared && (
|
||
<div className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-emerald-500/80 flex items-center justify-center">
|
||
<span className="text-[8px] text-white">✓</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{i < exp.nodes.length - 1 && (
|
||
<ChevronRight className={`h-3 w-3 mx-0.5 ${isPast || (isCurrent && isCleared) ? "text-amber-400/60" : "text-white/15"}`} />
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
<div className="mt-1.5">
|
||
<Progress value={pathProgress} className="h-1 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-amber-500 [&>div]:to-rose-400" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 当前节点事件卡 */}
|
||
<div
|
||
className="rounded-xl border p-3 relative overflow-hidden"
|
||
style={{
|
||
borderColor: `${nodeMeta.color}44`,
|
||
background: `linear-gradient(135deg, ${nodeMeta.color}11, rgba(0,0,0,0.4))`,
|
||
boxShadow: `0 0 20px ${nodeMeta.color}22`,
|
||
}}
|
||
>
|
||
<div className="absolute -top-8 -right-8 h-24 w-24 rounded-full blur-2xl" style={{ background: `${nodeMeta.color}22` }} />
|
||
<div className="relative">
|
||
<div className="flex items-center gap-2 mb-1.5">
|
||
<div
|
||
className="h-8 w-8 rounded-lg flex items-center justify-center"
|
||
style={{ background: nodeMeta.bg, border: `1px solid ${nodeMeta.color}44` }}
|
||
>
|
||
<NodeIcon className="h-4 w-4" style={{ color: nodeMeta.color }} />
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded" style={{ background: nodeMeta.bg, color: nodeMeta.color }}>
|
||
{nodeMeta.label}
|
||
</span>
|
||
{currentNode.type === "combat" && (
|
||
<span className="text-[10px] text-muted-foreground">
|
||
胜率 {Math.round(combatWinRate(exp.power, currentNode.difficulty) * 100)}%
|
||
</span>
|
||
)}
|
||
{currentNode.difficulty > 0 && (
|
||
<span className="text-[10px] text-muted-foreground/60">难度 {currentNode.difficulty}</span>
|
||
)}
|
||
</div>
|
||
<h4 className="text-sm font-semibold mt-0.5" style={{ color: nodeMeta.color }}>
|
||
{currentNode.title}
|
||
</h4>
|
||
</div>
|
||
</div>
|
||
<p className="text-[11px] text-muted-foreground/90 leading-relaxed">
|
||
{currentNode.desc}
|
||
</p>
|
||
|
||
{/* 上次结算结果 */}
|
||
{lastLog && (
|
||
<div className="mt-2 px-2 py-1.5 rounded-lg bg-black/40 border border-white/10">
|
||
<p className="text-[11px] text-foreground/90">{lastLog}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex items-center gap-1.5">
|
||
{!currentNode.cleared ? (
|
||
<Button
|
||
onClick={handleResolve}
|
||
className="flex-1 bg-gradient-to-r from-amber-600 to-rose-500 hover:from-amber-500 hover:to-rose-400 text-white border-0 h-8"
|
||
>
|
||
<Sparkles className="h-3.5 w-3.5 mr-1" />
|
||
探索节点
|
||
</Button>
|
||
) : exp.currentNode < exp.nodes.length - 1 ? (
|
||
<Button
|
||
onClick={handleAdvance}
|
||
className="flex-1 h-8"
|
||
variant="outline"
|
||
>
|
||
<ChevronRight className="h-3.5 w-3.5 mr-1" />
|
||
前进
|
||
</Button>
|
||
) : (
|
||
<div className="flex-1 text-center text-xs text-emerald-300 py-1.5">探险已完成 ✓</div>
|
||
)}
|
||
<Button
|
||
onClick={handleAbort}
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-8 px-2 text-muted-foreground hover:text-rose-300"
|
||
>
|
||
<LogOut className="h-3.5 w-3.5" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 本次奖励累计 */}
|
||
<div className="flex items-center gap-2 text-[10px] px-1">
|
||
<span className="text-muted-foreground/60">本次已获:</span>
|
||
{exp.rewards.crystals > 0 && <span className="text-emerald-300">{formatNum(exp.rewards.crystals)}晶体</span>}
|
||
{exp.rewards.insights > 0 && <span className="text-amber-300">{exp.rewards.insights}洞见</span>}
|
||
{exp.rewards.contact > 0 && <span className="text-fuchsia-300">{exp.rewards.contact.toFixed(1)}接触</span>}
|
||
{(exp.rewards.crystals === 0 && exp.rewards.insights === 0) && (
|
||
<span className="text-muted-foreground/40">暂无</span>
|
||
)}
|
||
</div>
|
||
|
||
<style jsx global>{`
|
||
.echo-scroll::-webkit-scrollbar { width: 4px; height: 4px; }
|
||
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
|
||
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
|
||
`}</style>
|
||
</div>
|
||
);
|
||
}
|