v0.11: 解码晶体卡片 hover 增强 + StatsPanel selector 性能优化

DecodeArray CrystalCard:
- hover 流光边框动画 (echo-border-flow)
- hover 扫描线动画 (echo-scan)
- T标签/tierLabel 上移至顶部 z-10, 不被底部覆盖层遮挡
- hover 底部显示晶体数值 (+6/+22/+90) + 解码提示
- 晶体图形 hover 旋转 200deg + scale 110%

StatsPanel (page.tsx):
- 全量订阅 useGameStore() → shallow 聚合 selector
- 避免每 tick(250ms) 无关字段变化触发重渲染

globals.css:
- 新增 echo-scan / echo-border-flow keyframes

QA: VLM hover 效果 9/10, lint 零错误
This commit is contained in:
2026-06-24 02:08:13 +00:00
parent 803857298f
commit 43c203ebd2
3 changed files with 75 additions and 8 deletions
+14
View File
@@ -177,3 +177,17 @@
0% { opacity: 0; transform: scale(0.92) translateY(8px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
/* v0.11 晶体卡片 hover 扫描线 */
@keyframes echo-scan {
0% { top: 0%; opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
/* v0.11 晶体卡片 hover 边框流光 */
@keyframes echo-border-flow {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
+27 -1
View File
@@ -33,6 +33,7 @@ import { useGameLoop } from "@/hooks/useGameLoop";
import { useAudioSync } from "@/hooks/useAudio";
import { useGlobalTide } from "@/hooks/useGlobalTide";
import { useGameStore } from "@/store/gameStore";
import { shallow } from "zustand/shallow";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import {
@@ -567,7 +568,32 @@ function CrystalYieldPanel() {
}
function StatsPanel() {
const s = useGameStore();
// v0.11 性能优化:用 shallow 聚合 selector 替代全量订阅 useGameStore()
// 避免每 tick(250ms)因无关字段变化触发重渲染。仅当所列字段引用变化才重渲染。
const s = useGameStore(
(st) => ({
totalDecoded: st.totalDecoded,
ascensions: st.ascensions,
blueprints: st.blueprints,
tech: st.tech,
fragments: st.fragments,
achievements: st.achievements,
constellation: st.constellation,
contact: st.contact,
crystalsPerSec: st.crystalsPerSec,
crystalCap: st.crystalCap,
insightMult: st.insightMult,
totalExpeditions: st.totalExpeditions,
bossKills: st.bossKills,
starTidesEncountered: st.starTidesEncountered,
chronicle: st.chronicle,
energy: st.energy,
energyMax: st.energyMax,
attributes: st.attributes,
pendingAttrPoints: st.pendingAttrPoints,
}),
shallow
);
const achCount = Object.values(s.achievements).filter(Boolean).length;
// 深空信标本地排行榜最高分(独立 localStorage
const [beaconBest, setBeaconBest] = useState<number | null>(null);
+34 -7
View File
@@ -408,13 +408,33 @@ function CrystalCard({
return (
<button
onClick={onClick}
className="group relative aspect-square rounded-2xl border border-white/10 bg-black/40 hover:border-white/30 transition-all duration-300 hover:scale-[1.03] focus:outline-none overflow-hidden"
className="group relative aspect-square rounded-2xl border border-white/10 bg-black/40 hover:border-white/40 transition-all duration-300 hover:scale-[1.04] focus:outline-none overflow-hidden"
style={{ boxShadow: `inset 0 0 24px ${tierColor}22` }}
>
{/* v0.11 hover 流光边框 */}
<div
className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
style={{
background: `linear-gradient(120deg, transparent 30%, ${tierColor}40 50%, transparent 70%)`,
backgroundSize: "200% 100%",
animation: "echo-border-flow 2s linear infinite",
mixBlendMode: "screen",
}}
/>
{/* v0.11 hover 扫描线 */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none overflow-hidden">
<div
className="absolute inset-x-0 h-px"
style={{
background: `linear-gradient(90deg, transparent, ${tierColor}, transparent)`,
animation: "echo-scan 1.8s ease-in-out infinite",
}}
/>
</div>
{/* 晶体图形 */}
<div className="absolute inset-0 flex items-center justify-center">
<div
className="relative h-12 w-12 transition-transform duration-500 group-hover:rotate-180"
className="relative h-12 w-12 transition-transform duration-500 group-hover:rotate-[200deg] group-hover:scale-110"
style={{ filter: `drop-shadow(0 0 12px ${tierColor}88)` }}
>
<div
@@ -430,15 +450,22 @@ function CrystalCard({
/>
</div>
</div>
{/* 标签 */}
<div className="absolute top-1.5 left-1.5 text-[10px] font-mono px-1.5 py-0.5 rounded bg-black/50" style={{ color: tierColor }}>
{/* v0.11 标签上移至顶部,z-10 避免被底部覆盖层遮挡 */}
<div
className="absolute top-1.5 left-1.5 z-10 text-[10px] font-mono font-semibold px-1.5 py-0.5 rounded bg-black/60 backdrop-blur-sm border"
style={{ color: tierColor, borderColor: `${tierColor}40` }}
>
T{crystal.tier}
</div>
<div className="absolute bottom-1.5 right-1.5 text-[10px] text-white/70">
<div className="absolute top-1.5 right-1.5 z-10 text-[9px] text-white/60 font-mono tracking-wider">
{tierLabel}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-end justify-center pb-2">
<span className="text-[11px] font-medium text-white"> </span>
{/* v0.11 hover 底部显示晶体数值 + 解码提示 */}
<div className="absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-black/85 via-black/50 to-transparent pt-4 pb-1.5 px-1 text-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="text-[12px] font-mono font-bold leading-none" style={{ color: tierColor }}>
+{crystal.value}
</div>
<div className="text-[8px] text-white/60 uppercase tracking-[0.15em] mt-0.5"> </div>
</div>
</button>
);