Files
controller-v2/client/src/pages/VUPage.tsx
T

109 lines
4.0 KiB
TypeScript
Raw Normal View History

/* ============================================================
VU PAGE — VU Meter Gallery
Design: Reference luxsin_x8_VU表.png
- 3-column grid of VU style cards
- Active = cyan check circle below card
- Inactive = empty circle
============================================================ */
import { useDevice } from "@/contexts/DeviceContext";
import { cn } from "@/lib/utils";
2026-04-22 14:34:23 +08:00
import BottomNav from "@/components/BottomNav";
import { Check, ChevronLeft } from "lucide-react";
import { useLocation } from "wouter";
import { useMemo } from "react";
import localeZh from "@/locales/data-zh.json";
import localeEn from "@/locales/data-en.json";
import { resolveLocalePack } from "@/locales/resolveLocale";
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
type VUPageLocale = NonNullable<(typeof localeZh)["vuPage"]>;
interface VUStyle {
index: number;
name: string;
preview: string;
}
const VU_STYLES: VUStyle[] = Array.from({ length: 16 }, (_, i) => ({
index: i,
name: `VU ${i + 1}`,
preview: `${import.meta.env.BASE_URL}images/vu/vu${i + 1}.png`,
}));
export default function VUPage() {
const [, setLocation] = useLocation();
const { deviceState, updateSetting } = useDevice();
const currentVU = deviceState?.vu ?? 0;
const vuPageText = useMemo((): VUPageLocale => {
const loc = resolveLocalePack(deviceState?.language);
return (loc.vuPage ?? localeEn.vuPage ?? {}) as VUPageLocale;
}, [deviceState?.language]);
const pageTitle = usePageTitleWithConnection(vuPageText.title ?? "VU");
return (
<div className="min-h-screen bg-black">
<div className="page-header">
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
<ChevronLeft size={24} />
</button>
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
{pageTitle}
</h1>
<div className="w-8" />
</div>
<div className="px-4 pb-32 pt-4">
<div className="grid grid-cols-3 gap-x-2 gap-y-4">
{VU_STYLES.map((style) => {
const isActive = currentVU === style.index;
return (
<div key={style.index} className="flex flex-col items-center gap-1.5 min-w-0">
<div
className={cn(
"ios-list-group p-1.5 cursor-pointer transition-all duration-200 w-full",
isActive && "border-[#00FFF6]/30"
)}
style={isActive ? { borderColor: "rgba(0,255,246,0.3)", boxShadow: "0 0 16px rgba(0,255,246,0.08)" } : {}}
onClick={() => updateSetting({ vu: style.index })}
>
<img
src={style.preview}
alt={style.name}
className="w-full h-[4.5rem] sm:h-24 object-contain rounded-lg bg-black/20"
/>
</div>
<button
type="button"
onClick={() => updateSetting({ vu: style.index })}
className={cn(
"w-6 h-6 rounded-full flex items-center justify-center transition-all duration-200 shrink-0",
isActive
? "bg-[#00FFF6] text-black shadow-[0_0_10px_rgba(0,255,246,0.45)]"
: "border-2 border-white/20 text-transparent"
)}
aria-label={isActive ? `${style.name}(已选)` : `选择${style.name}`}
>
{isActive && <Check size={12} strokeWidth={3} />}
</button>
<p
className={cn(
"text-center text-[11px] sm:text-xs font-medium truncate w-full px-0.5",
isActive ? "text-[#00FFF6]" : "text-white/60"
)}
>
{style.name}
</p>
</div>
);
})}
</div>
</div>
2026-04-22 14:34:23 +08:00
<BottomNav />
</div>
);
}