Files
app/wei-ai-demo/pages/ManualControl.tsx
2026-01-28 19:10:19 +08:00

331 lines
15 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import {
Activity,
RotateCw,
Bluetooth,
Battery,
ChevronLeft,
Zap,
Sliders,
Waves,
Power
} from 'lucide-react';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
type ViewMode = 'hub' | 'free' | 'pattern';
type DeviceState = 'disconnected' | 'connecting' | 'connected';
interface Pattern {
id: string;
name: string;
icon: React.ReactNode;
color: string;
}
const PATTERNS: Pattern[] = [
{ id: 'pulse', name: '脉冲跳动', icon: <Activity size={18} />, color: '#C084FC' },
{ id: 'wave', name: '深海潮汐', icon: <Waves size={18} />, color: '#60A5FA' },
{ id: 'climb', name: '登峰造极', icon: <RotateCw size={18} />, color: '#34D399' },
{ id: 'storm', name: '雷雨风暴', icon: <Zap size={18} />, color: '#FBBF24' },
{ id: 'chaos', name: '随机漫步', icon: <Sliders size={18} />, color: '#F472B6' },
{ id: 'sos', name: 'SOS', icon: <Power size={18} />, color: '#F87171' },
];
const DeviceCard: React.FC<{
status: DeviceState;
onConnect: () => void;
onDisconnect: () => void;
}> = ({ status, onConnect, onDisconnect }) => {
return (
<div className="w-full glass-panel p-5 rounded-3xl mb-6 bg-white/10 border border-white/20 relative overflow-hidden shadow-lg">
<div className="absolute -right-6 -top-6 text-white/5">
<Bluetooth size={120} />
</div>
<div className="flex justify-between items-start relative z-10">
<div>
<h3 className="text-white font-bold text-lg flex items-center gap-2">
Link-X Pro
{status === 'connected' && (
<span className="text-[10px] font-bold bg-[#34D399]/20 text-[#34D399] px-2 py-0.5 rounded-full border border-[#34D399]/30 leading-none mt-0.5">
ONLINE
</span>
)}
</h3>
<p className="text-white/60 text-xs font-mono mt-1">
{status === 'disconnected' ? '设备未连接' : status === 'connecting' ? '正在搜索信号...' : 'ID: 884-X9-01'}
</p>
</div>
<button
onClick={status === 'connected' ? onDisconnect : onConnect}
className={`px-5 py-2 rounded-full text-xs font-bold transition-all ${
status === 'connected'
? 'bg-white/10 text-white/70 border border-white/10'
: status === 'connecting'
? 'bg-white/20 text-white animate-pulse'
: 'bg-white text-[#2e1065] shadow-lg hover:shadow-xl'
}`}
>
{status === 'connected' ? '断开' : status === 'connecting' ? '连接中' : '连接设备'}
</button>
</div>
{status === 'connected' && (
<div className="mt-6 flex items-center gap-6 relative z-10">
<div className="flex items-center gap-2">
<Battery size={16} className="text-[#34D399]" />
<span className="text-sm font-mono text-white">85%</span>
</div>
</div>
)}
</div>
);
};
const FreeControlView: React.FC<{ onBack: () => void }> = ({ onBack }) => {
const [intensity, setIntensity] = useState(0);
const [isClimax, setIsClimax] = useState(false);
const controlRef = useRef<HTMLDivElement>(null);
const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
if (!controlRef.current) return;
const rect = controlRef.current.getBoundingClientRect();
const clientY = 'touches' in e ? e.touches[0].clientY : (e as React.MouseEvent).clientY;
const relativeY = Math.max(0, Math.min(1, (rect.bottom - clientY) / rect.height));
setIntensity(Math.round(relativeY * 100));
};
const handleClimax = () => {
setIsClimax(true);
setIntensity(100);
if (window.navigator.vibrate) window.navigator.vibrate(500);
setTimeout(() => {
setIsClimax(false);
setIntensity(20);
}, 3000);
};
return (
<div className="h-full flex flex-col">
<div className="flex items-center mb-6">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white">
<ChevronLeft size={24} />
</button>
<h2 className="text-lg font-bold text-white ml-2"></h2>
</div>
<div className="flex-1 flex flex-col items-center justify-center">
<div
ref={controlRef}
className="relative w-48 h-[60vh] rounded-[32px] border border-white/20 bg-white/5 shadow-2xl overflow-hidden touch-none backdrop-blur-xl"
onTouchMove={handleInteraction}
onMouseDown={(e) => e.buttons === 1 && handleInteraction(e)}
onMouseMove={(e) => e.buttons === 1 && handleInteraction(e)}
>
<div
className={`absolute bottom-0 w-full transition-all duration-75 ease-linear backdrop-blur-md flex items-start justify-center pt-2 ${isClimax ? 'bg-red-500/50' : 'bg-[#C084FC]/50'}`}
style={{ height: `${intensity}%` }}
>
<div className={`w-full h-[3px] ${isClimax ? 'bg-red-500 shadow-[0_0_20px_white]' : 'bg-white shadow-[0_0_15px_white]'}`}></div>
</div>
<div className="absolute inset-0 flex flex-col justify-between p-6 pointer-events-none">
<span className="text-xs text-white/40 font-mono mx-auto">MAX</span>
<div className="flex-1 flex items-center justify-center">
<span className={`text-5xl font-bold font-mono tracking-tighter transition-colors drop-shadow-lg ${isClimax ? 'text-red-200' : 'text-white'}`}>
{intensity}
</span>
</div>
<span className="text-xs text-white/40 font-mono mx-auto">OFF</span>
</div>
</div>
<p className="mt-6 text-white/60 text-xs font-mono tracking-wider"></p>
</div>
<button
onClick={handleClimax}
disabled={isClimax}
className={`w-full py-4 mt-6 rounded-2xl font-bold tracking-widest text-white transition-all active:scale-95 shadow-lg ${
isClimax
? 'bg-red-500 animate-pulse cursor-not-allowed'
: 'bg-white/10 border border-red-400/50 text-red-300 hover:bg-red-500/20'
}`}
>
{isClimax ? 'MAX OUTPUT...' : '一键爆发'}
</button>
</div>
);
};
const PatternControlView: React.FC<{ onBack: () => void }> = ({ onBack }) => {
const [activePattern, setActivePattern] = useState<string | null>(null);
const [globalIntensity, setGlobalIntensity] = useState(50);
const generateWaveData = (patternId: string | null) => {
if (!patternId) return Array(20).fill({ v: 10 });
return Array.from({ length: 20 }, (_, i) => ({
v: patternId === 'pulse' ? (i % 5 === 0 ? 90 : 20) :
patternId === 'wave' ? 40 + Math.sin(i) * 30 :
Math.random() * 80 + 10
}));
};
const [waveData, setWaveData] = useState(generateWaveData(null));
useEffect(() => {
if (activePattern) {
const interval = setInterval(() => {
setWaveData(prev => {
const next = [...prev.slice(1), { v: Math.random() * (globalIntensity) + 20 }];
return next;
});
}, 100);
return () => clearInterval(interval);
} else {
setWaveData(Array(20).fill({ v: 10 }));
}
}, [activePattern, globalIntensity]);
return (
<div className="h-full flex flex-col">
<div className="flex items-center mb-6">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white">
<ChevronLeft size={24} />
</button>
<h2 className="text-lg font-bold text-white ml-2"></h2>
</div>
<div className="h-40 w-full glass-panel rounded-3xl mb-8 p-4 flex items-center justify-center relative overflow-hidden bg-white/5 border border-white/10">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={waveData}>
<defs>
<linearGradient id="waveGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#C084FC" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#C084FC" stopOpacity={0}/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="v"
stroke="#E9D5FF"
strokeWidth={3}
fill="url(#waveGradient)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mb-8">
<div className="flex justify-between text-xs text-white/60 mb-2 uppercase tracking-wider">
<span></span>
<span>{globalIntensity}%</span>
</div>
<input
type="range"
min="0"
max="100"
value={globalIntensity}
onChange={(e) => setGlobalIntensity(parseInt(e.target.value))}
className="w-full h-2 bg-white/20 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-lg"
/>
</div>
<div className="grid grid-cols-2 gap-3 overflow-y-auto no-scrollbar pb-6">
{PATTERNS.map(p => {
const isActive = activePattern === p.id;
return (
<button
key={p.id}
onClick={() => setActivePattern(isActive ? null : p.id)}
className={`p-4 rounded-2xl border transition-all duration-300 flex flex-col items-center gap-2 ${
isActive
? 'bg-white text-[#2e1065] border-white shadow-lg scale-[1.02]'
: 'bg-white/5 border-white/10 text-white/60 hover:bg-white/10'
}`}
>
<div className={`p-2 rounded-full ${isActive ? 'bg-[#2e1065]/10 text-[#2e1065]' : 'bg-white/5'}`}>
{p.icon}
</div>
<span className="text-sm font-bold">
{p.name}
</span>
</button>
)
})}
</div>
</div>
);
};
const ManualControl: React.FC = () => {
const [view, setView] = useState<ViewMode>('hub');
const [deviceStatus, setDeviceStatus] = useState<DeviceState>('disconnected');
const connectDevice = () => {
setDeviceStatus('connecting');
setTimeout(() => setDeviceStatus('connected'), 1500);
};
const disconnectDevice = () => {
setDeviceStatus('disconnected');
};
if (view === 'free') return <div className="pt-4 px-6 h-full"><FreeControlView onBack={() => setView('hub')} /></div>;
if (view === 'pattern') return <div className="pt-4 px-6 h-full"><PatternControlView onBack={() => setView('hub')} /></div>;
return (
<div className="pt-4 px-6 h-full flex flex-col">
<div className="mb-2">
<DeviceCard
status={deviceStatus}
onConnect={connectDevice}
onDisconnect={disconnectDevice}
/>
</div>
<h2 className="text-xs font-bold text-white/40 uppercase tracking-widest mb-3"></h2>
<div className="grid grid-cols-1 gap-4">
<button
onClick={() => setView('free')}
disabled={deviceStatus !== 'connected'}
className={`group relative overflow-hidden h-36 rounded-3xl flex items-center px-8 transition-all ${
deviceStatus === 'connected'
? 'bg-white/10 border border-white/20 active:scale-[0.98] backdrop-blur-md'
: 'bg-white/5 border border-white/5 opacity-50 cursor-not-allowed'
}`}
>
<div className="w-14 h-14 rounded-full bg-[#C084FC]/20 flex items-center justify-center text-[#C084FC] group-hover:scale-110 transition-transform">
<Sliders size={28} />
</div>
<div className="ml-5 text-left">
<h3 className="text-xl font-bold text-white group-hover:text-[#C084FC] transition-colors"></h3>
<p className="text-xs text-white/50 mt-1"> </p>
</div>
</button>
<button
onClick={() => setView('pattern')}
disabled={deviceStatus !== 'connected'}
className={`group relative overflow-hidden h-36 rounded-3xl flex items-center px-8 transition-all ${
deviceStatus === 'connected'
? 'bg-white/10 border border-white/20 active:scale-[0.98] backdrop-blur-md'
: 'bg-white/5 border border-white/5 opacity-50 cursor-not-allowed'
}`}
>
<div className="w-14 h-14 rounded-full bg-[#60A5FA]/20 flex items-center justify-center text-[#60A5FA] group-hover:scale-110 transition-transform">
<Waves size={28} />
</div>
<div className="ml-5 text-left">
<h3 className="text-xl font-bold text-white group-hover:text-[#60A5FA] transition-colors"></h3>
<p className="text-xs text-white/50 mt-1">9</p>
</div>
</button>
</div>
</div>
);
};
export default ManualControl;