feat: app 端 ui 设计完成

This commit is contained in:
liqupan
2026-01-28 19:10:19 +08:00
commit a4e7898e94
149 changed files with 11302 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
import React, { useState, useEffect } from 'react';
import {
ChevronLeft,
Bluetooth,
RefreshCw,
Zap,
Smartphone,
Wifi,
Battery,
Settings,
Power,
Activity,
CheckCircle2
} from 'lucide-react';
interface DeviceManagerProps {
onBack: () => void;
}
type ScanStatus = 'scanning' | 'found' | 'connected';
interface Device {
id: string;
name: string;
signal: number;
isPaired: boolean;
}
const DeviceManager: React.FC<DeviceManagerProps> = ({ onBack }) => {
const [status, setStatus] = useState<ScanStatus>('scanning');
const [scannedDevices, setScannedDevices] = useState<Device[]>([]);
const [connectStep, setConnectStep] = useState(0); // 0: idle, 1: connecting, 2: success
// Simulation: Scan for devices
useEffect(() => {
if (status === 'scanning') {
const timer = setTimeout(() => {
setScannedDevices([
{ id: '1', name: 'Link-X Pro', signal: 92, isPaired: true },
{ id: '2', name: 'Unknown Signal (Weak)', signal: 30, isPaired: false },
]);
setStatus('found');
}, 2500);
return () => clearTimeout(timer);
}
}, [status]);
const handleConnect = (device: Device) => {
setConnectStep(1);
// Simulate connection delay
setTimeout(() => {
setConnectStep(2);
setTimeout(() => {
setStatus('connected');
setConnectStep(0);
}, 1000);
}, 1500);
};
const handleDisconnect = () => {
if(window.confirm('确定要断开与设备的连接吗?')) {
setStatus('scanning');
setScannedDevices([]);
}
};
const testVibration = () => {
if (window.navigator.vibrate) {
window.navigator.vibrate([200, 100, 200]);
}
alert('已发送震动测试指令');
};
// --- RENDER: Connected View ---
if (status === 'connected') {
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in fade-in duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-6">
{/* Device Visual Card */}
<div className="relative w-full aspect-video bg-[#1C1F26] rounded-2xl border border-white/10 overflow-hidden mb-6 flex items-center justify-center group">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-20"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#8B5CF6]/10 via-transparent to-[#0F1014]"></div>
{/* Glowing Center */}
<div className="relative z-10 flex flex-col items-center">
<div className="w-24 h-24 rounded-full bg-[#0F1014] border-2 border-[#8B5CF6] shadow-[0_0_30px_#8B5CF6] flex items-center justify-center mb-4 relative">
<Bluetooth size={40} className="text-white" />
<div className="absolute inset-0 rounded-full border border-[#8B5CF6] animate-ping opacity-20"></div>
</div>
<h2 className="text-xl font-bold text-white tracking-widest">Link-X Pro</h2>
<div className="flex items-center gap-1.5 mt-1">
<div className="w-1.5 h-1.5 rounded-full bg-[#10B981] animate-pulse"></div>
<span className="text-xs text-[#10B981] font-mono"></span>
</div>
</div>
</div>
{/* Status Grid */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col gap-2">
<div className="flex items-center gap-2 text-[#94A3B8]">
<Battery size={16} />
<span className="text-xs"></span>
</div>
<span className="text-2xl font-bold text-white font-mono">85<span className="text-sm text-[#94A3B8]">%</span></span>
<div className="w-full h-1 bg-white/10 rounded-full overflow-hidden">
<div className="h-full w-[85%] bg-[#10B981]"></div>
</div>
</div>
<div className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col gap-2">
<div className="flex items-center gap-2 text-[#94A3B8]">
<Wifi size={16} />
<span className="text-xs"></span>
</div>
<span className="text-2xl font-bold text-white font-mono">-42<span className="text-sm text-[#94A3B8]">dBm</span></span>
<div className="flex gap-1 h-1 mt-auto">
<div className="flex-1 bg-[#8B5CF6] rounded-full"></div>
<div className="flex-1 bg-[#8B5CF6] rounded-full"></div>
<div className="flex-1 bg-[#8B5CF6] rounded-full"></div>
<div className="flex-1 bg-white/10 rounded-full"></div>
</div>
</div>
</div>
{/* Actions */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3"></h3>
<div className="space-y-3">
<button onClick={testVibration} className="w-full p-4 bg-[#1C1F26] active:bg-[#2D3039] rounded-xl flex items-center justify-between group transition-all border border-white/5 hover:border-[#8B5CF6]/50">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[#8B5CF6]/10 text-[#8B5CF6] group-hover:bg-[#8B5CF6] group-hover:text-white transition-colors">
<Activity size={18} />
</div>
<span className="text-sm text-white font-medium"></span>
</div>
<span className="text-xs text-[#64748B]"></span>
</button>
<button className="w-full p-4 bg-[#1C1F26] active:bg-[#2D3039] rounded-xl flex items-center justify-between group transition-all border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8] group-hover:text-white transition-colors">
<RefreshCw size={18} />
</div>
<div className="flex flex-col items-start">
<span className="text-sm text-white font-medium"></span>
<span className="text-[10px] text-[#64748B]">当前版本: v2.0.4 ()</span>
</div>
</div>
</button>
<button className="w-full p-4 bg-[#1C1F26] active:bg-[#2D3039] rounded-xl flex items-center justify-between group transition-all border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8] group-hover:text-white transition-colors">
<Settings size={18} />
</div>
<span className="text-sm text-white font-medium"></span>
</div>
</button>
</div>
<button
onClick={handleDisconnect}
className="w-full mt-8 p-4 rounded-xl border border-[#F43F5E]/30 text-[#F43F5E] flex items-center justify-center gap-2 text-sm font-bold active:bg-[#F43F5E]/10 transition-colors"
>
<Power size={16} />
</button>
</div>
</div>
);
}
// --- RENDER: Scanning View ---
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8">
{status === 'scanning' && <RefreshCw size={18} className="text-[#8B5CF6] animate-spin" />}
</div>
</div>
<div className="flex-1 flex flex-col items-center pt-12 px-6">
{/* Radar Animation */}
<div className="relative w-64 h-64 flex items-center justify-center mb-12">
{/* Rings */}
<div className="absolute inset-0 rounded-full border border-white/5"></div>
<div className="absolute inset-12 rounded-full border border-white/5"></div>
<div className="absolute inset-24 rounded-full border border-white/5"></div>
{/* Active Scan Line */}
{status === 'scanning' && (
<div className="absolute inset-0 rounded-full animate-[spin_3s_linear_infinite] bg-gradient-to-tr from-transparent via-transparent to-[#8B5CF6]/20 border-t border-[#8B5CF6]/50" style={{ clipPath: 'polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 50%)' }}></div>
)}
{/* Center Icon */}
<div className="relative z-10 w-20 h-20 bg-[#1C1F26] rounded-full flex items-center justify-center border border-white/10 shadow-[0_0_30px_rgba(0,0,0,0.5)]">
<Bluetooth size={32} className={`${status === 'scanning' ? 'text-[#8B5CF6] animate-pulse' : 'text-white'}`} />
</div>
{/* Status Text */}
<div className="absolute -bottom-10 left-0 w-full text-center">
<p className="text-xs text-[#94A3B8] font-mono tracking-wider">
{status === 'scanning' ? 'SCANNING FOR SIGNALS...' : 'SCAN COMPLETE'}
</p>
</div>
</div>
{/* Device List */}
<div className="w-full space-y-3">
{scannedDevices.map(device => (
<button
key={device.id}
onClick={() => handleConnect(device)}
disabled={connectStep !== 0}
className="w-full bg-[#1C1F26]/80 p-4 rounded-xl flex items-center justify-between border border-white/5 active:scale-[0.98] transition-all group hover:border-[#8B5CF6]/50"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-black/40 flex items-center justify-center text-white">
{connectStep === 1 ? <RefreshCw size={18} className="animate-spin text-[#8B5CF6]" /> : <Smartphone size={18} />}
</div>
<div className="flex flex-col items-start">
<span className="text-sm font-bold text-white group-hover:text-[#8B5CF6] transition-colors">{device.name}</span>
<div className="flex items-center gap-2 mt-0.5">
<div className="flex gap-0.5">
{[1,2,3,4].map(i => (
<div key={i} className={`w-0.5 h-2 rounded-full ${i*25 <= device.signal ? 'bg-[#10B981]' : 'bg-[#334155]'}`}></div>
))}
</div>
<span className="text-[10px] text-[#64748B]">RSSI {device.signal}dBm</span>
</div>
</div>
</div>
{device.isPaired && (
<span className="text-[10px] bg-[#8B5CF6]/20 text-[#8B5CF6] px-2 py-0.5 rounded border border-[#8B5CF6]/30">
</span>
)}
</button>
))}
</div>
{/* Help */}
<div className="mt-auto pb-12 flex items-center gap-2 text-[#64748B] text-xs">
<Zap size={12} />
<span></span>
</div>
</div>
{/* Full Screen Loading Overlay for Connection */}
{connectStep === 2 && (
<div className="absolute inset-0 z-50 bg-[#0F1014]/90 backdrop-blur-md flex flex-col items-center justify-center animate-in fade-in">
<CheckCircle2 size={48} className="text-[#10B981] mb-4 animate-bounce" />
<h3 className="text-lg font-bold text-white"></h3>
</div>
)}
</div>
);
};
export default DeviceManager;

View File

@@ -0,0 +1,123 @@
import React, { useState } from 'react';
import { Lock, Flame } from 'lucide-react';
import { MOCK_CHARACTERS } from '../constants';
import { Character } from '../types';
interface DiscoveryProps {
onSelectCharacter: (char: Character) => void;
}
const Discovery: React.FC<DiscoveryProps> = ({ onSelectCharacter }) => {
const [activeFilter, setActiveFilter] = useState('all');
const filters = [
{ id: 'all', label: '全部' },
{ id: 'gentle', label: '温柔治愈' },
{ id: 'dom', label: '主导强势' },
{ id: 'wild', label: '反差/猎奇' },
{ id: 'voice', label: '语音陪聊' },
{ id: 'scenario', label: '场景扮演' },
{ id: 'exclusive', label: '会员限定' },
];
const filteredCharacters = activeFilter === 'all'
? MOCK_CHARACTERS
: MOCK_CHARACTERS.filter(c => {
const tags = c.tags.join('');
if (activeFilter === 'gentle') return tags.includes('治愈') || tags.includes('温顺') || tags.includes('医疗');
if (activeFilter === 'dom') return tags.includes('强势') || tags.includes('调教') || tags.includes('指令');
if (activeFilter === 'wild') return tags.includes('病娇') || tags.includes('神秘') || tags.includes('不稳定') || tags.includes('极乐');
if (activeFilter === 'exclusive') return c.isLocked;
return false;
});
return (
<div className="pb-24 px-4 h-full">
{/* Filter Bar */}
<div className="sticky top-0 z-20 pt-2 pb-2 -mx-4 mb-4">
<div className="relative">
{/* Scroll Container */}
<div className="flex items-center px-6 gap-3 overflow-x-auto no-scrollbar pr-12">
{filters.map(filter => {
const isActive = activeFilter === filter.id;
return (
<button
key={filter.id}
onClick={() => setActiveFilter(filter.id)}
className={`relative px-4 py-1.5 rounded-full border transition-all duration-300 shrink-0 ${
isActive
? 'bg-white text-[#2e1065] font-bold border-white shadow-[0_0_15px_rgba(255,255,255,0.4)]'
: 'bg-white/5 text-white/70 border-white/10 hover:bg-white/10 backdrop-blur-sm'
}`}
>
<span className="text-sm">{filter.label}</span>
</button>
);
})}
</div>
{/* Right Fade */}
<div className="absolute top-0 right-0 h-full w-12 bg-gradient-to-l from-[#4c1d95]/0 to-transparent pointer-events-none"></div>
</div>
</div>
{/* Grid Layout */}
<div className="grid grid-cols-2 gap-4">
{filteredCharacters.map((char) => (
<div
key={char.id}
onClick={() => !char.isLocked && onSelectCharacter(char)}
className={`relative w-full aspect-[3/4] rounded-3xl overflow-hidden transition-all duration-300 border border-white/20 group ${
char.isLocked ? 'grayscale opacity-70' : 'active:scale-[0.98] shadow-lg hover:shadow-[0_0_25px_rgba(192,132,252,0.3)] hover:border-white/40'
}`}
>
{/* Background Image */}
<img src={char.avatar} alt={char.name} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" />
{/* Gradient Overlay - Lighter/Purple based */}
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-transparent to-transparent opacity-80"></div>
{/* Top Left: Popularity Badge */}
{!char.isLocked && (
<div className="absolute top-3 left-3 flex items-center gap-1 bg-black/40 backdrop-blur-md border border-white/10 pl-2 pr-2.5 py-1 rounded-full shadow-lg z-10">
<Flame size={12} className="text-[#F472B6] fill-[#F472B6]" />
<span className="text-xs font-mono font-bold text-white tracking-wide">
{char.compatibility}%
</span>
</div>
)}
{/* Lock Icon */}
{char.isLocked && (
<div className="absolute top-3 right-3 w-8 h-8 rounded-full bg-black/40 backdrop-blur-md flex items-center justify-center border border-white/20 z-10">
<Lock size={14} className="text-white/80" />
</div>
)}
{/* Content info */}
<div className="absolute bottom-0 left-0 w-full p-4 flex flex-col items-start">
<div className="w-full">
<h2 className="text-lg font-bold text-white leading-tight mb-1 drop-shadow-md">{char.name}</h2>
<div className="flex flex-wrap gap-1.5 opacity-90">
{char.tags.slice(0, 2).map((tag, i) => (
<span key={i} className="text-[10px] text-white bg-white/10 border border-white/10 px-2 py-0.5 rounded-md backdrop-blur-md">
{tag}
</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filteredCharacters.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-white/50">
<p className="text-sm"></p>
</div>
)}
</div>
);
};
export default Discovery;

View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { ChevronLeft, ChevronDown, MessageSquare, BookOpen, Send, HelpCircle, FileText } from 'lucide-react';
interface HelpFeedbackProps {
onBack: () => void;
}
const FAQS = [
{
q: "如何连接 Link-X 设备?",
a: "请确保设备已开机长按电源键3秒蓝灯闪烁。在首页点击底部导航栏的「操控」或在个人中心点击「我的设备」进行搜索配对。"
},
{
q: "订阅会员后权益未生效?",
a: "数据同步可能存在延迟,请尝试下拉刷新个人中心页面,或在设置中点击「恢复购买」。如仍有问题,请提交反馈工单。"
},
{
q: "如何创建自定义剧本?",
a: "Wei AI Pro 用户可在「剧本馆」点击右上角的「+」号进入编辑器支持设置时间轴、震动波形及AI对话逻辑。"
},
{
q: "应用是否会保存我的隐私数据?",
a: "Wei AI 采用端对端加密技术。默认情况下,所有对话记录与震动偏好仅保存在您的本地设备上,除非您手动开启云备份。"
}
];
const HelpFeedback: React.FC<HelpFeedbackProps> = ({ onBack }) => {
const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(0);
const [feedbackText, setFeedbackText] = useState('');
const [isSending, setIsSending] = useState(false);
const toggleFaq = (index: number) => {
setOpenFaqIndex(openFaqIndex === index ? null : index);
};
const handleSendFeedback = () => {
if (!feedbackText.trim()) return;
setIsSending(true);
setTimeout(() => {
setIsSending(false);
setFeedbackText('');
alert('反馈已收到,指挥中心正在分析...');
}, 1500);
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-5 pb-20">
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-4 mb-8">
<button className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col items-center justify-center gap-2 active:scale-[0.98] transition-transform">
<div className="w-10 h-10 rounded-full bg-[#8B5CF6]/10 flex items-center justify-center text-[#8B5CF6]">
<BookOpen size={20} />
</div>
<span className="text-sm font-bold text-white">使</span>
</button>
<button className="bg-[#1C1F26]/60 p-4 rounded-xl border border-white/5 flex flex-col items-center justify-center gap-2 active:scale-[0.98] transition-transform">
<div className="w-10 h-10 rounded-full bg-[#3B82F6]/10 flex items-center justify-center text-[#3B82F6]">
<MessageSquare size={20} />
</div>
<span className="text-sm font-bold text-white"></span>
</button>
</div>
{/* FAQ Section */}
<div className="mb-8">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<HelpCircle size={12} />
</h3>
<div className="space-y-2">
{FAQS.map((faq, i) => {
const isOpen = openFaqIndex === i;
return (
<div key={i} className="bg-[#1C1F26]/40 border border-white/5 rounded-xl overflow-hidden transition-all duration-300">
<button
onClick={() => toggleFaq(i)}
className="w-full p-4 flex items-center justify-between text-left"
>
<span className={`text-sm font-medium ${isOpen ? 'text-[#8B5CF6]' : 'text-[#E2E8F0]'}`}>
{faq.q}
</span>
<ChevronDown size={16} className={`text-[#64748B] transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
</button>
<div className={`overflow-hidden transition-all duration-300 ${isOpen ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'}`}>
<div className="px-4 pb-4 pt-0 text-xs text-[#94A3B8] leading-relaxed border-t border-white/5 mt-2">
<div className="pt-3">{faq.a}</div>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Feedback Form */}
<div>
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<FileText size={12} />
</h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl p-4">
<textarea
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
placeholder="请描述您遇到的问题或建议..."
className="w-full bg-transparent text-sm text-white placeholder-[#64748B] focus:outline-none min-h-[100px] resize-none"
/>
<div className="flex justify-between items-center mt-3 pt-3 border-t border-white/5">
<span className="text-[10px] text-[#64748B]"> jpg, png </span>
<button
onClick={handleSendFeedback}
disabled={!feedbackText.trim() || isSending}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
feedbackText.trim() && !isSending
? 'bg-[#8B5CF6] text-white shadow-[0_0_15px_rgba(139,92,246,0.3)]'
: 'bg-[#334155] text-[#94A3B8] cursor-not-allowed'
}`}
>
{isSending ? (
<span>...</span>
) : (
<>
<Send size={12} />
</>
)}
</button>
</div>
</div>
</div>
<div className="mt-8 text-center">
<p className="text-[10px] text-[#475569]">Service Code: 884-HELP-V2</p>
</div>
</div>
</div>
);
};
export default HelpFeedback;

View File

@@ -0,0 +1,232 @@
import React, { useState, useRef, useEffect } from 'react';
import { Send, Mic, MicOff, ChevronLeft, MoreVertical, Headphones, PhoneOff, Volume2, VolumeX } from 'lucide-react';
import { Message, Character } from '../types';
interface InteractionProps {
character: Character | null;
onBack: () => void;
}
const Interaction: React.FC<InteractionProps> = ({ character, onBack }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isVoiceMode, setIsVoiceMode] = useState(false);
const [isMicMuted, setIsMicMuted] = useState(false);
const [isSpeakerOn, setIsSpeakerOn] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (character) {
setMessages([
{
id: 'init',
text: character.description ? `${character.description}\n\n指挥官${character.name} 已就位。` : '连接建立。',
sender: 'ai',
type: 'text',
timestamp: Date.now(),
}
]);
}
}, [character]);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const handleSend = () => {
if (!inputValue.trim()) return;
const newMsg: Message = {
id: Date.now().toString(),
text: inputValue,
sender: 'user',
type: 'text',
timestamp: Date.now(),
};
setMessages(prev => [...prev, newMsg]);
setInputValue('');
setTimeout(() => {
const aiMsg: Message = {
id: (Date.now() + 1).toString(),
text: '收到反馈。硬件同步率正在上升...',
sender: 'ai',
type: 'text',
timestamp: Date.now(),
};
setMessages(prev => [...prev, aiMsg]);
}, 1500);
};
if (isVoiceMode && character) {
return (
<div className="fixed inset-0 z-[70] bg-[#2e1065] flex flex-col items-center justify-between py-safe overflow-hidden">
<div className="absolute inset-0 z-0">
<img src={character.avatar} className="w-full h-full object-cover opacity-30 blur-2xl scale-125" />
<div className="absolute inset-0 bg-gradient-to-b from-[#2e1065] via-[#4c1d95]/80 to-[#2e1065]"></div>
</div>
<div className="relative z-10 w-full px-6 pt-6 flex justify-between items-start">
<button onClick={() => setIsVoiceMode(false)} className="p-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-white/70 active:bg-white/20 transition-colors">
<ChevronLeft size={24} />
</button>
</div>
<div className="relative z-10 flex-1 flex flex-col items-center justify-center w-full">
<div className="text-center mb-12">
<h2 className="text-2xl font-bold text-white tracking-wide drop-shadow-lg mb-2">{character.name}</h2>
<p className="text-white/60 text-xs font-medium tracking-[0.2em] uppercase">
{isMicMuted ? 'Mic Muted' : 'Listening...'}
</p>
</div>
<div className="relative w-48 h-48 flex items-center justify-center">
{!isMicMuted && (
<>
<div className="absolute inset-0 rounded-full border border-white/20 animate-[ping_3s_ease-in-out_infinite] opacity-50"></div>
<div className="absolute inset-0 rounded-full border border-white/10 animate-[ping_3s_ease-in-out_infinite_1s] opacity-30"></div>
</>
)}
<div className="w-40 h-40 rounded-full overflow-hidden border-2 border-white/30 relative shadow-[0_0_50px_rgba(192,132,252,0.5)] z-10">
<img src={character.avatar} className="w-full h-full object-cover" />
</div>
</div>
<div className="mt-12 flex items-center gap-1 h-8">
{[...Array(5)].map((_, i) => (
<div key={i} className={`w-1 bg-white rounded-full transition-all duration-300 ${isMicMuted ? 'h-1 opacity-20' : 'animate-pulse'}`} style={{ height: isMicMuted ? 4 : Math.random() * 24 + 8, animationDelay: `${i * 0.1}s` }}></div>
))}
</div>
</div>
<div className="relative z-10 w-full px-12 pb-12 flex items-center justify-between">
<button
onClick={() => setIsMicMuted(!isMicMuted)}
className={`p-4 rounded-full border transition-all duration-300 ${
isMicMuted
? 'bg-white text-[#2e1065] border-white shadow-lg'
: 'bg-white/10 backdrop-blur-md border-white/10 text-white hover:bg-white/20'
}`}
>
{isMicMuted ? <MicOff size={24} /> : <Mic size={24} />}
</button>
<button
onClick={() => setIsVoiceMode(false)}
className="w-20 h-20 rounded-full bg-red-500 text-white flex items-center justify-center shadow-lg active:scale-95 transition-transform"
>
<PhoneOff size={32} fill="white" />
</button>
<button
onClick={() => setIsSpeakerOn(!isSpeakerOn)}
className={`p-4 rounded-full border transition-all duration-300 ${
isSpeakerOn
? 'bg-white/10 backdrop-blur-md border-white/30 text-white'
: 'bg-white/5 backdrop-blur-md border-white/10 text-white/40'
}`}
>
{isSpeakerOn ? <Volume2 size={24} /> : <VolumeX size={24} />}
</button>
</div>
</div>
);
}
if (!character) return null;
return (
<div className="fixed inset-0 z-[60] bg-[#2e1065] flex flex-col overflow-hidden">
{/* Background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<img
src={character.avatar}
alt="AI Character"
className="w-full h-full object-cover opacity-20 blur-xl scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065]/90 to-[#2e1065]/50"></div>
</div>
{/* Header */}
<div className="relative z-20 pt-safe px-4 py-3 flex items-center justify-between border-b border-white/10 bg-[#2e1065]/60 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/80 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} strokeWidth={1.5} />
</button>
<div className="flex flex-col items-center">
<h3 className="text-base font-bold text-white tracking-wide drop-shadow-md">{character.name}</h3>
<span className="text-[10px] text-white/60 font-medium tracking-wider uppercase">{character.tagline}</span>
</div>
<button className="p-2 -mr-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<MoreVertical size={20} strokeWidth={1.5} />
</button>
</div>
{/* Chat Area */}
<div className="flex-1 z-10 flex flex-col justify-end pb-2 px-3 overflow-hidden relative">
<div className="absolute top-0 left-0 w-full h-8 bg-gradient-to-b from-[#2e1065]/50 to-transparent z-10 pointer-events-none"></div>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto no-scrollbar space-y-6 pt-4 pb-2"
>
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'} group items-end`}>
{msg.sender === 'ai' && (
<div className="w-8 h-8 rounded-full overflow-hidden mr-3 border border-white/20 shrink-0">
<img src={character.avatar} className="w-full h-full object-cover" />
</div>
)}
<div className={`max-w-[80%] p-3 relative ${
msg.sender === 'user'
? 'bg-gradient-to-br from-[#C084FC] to-[#F472B6] text-white rounded-2xl rounded-tr-sm shadow-md'
: 'bg-white/10 backdrop-blur-md border border-white/10 text-white rounded-2xl rounded-tl-sm shadow-sm'
}`}>
{msg.type === 'text' && <p className="text-sm leading-relaxed tracking-wide font-normal">{msg.text}</p>}
</div>
</div>
))}
</div>
{/* Input Area */}
<div className="relative flex items-center gap-2 pt-1 pb-1">
<button
onClick={() => setIsVoiceMode(true)}
className="w-10 h-10 flex items-center justify-center rounded-full transition-all duration-300 border border-white/10 bg-white/10 backdrop-blur-md text-white/70 hover:text-white hover:bg-white/20"
>
<Headphones size={20} strokeWidth={1.5} />
</button>
<div className="flex-1 h-11 bg-white/10 backdrop-blur-md rounded-full flex items-center px-4 border border-white/10 focus-within:border-white/30 focus-within:bg-white/20 transition-all shadow-sm">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入指令..."
className="bg-transparent w-full text-sm text-white placeholder-white/40 focus:outline-none"
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button className="text-white/50 hover:text-white ml-1 transition-colors">
<Mic size={18} strokeWidth={1.5} />
</button>
</div>
<button
onClick={handleSend}
disabled={!inputValue.trim()}
className={`w-10 h-10 flex items-center justify-center rounded-full transition-all duration-300 ${
inputValue.trim()
? 'bg-white text-[#2e1065] scale-100 opacity-100 shadow-lg'
: 'bg-white/5 text-white/20 scale-95 opacity-50'
}`}
>
<Send size={18} className="-ml-0.5 mt-0.5" strokeWidth={2.5} />
</button>
</div>
</div>
</div>
);
};
export default Interaction;

View File

@@ -0,0 +1,120 @@
import React, { useState } from 'react';
import { Play, Lock, Activity, Zap } from 'lucide-react';
import { MOCK_SCENARIOS } from '../constants';
import { Scenario } from '../types';
interface LibraryProps {
onPlay: (scenario: Scenario) => void;
}
const Library: React.FC<LibraryProps> = ({ onPlay }) => {
const categories = ['全部', '职场', '邻家', '科幻', 'ASMR'];
const [activeCategory, setActiveCategory] = useState('全部');
const filtered = activeCategory === '全部'
? MOCK_SCENARIOS
: MOCK_SCENARIOS.filter(s => s.category === activeCategory || s.tags.includes(activeCategory));
const getIntensityColor = (intensity: string) => {
switch(intensity) {
case 'Low': return 'text-[#34D399]';
case 'Medium': return 'text-[#60A5FA]';
case 'High': return 'text-[#FBBF24]';
case 'Extreme': return 'text-[#F472B6]';
default: return 'text-white/50';
}
};
return (
<div className="pb-24 px-4 min-h-full">
{/* Filter Bar */}
<div className="sticky top-0 z-20 pt-2 pb-2 -mx-4 mb-4">
<div className="relative">
<div className="flex items-center px-6 gap-3 overflow-x-auto no-scrollbar pr-12">
{categories.map(cat => {
const isActive = activeCategory === cat;
return (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`relative px-4 py-1.5 rounded-full border transition-all duration-300 shrink-0 ${
isActive
? 'bg-gradient-to-r from-[#C084FC] to-[#F472B6] text-white font-bold border-transparent shadow-[0_0_15px_rgba(192,132,252,0.4)]'
: 'bg-white/5 text-white/70 border-white/10 hover:bg-white/10 backdrop-blur-md'
}`}
>
<span className="text-sm">{cat}</span>
</button>
);
})}
</div>
<div className="absolute top-0 right-0 h-full w-12 bg-gradient-to-l from-[#4c1d95]/0 to-transparent pointer-events-none"></div>
</div>
</div>
{/* List Layout */}
<div className="flex flex-col gap-3">
{filtered.map(scenario => (
<div
key={scenario.id}
onClick={() => onPlay(scenario)}
className="group relative flex items-center p-3 rounded-2xl bg-[#4c1d95]/20 border border-white/10 overflow-hidden active:scale-[0.98] transition-all duration-300 backdrop-blur-md hover:border-[#C084FC]/40 hover:bg-[#4c1d95]/40 hover:shadow-[0_4px_20px_rgba(0,0,0,0.2)]"
>
{/* Left: Thumbnail */}
<div className="relative w-20 h-20 rounded-xl overflow-hidden shrink-0 shadow-lg border border-white/10">
<img src={scenario.cover} alt={scenario.title} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
{scenario.isLocked && (
<div className="absolute inset-0 bg-[#2e1065]/60 flex items-center justify-center backdrop-blur-[2px]">
<Lock size={16} className="text-white/90" />
</div>
)}
</div>
{/* Center: Info */}
<div className="flex-1 ml-4 flex flex-col justify-center min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] text-white/90 font-mono uppercase tracking-wider bg-white/10 px-1.5 rounded border border-white/10 backdrop-blur-md">
{scenario.category}
</span>
<div className="flex items-center gap-1">
<Zap size={10} className={getIntensityColor(scenario.intensity)} fill="currentColor" />
<span className={`text-[9px] font-bold ${getIntensityColor(scenario.intensity)}`}>{scenario.intensity}</span>
</div>
</div>
<h3 className="text-sm font-bold text-white mb-1.5 truncate pr-2 group-hover:text-[#F472B6] transition-colors drop-shadow-sm">{scenario.title}</h3>
{/* Tags */}
<div className="flex flex-wrap gap-1.5">
{scenario.tags.slice(0, 3).map((tag, i) => (
<span key={i} className="text-[10px] text-[#E2E8F0] bg-white/5 px-1.5 py-0.5 rounded-sm border border-white/5">#{tag}</span>
))}
</div>
</div>
{/* Right: Action Button */}
<div className="shrink-0 mr-1">
<div className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${
scenario.isLocked
? 'bg-white/5 text-white/30'
: 'bg-[#C084FC]/20 text-[#C084FC] group-hover:bg-gradient-to-r group-hover:from-[#C084FC] group-hover:to-[#F472B6] group-hover:text-white group-hover:shadow-[0_0_15px_rgba(192,132,252,0.5)]'
}`}>
{scenario.isLocked ? <Lock size={16} /> : <Play size={18} fill="currentColor" className="ml-0.5" />}
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 opacity-50">
<Activity size={32} className="text-white mb-2" />
<p className="text-xs text-white"></p>
</div>
)}
</div>
);
};
export default Library;

View File

@@ -0,0 +1,331 @@
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;

View File

@@ -0,0 +1,285 @@
import React, { useEffect, useState, useMemo, useRef } from 'react';
import { X, Pause, Play, AlertTriangle } from 'lucide-react';
import { AreaChart, Area, ResponsiveContainer, YAxis, XAxis, ReferenceDot } from 'recharts';
import { Scenario } from '../types';
interface PlayerProps {
scenario: Scenario | null;
onClose: () => void;
}
// Mock script synced with progress (0-100)
const DIALOGUE_SCRIPT = [
{ time: 0, text: "正在建立神经连接..." },
{ time: 5, text: "(检测到心率轻微上升)" },
{ time: 12, text: "“放松,把控制权交给我。”" },
{ time: 20, text: "“很好,保持呼吸频率...”" },
{ time: 28, text: "正在启动触觉反馈模块" },
{ time: 35, text: "“感觉到那个节奏了吗?”" },
{ time: 45, text: "强度正在逐渐增加..." },
{ time: 55, text: "“不要抵抗,顺从它。”" },
{ time: 65, text: "“我会稍微加快一点速度。”" },
{ time: 75, text: "(设备输出功率提升至 80%)" },
{ time: 85, text: "“就是现在...”" },
{ time: 92, text: "“做得很好,指挥官。”" },
{ time: 100, text: "连接结束。" },
];
const Player: React.FC<PlayerProps> = ({ scenario, onClose }) => {
const [isPlaying, setIsPlaying] = useState(true);
const [progress, setProgress] = useState(0);
const [isDragging, setIsDragging] = useState(false); // Track dragging state
const [showAlert, setShowAlert] = useState(false);
// Generate 101 data points (0 to 100) to map perfectly to progress
const data = useMemo(() => {
return Array.from({ length: 101 }, (_, i) => {
// Create a realistic wave: Warmup -> Build up -> Climax -> Cooldown
let hz = 10;
if (i < 20) hz = 10 + (i * 1.5) + (Math.random() * 5); // Warmup
else if (i < 50) hz = 40 + Math.sin(i * 0.5) * 10 + (Math.random() * 10); // Plateau/Tease
else if (i < 85) hz = 70 + (i - 50) * 0.8 + (Math.random() * 15); // Build up to climax
else hz = 90 - ((i - 85) * 5) + (Math.random() * 5); // Cooldown
return {
time: i,
hz: Math.max(0, Math.min(100, hz))
};
});
}, []);
useEffect(() => {
let interval: any;
if (isPlaying && !isDragging) {
interval = setInterval(() => {
setProgress(p => {
if (p >= 100) {
setIsPlaying(false);
return 100;
}
return p + 0.1;
});
}, 50);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isPlaying, isDragging]);
const handleEmergencyStop = () => {
setIsPlaying(false);
setShowAlert(true);
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([100, 50, 100]);
}
setTimeout(() => {
onClose();
}, 1500);
};
// Handle Seek / Drag
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVal = parseFloat(e.target.value);
setProgress(newVal);
};
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = () => {
setIsDragging(false);
};
// Calculate active line based on progress
const activeLineIndex = DIALOGUE_SCRIPT.findIndex((line, index) => {
const nextLine = DIALOGUE_SCRIPT[index + 1];
return progress >= line.time && (!nextLine || progress < nextLine.time);
});
// Get current HZ for the dot
const currentHz = data[Math.min(100, Math.floor(progress))]?.hz || 0;
if (!scenario) return null;
return (
<div className="fixed inset-0 z-[100] bg-[#2e1065] flex flex-col" onDoubleClick={handleEmergencyStop}>
{/* Background Visual */}
<div className="absolute inset-0 opacity-40">
<img src={scenario.cover} className="w-full h-full object-cover blur-lg scale-110" alt="bg" />
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065]/80 to-[#4c1d95]/60"></div>
</div>
{/* Emergency Overlay */}
{showAlert && (
<div className="absolute inset-0 z-[110] bg-[#2e1065]/95 flex flex-col items-center justify-center animate-pulse">
<AlertTriangle size={64} className="text-[#F43F5E] mb-4" />
<h2 className="text-2xl font-bold text-[#F43F5E] tracking-widest">EMERGENCY STOP</h2>
<p className="text-[#E2E8F0] mt-2"></p>
</div>
)}
{/* Header - Seamless Gradient */}
<div className="relative z-10 flex justify-between items-center p-6 bg-gradient-to-b from-[#2e1065] via-[#2e1065]/60 to-transparent">
<div className="flex flex-col">
<h2 className="text-lg font-bold text-white tracking-wide drop-shadow-md">{scenario.title}</h2>
<span className="text-[10px] text-[#C084FC] uppercase tracking-wider font-medium">{scenario.category} SCENARIO</span>
</div>
<button onClick={onClose} className="p-2 rounded-full bg-white/10 backdrop-blur-md active:bg-white/20 transition-colors border border-white/10 hover:border-white/30">
<X size={20} className="text-white/90" />
</button>
</div>
{/* Central Content (Rolling Lyrics) */}
<div className="flex-1 relative z-10 w-full overflow-hidden">
{/* Top Gradient Mask */}
<div className="absolute top-0 left-0 w-full h-24 bg-gradient-to-b from-[#2e1065]/0 to-transparent z-20 pointer-events-none"></div>
<div
className="flex flex-col items-center w-full transition-transform duration-700 ease-[cubic-bezier(0.25,1,0.5,1)]"
style={{
marginTop: 'calc(50vh - 64px)',
transform: `translateY(-${activeLineIndex * 64}px)`
}}
>
{DIALOGUE_SCRIPT.map((line, i) => {
const isActive = i === activeLineIndex;
const distance = Math.abs(activeLineIndex - i);
const isFar = distance > 2;
return (
<div
key={i}
className={`h-16 w-full px-8 flex items-center justify-center text-center transition-all duration-500 ${
isActive
? 'scale-110 opacity-100 blur-0'
: isFar
? 'opacity-10 scale-90 blur-[2px]'
: 'opacity-40 scale-95 blur-[0.5px]'
}`}
>
<p className={`font-medium leading-tight ${isActive ? 'text-white text-lg drop-shadow-[0_0_15px_rgba(192,132,252,0.6)]' : 'text-[#CBD5E1] text-base'}`}>
{line.text}
</p>
</div>
);
})}
</div>
</div>
{/* Bottom Controls & Viz - Unified Gradient */}
<div className="relative z-20 pb-12 px-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065] to-transparent pt-32 -mt-32 pointer-events-none">
{/* Chart Visualizer */}
<div className="h-28 w-full mb-2 px-0 relative opacity-90">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 10, right: 0, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorHz" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#C084FC" stopOpacity={0.6}/>
<stop offset="95%" stopColor="#C084FC" stopOpacity={0}/>
</linearGradient>
<linearGradient id="strokeGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#C084FC" stopOpacity={0.6} />
<stop offset="50%" stopColor="#F472B6" stopOpacity={1} />
<stop offset="100%" stopColor="#C084FC" stopOpacity={0.6} />
</linearGradient>
</defs>
<XAxis dataKey="time" type="number" domain={[0, 100]} hide />
<YAxis hide domain={[0, 100]} />
<Area
type="monotone"
dataKey="hz"
stroke="url(#strokeGradient)"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorHz)"
isAnimationActive={false}
/>
<ReferenceDot
x={progress}
y={currentHz}
r={6}
fill="#F8FAFC"
stroke="#F472B6"
strokeWidth={3}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Controls (Pointer events enabled) */}
<div className="px-8 flex items-center gap-6 pointer-events-auto">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="shrink-0 w-12 h-12 rounded-full bg-white text-[#2e1065] flex items-center justify-center shadow-[0_0_25px_rgba(255,255,255,0.3)] active:scale-95 transition-all hover:scale-105"
>
{isPlaying ? <Pause size={20} fill="currentColor" /> : <Play size={20} fill="currentColor" className="ml-0.5" />}
</button>
<div className="flex-1">
<div className="flex justify-between items-end mb-2">
<div className="flex flex-col">
<span className="text-[10px] text-[#CBD5E1] font-bold uppercase tracking-wider mb-0.5">Device Intensity</span>
<span className="text-sm text-[#F472B6] font-mono font-bold drop-shadow-sm">
: {Math.max(1, Math.ceil(progress / 10))}
</span>
</div>
<span className="text-[10px] text-[#CBD5E1] font-mono">{(progress * 1.5).toFixed(0)}s / {scenario.duration}</span>
</div>
{/* Draggable Progress Bar Container */}
<div className="relative h-6 flex items-center group">
{/* 1. Visual Track (Background) */}
<div className="absolute w-full h-1 bg-white/20 rounded-full overflow-hidden pointer-events-none backdrop-blur-sm">
{/* Visual Progress Fill */}
<div
className="h-full bg-gradient-to-r from-[#C084FC] to-[#F472B6] shadow-[0_0_15px_#F472B6]"
style={{ width: `${progress}%` }}
></div>
</div>
{/* 2. Visual Thumb (Knob) - Moves with progress */}
<div
className="absolute h-4 w-4 bg-white rounded-full shadow-[0_0_15px_rgba(255,255,255,0.8)] pointer-events-none transition-transform duration-75 border-2 border-[#F472B6]"
style={{
left: `${progress}%`,
transform: `translateX(-50%) scale(${isDragging ? 1.2 : 1})`
}}
></div>
{/* 3. Invisible Input Range (The actual interaction layer) */}
<input
type="range"
min="0"
max="100"
step="0.1"
value={progress}
onChange={handleSeek}
onMouseDown={handleDragStart}
onMouseUp={handleDragEnd}
onTouchStart={handleDragStart}
onTouchEnd={handleDragEnd}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
</div>
</div>
</div>
<div className="text-center mt-8 pointer-events-auto">
<p className="text-[10px] text-[#CBD5E1]/60 font-medium tracking-widest uppercase"></p>
</div>
</div>
</div>
);
};
export default Player;

View File

@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import {
ChevronLeft,
ShieldCheck,
Lock,
EyeOff,
Fingerprint,
CloudOff,
Trash2,
AlertTriangle,
History
} from 'lucide-react';
interface PrivacySafetyProps {
onBack: () => void;
}
const PrivacySafety: React.FC<PrivacySafetyProps> = ({ onBack }) => {
const [settings, setSettings] = useState({
biometric: true,
incognito: false,
cloudSync: false,
analytics: false,
blurApp: true
});
const toggleSetting = (key: keyof typeof settings) => {
setSettings(prev => ({ ...prev, [key]: !prev[key] }));
};
const handleWipeData = () => {
if(window.confirm("警告:此操作不可逆!\n\n将清除本地所有聊天记录、自定义剧本及偏好设置。\n确定执行吗")) {
alert("正在执行安全擦除...");
setTimeout(() => {
alert("数据已销毁。");
onBack();
}, 1500);
}
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-5 pb-20">
{/* Security Status Card */}
<div className="bg-[#1C1F26]/40 border border-[#10B981]/20 rounded-2xl p-6 mb-8 flex flex-col items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-[#10B981]/5 to-transparent"></div>
<div className="relative z-10 w-16 h-16 rounded-full bg-[#10B981]/10 flex items-center justify-center mb-3 shadow-[0_0_20px_rgba(16,185,129,0.2)]">
<ShieldCheck size={32} className="text-[#10B981]" />
</div>
<h2 className="text-lg font-bold text-white tracking-wide"></h2>
<p className="text-xs text-[#94A3B8] mt-1"> </p>
</div>
{/* Settings Group 1: Access Control */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1">访</h3>
<div className="space-y-3 mb-8">
{/* Biometric */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[#8B5CF6]/10 text-[#8B5CF6]">
<Fingerprint size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]">使 FaceID/ </div>
</div>
</div>
<button
onClick={() => toggleSetting('biometric')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.biometric ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.biometric ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
{/* App Switcher Blur */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8]">
<EyeOff size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]"></div>
</div>
</div>
<button
onClick={() => toggleSetting('blurApp')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.blurApp ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.blurApp ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
</div>
{/* Settings Group 2: Data Privacy */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="space-y-3 mb-8">
{/* Cloud Sync */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8]">
<CloudOff size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]"></div>
</div>
</div>
<button
onClick={() => toggleSetting('cloudSync')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.cloudSync ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.cloudSync ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
{/* Incognito Mode */}
<div className="bg-[#1C1F26]/60 p-4 rounded-xl flex items-center justify-between border border-white/5">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-white/5 text-[#94A3B8]">
<Lock size={20} />
</div>
<div>
<div className="text-sm font-bold text-white"></div>
<div className="text-[10px] text-[#94A3B8]"></div>
</div>
</div>
<button
onClick={() => toggleSetting('incognito')}
className={`w-11 h-6 rounded-full transition-colors relative ${settings.incognito ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.incognito ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
</div>
{/* Danger Zone */}
<div className="mt-8 border-t border-white/5 pt-6">
<button
onClick={handleWipeData}
className="w-full group relative overflow-hidden rounded-xl border border-[#F43F5E]/30 bg-[#F43F5E]/5 p-4 flex items-center justify-between active:bg-[#F43F5E]/10 transition-all"
>
<div className="flex items-center gap-3 z-10">
<div className="p-2 rounded-lg bg-[#F43F5E]/10 text-[#F43F5E]">
<AlertTriangle size={20} />
</div>
<div className="text-left">
<div className="text-sm font-bold text-[#F43F5E]"></div>
<div className="text-[10px] text-[#F43F5E]/70"></div>
</div>
</div>
<Trash2 size={18} className="text-[#F43F5E] z-10" />
</button>
<div className="mt-4 flex justify-center">
<p className="text-[10px] text-[#475569] font-mono">ID: 884-291-00X-SECURE</p>
</div>
</div>
</div>
</div>
);
};
export default PrivacySafety;

View File

@@ -0,0 +1,154 @@
import React from 'react';
import {
Settings,
CreditCard,
ChevronRight,
Bluetooth,
Zap,
Crown,
Shield,
HelpCircle,
LogOut
} from 'lucide-react';
interface ProfileProps {
onTopUp: () => void;
onOpenDeviceManager: () => void;
onOpenSubscription: () => void;
onOpenPrivacy: () => void;
onOpenHelp: () => void;
onOpenSettings: () => void;
}
const Profile: React.FC<ProfileProps> = ({
onTopUp,
onOpenDeviceManager,
onOpenSubscription,
onOpenPrivacy,
onOpenHelp,
onOpenSettings
}) => {
const menuItems = [
{ id: 'device', icon: Bluetooth, label: '我的设备', sub: 'Link-X Pro' },
{ id: 'sub', icon: CreditCard, label: '订阅管理', sub: '' },
{ id: 'privacy', icon: Shield, label: '隐私安全', sub: '' },
{ id: 'help', icon: HelpCircle, label: '帮助与反馈', sub: '' },
];
const handleMenuClick = (id: string) => {
if (id === 'device') {
onOpenDeviceManager();
} else if (id === 'sub') {
onOpenSubscription();
} else if (id === 'privacy') {
onOpenPrivacy();
} else if (id === 'help') {
onOpenHelp();
}
};
return (
<div className="pt-4 px-5 pb-24 h-full overflow-y-auto no-scrollbar">
{/* 1. User Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="relative">
<div className="w-18 h-18 rounded-full p-[2px] bg-gradient-to-tr from-[#C084FC] to-[#F472B6]">
<div className="w-16 h-16 rounded-full overflow-hidden border-2 border-white/20">
{/* Updated to reliable source */}
<img src="https://tse1.mm.bing.net/th?q=anime%20cool%20guy%20cyberpunk%20avatar&w=200&h=200&c=7" alt="User" className="w-full h-full object-cover" />
</div>
</div>
</div>
<div>
<h2 className="text-2xl font-bold text-white tracking-wide drop-shadow-md">Commander</h2>
<div className="flex items-center gap-1.5 mt-1 bg-white/10 backdrop-blur-md px-2.5 py-0.5 rounded-full border border-white/10 w-fit">
<Crown size={12} className="text-[#FBBF24] fill-[#FBBF24]" />
<span className="text-xs text-white font-bold tracking-wider">LV.4 </span>
</div>
</div>
</div>
<button
onClick={onOpenSettings}
className="w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white/70 hover:bg-white/20 hover:text-white transition-all border border-white/10"
>
<Settings size={20} />
</button>
</div>
{/* 2. Stats Grid - Lighter Glass */}
<div className="grid grid-cols-3 gap-3 mb-6">
{[
{ label: '互动时长', val: '42h', unit: '' },
{ label: '亲密指数', val: '85', unit: '%' },
{ label: '解锁剧本', val: '12', unit: '个' },
].map((stat, i) => (
<div key={i} className="glass-panel rounded-2xl p-4 flex flex-col items-center justify-center bg-white/5 border border-white/10">
<span className="text-xl font-bold text-white font-mono drop-shadow-sm">{stat.val}<span className="text-[10px] text-white/60 ml-0.5">{stat.unit}</span></span>
<span className="text-[10px] text-white/50 mt-0.5 uppercase tracking-wide">{stat.label}</span>
</div>
))}
</div>
{/* 3. VIP / Wallet Card - Ultra Vibrant */}
<div className="w-full relative h-32 rounded-3xl overflow-hidden mb-8 group active:scale-[0.99] transition-transform shadow-[0_10px_40px_-10px_rgba(192,132,252,0.5)]">
<div className="absolute inset-0 bg-gradient-to-r from-[#7C3AED] via-[#9333EA] to-[#DB2777]"></div>
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-30 mix-blend-overlay"></div>
<div className="relative z-10 p-6 flex justify-between items-center h-full">
<div className="flex flex-col justify-center">
<div className="flex items-center gap-2 mb-1 opacity-90">
<Zap size={16} fill="white" className="text-white" />
<span className="text-xs font-bold tracking-wider text-white uppercase">Joy Points</span>
</div>
<h3 className="text-4xl font-bold text-white font-mono tracking-tight drop-shadow-lg">2,450</h3>
</div>
<button
onClick={onTopUp}
className="bg-white text-[#9333EA] px-6 py-2.5 rounded-full text-sm font-bold shadow-lg active:bg-gray-100 transition-colors hover:scale-105 transform"
>
</button>
</div>
</div>
{/* 4. Menu List - Lighter Glass */}
<div className="space-y-3">
<h3 className="px-2 text-xs font-bold text-white/40 uppercase tracking-widest mb-2">General</h3>
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => handleMenuClick(item.id)}
className="w-full glass-panel p-4 rounded-2xl flex items-center justify-between active:scale-[0.98] transition-all bg-white/5 hover:bg-white/10 border border-white/10 group"
>
<div className="flex items-center gap-4">
<div className="p-2.5 rounded-xl bg-white/5 text-white/70 group-hover:text-white group-hover:bg-white/20 transition-colors">
<item.icon size={20} />
</div>
<span className="text-sm text-white font-medium">{item.label}</span>
</div>
<div className="flex items-center gap-2">
{item.sub && <span className="text-xs text-white/50 font-medium">{item.sub}</span>}
<ChevronRight size={18} className="text-white/30 group-hover:text-white transition-colors" />
</div>
</button>
))}
</div>
{/* 5. System / Logout */}
<div className="mt-8 mb-4 px-2">
<button className="w-full p-4 rounded-2xl border border-red-400/30 text-red-400 flex items-center justify-center gap-2 text-sm font-medium hover:bg-red-400/10 active:scale-[0.98] transition-all bg-white/5">
<LogOut size={18} />
退
</button>
<div className="mt-6 text-center">
<p className="text-[10px] text-white/30 font-mono">Wei AI v2.5.0 (Neon Edition)</p>
</div>
</div>
</div>
);
};
export default Profile;

View File

@@ -0,0 +1,118 @@
import React, { useState } from 'react';
import {
ChevronLeft,
Bell,
Globe,
FileText,
Info,
ChevronRight,
Bot
} from 'lucide-react';
interface SettingsProps {
onBack: () => void;
}
const Settings: React.FC<SettingsProps> = ({ onBack }) => {
const [notifications, setNotifications] = useState({
push: true,
aiMsg: true
});
const toggleNotify = (key: keyof typeof notifications) => {
setNotifications(prev => ({ ...prev, [key]: !prev[key] }));
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar p-5 pb-20">
{/* Section 1: Notifications */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl overflow-hidden mb-6">
<div className="p-4 flex items-center justify-between border-b border-white/5">
<div className="flex items-center gap-3">
<Bell size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"></span>
</div>
<button
onClick={() => toggleNotify('push')}
className={`w-10 h-5 rounded-full transition-colors relative ${notifications.push ? 'bg-[#8B5CF6]' : 'bg-[#334155]'}`}
>
<div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-all ${notifications.push ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Bot size={18} className="text-[#E2E8F0]" />
<div>
<span className="text-sm text-white">AI </span>
<p className="text-[10px] text-[#64748B]"></p>
</div>
</div>
<button
onClick={() => toggleNotify('aiMsg')}
disabled={!notifications.push}
className={`w-10 h-5 rounded-full transition-colors relative ${notifications.push ? (notifications.aiMsg ? 'bg-[#8B5CF6]' : 'bg-[#334155]') : 'bg-white/5 opacity-50'}`}
>
<div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-all ${notifications.aiMsg ? 'left-6' : 'left-1'}`}></div>
</button>
</div>
</div>
{/* Section 2: App Preferences */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl overflow-hidden mb-6">
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Globe size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"> / Language</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[#94A3B8]"></span>
<ChevronRight size={14} className="text-[#64748B]" />
</div>
</div>
</div>
{/* Section 4: About */}
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3 ml-1"></h3>
<div className="bg-[#1C1F26]/60 border border-white/5 rounded-xl overflow-hidden mb-8">
<button className="w-full p-4 flex items-center justify-between border-b border-white/5 active:bg-white/5 transition-colors">
<div className="flex items-center gap-3">
<FileText size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"></span>
</div>
<ChevronRight size={14} className="text-[#64748B]" />
</button>
<button className="w-full p-4 flex items-center justify-between border-b border-white/5 active:bg-white/5 transition-colors">
<div className="flex items-center gap-3">
<Info size={18} className="text-[#E2E8F0]" />
<span className="text-sm text-white"></span>
</div>
<ChevronRight size={14} className="text-[#64748B]" />
</button>
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-4 h-4 rounded-full bg-[#8B5CF6] flex items-center justify-center text-[8px] font-bold text-black">V</div>
<span className="text-sm text-white"></span>
</div>
<span className="text-xs text-[#94A3B8]">v2.4.0 (8849)</span>
</div>
</div>
</div>
</div>
);
};
export default Settings;

View File

@@ -0,0 +1,175 @@
import React, { useState } from 'react';
import { ChevronLeft, Check, Crown, Zap, Star, Shield, Smartphone, Infinity } from 'lucide-react';
interface SubscriptionProps {
onBack: () => void;
}
const PLANS = [
{
id: 'month',
name: '月度协议',
price: '¥28',
period: '/月',
desc: '灵便之选,随时取消',
isPopular: false
},
{
id: 'year',
name: '年度神经连接',
price: '¥298',
period: '/年',
desc: '节省 20%,尊享全年',
originalPrice: '¥336',
isPopular: true
},
];
const PRIVILEGES = [
{ icon: Infinity, title: '剧本库无限畅玩', desc: '解锁全部付费/限定剧本' },
{ icon: Zap, title: '高频信号通道', desc: '解锁 Extreme 级震动强度' },
{ icon: Smartphone, title: '多设备同步', desc: '支持多台 Link-X 设备同时控制' },
{ icon: Star, title: 'AI 专属人格', desc: '解锁隐藏性格与深度记忆模式' },
{ icon: Crown, title: '尊贵身份标识', desc: '专属头像框与社区徽章' },
{ icon: Shield, title: '隐私加密通道', desc: '端对端加密,无痕浏览' },
];
const Subscription: React.FC<SubscriptionProps> = ({ onBack }) => {
const [selectedPlan, setSelectedPlan] = useState('year');
const [isProcessing, setIsProcessing] = useState(false);
const handleSubscribe = () => {
setIsProcessing(true);
setTimeout(() => {
setIsProcessing(false);
alert('订阅成功!欢迎加入神经连接计划。');
onBack();
}, 1500);
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-bottom-10 duration-300 fade-in">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div>
</div>
<div className="flex-1 overflow-y-auto no-scrollbar pb-32">
{/* Hero Section */}
<div className="relative h-48 overflow-hidden mb-6 shrink-0">
<div className="absolute inset-0 bg-gradient-to-br from-[#1C1F26] via-[#0F1014] to-[#0F1014]"></div>
{/* Gold Glow */}
<div className="absolute top-[-50%] left-1/2 -translate-x-1/2 w-[120%] h-full bg-[#F59E0B]/10 blur-[60px] rounded-full"></div>
<div className="relative z-10 h-full flex flex-col items-center justify-center text-center px-6">
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-[#F59E0B] to-[#FCD34D] flex items-center justify-center mb-3 shadow-[0_0_20px_rgba(245,158,11,0.3)]">
<Crown size={24} className="text-black" fill="currentColor" />
</div>
<h2 className="text-xl font-bold text-white tracking-wide"> Wei AI Pro</h2>
<p className="text-xs text-[#94A3B8] mt-2 max-w-[200px]"></p>
</div>
</div>
{/* Plan Selection */}
<div className="px-4 space-y-4 mb-8">
{PLANS.map((plan) => {
const isSelected = selectedPlan === plan.id;
return (
<div
key={plan.id}
onClick={() => setSelectedPlan(plan.id)}
className={`relative p-0.5 rounded-2xl transition-all duration-300 active:scale-[0.99] ${
isSelected
? 'bg-gradient-to-r from-[#F59E0B] to-[#FCD34D] shadow-[0_0_20px_rgba(245,158,11,0.15)]'
: 'bg-white/10'
}`}
>
<div className="bg-[#1C1F26] rounded-[14px] p-4 flex items-center justify-between relative z-10">
<div className="flex items-center gap-4">
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors shrink-0 ${
isSelected ? 'border-[#F59E0B] bg-[#F59E0B]' : 'border-[#64748B]'
}`}>
{isSelected && <Check size={12} className="text-black stroke-[3]" />}
</div>
<div>
<div className="flex items-center gap-2">
<h3 className={`text-base font-bold ${isSelected ? 'text-white' : 'text-[#94A3B8]'}`}>{plan.name}</h3>
{plan.isPopular && (
<span className="text-[9px] font-bold bg-[#F59E0B] text-black px-1.5 py-0.5 rounded tracking-wide shrink-0">
</span>
)}
</div>
<p className="text-xs text-[#64748B] mt-1 line-clamp-1">{plan.desc}</p>
</div>
</div>
<div className="text-right shrink-0">
{plan.originalPrice && (
<span className="block text-[10px] text-[#64748B] line-through mb-0.5">{plan.originalPrice}</span>
)}
<div className="flex items-end justify-end">
<span className={`text-xl font-bold font-mono ${isSelected ? 'text-[#F59E0B]' : 'text-white'}`}>{plan.price}</span>
<span className="text-xs text-[#64748B] mb-1 ml-0.5">{plan.period}</span>
</div>
</div>
</div>
</div>
);
})}
</div>
{/* Privileges Grid */}
<div className="px-5">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<Star size={12} />
</h3>
<div className="grid grid-cols-2 gap-x-4 gap-y-6">
{PRIVILEGES.map((item, idx) => (
<div key={idx} className="flex gap-3">
<div className="shrink-0 w-8 h-8 rounded-lg bg-[#F59E0B]/10 flex items-center justify-center text-[#F59E0B]">
<item.icon size={16} />
</div>
<div>
<h4 className="text-xs font-bold text-white mb-0.5">{item.title}</h4>
<p className="text-[10px] text-[#64748B] leading-tight">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* Terms */}
<div className="px-6 mt-8 mb-4 text-[10px] text-[#475569] text-center leading-relaxed">
<p>24</p>
<p className="mt-2"></p>
</div>
</div>
{/* Bottom Action Bar */}
<div className="absolute bottom-0 left-0 w-full p-4 bg-[#0F1014]/90 backdrop-blur-xl border-t border-white/5 z-30">
<button
onClick={handleSubscribe}
disabled={isProcessing}
className="w-full h-12 rounded-xl bg-gradient-to-r from-[#F59E0B] to-[#FBBF24] text-black font-bold tracking-wide shadow-[0_0_20px_rgba(245,158,11,0.2)] active:scale-[0.98] transition-transform flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
>
{isProcessing ? (
<span className="animate-pulse">...</span>
) : (
<>
<Zap size={18} fill="currentColor" />
{PLANS.find(p => p.id === selectedPlan)?.price}
</>
)}
</button>
</div>
</div>
);
};
export default Subscription;

176
wei-ai-demo/pages/TopUp.tsx Normal file
View File

@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import { ChevronLeft, Zap, CreditCard, Check, ShieldCheck, Gem } from 'lucide-react';
interface TopUpProps {
onBack: () => void;
}
const RECHARGE_OPTIONS = [
{ id: 1, points: 60, price: '¥6.00', bonus: null, tag: null },
{ id: 2, points: 300, price: '¥30.00', bonus: '+15', tag: null },
{ id: 3, points: 680, price: '¥68.00', bonus: '+50', tag: '热销' },
{ id: 4, points: 1280, price: '¥128.00', bonus: '+120', tag: null },
{ id: 5, points: 3280, price: '¥328.00', bonus: '+350', tag: '超值' },
{ id: 6, points: 6480, price: '¥648.00', bonus: '+800', tag: null },
];
const PAYMENT_METHODS = [
{ id: 'alipay', name: '支付宝', icon: '支' },
{ id: 'wechat', name: '微信支付', icon: '微' },
];
const TopUp: React.FC<TopUpProps> = ({ onBack }) => {
const [selectedOption, setSelectedOption] = useState<number>(3);
const [paymentMethod, setPaymentMethod] = useState('alipay');
const [isProcessing, setIsProcessing] = useState(false);
const handlePay = () => {
setIsProcessing(true);
setTimeout(() => {
setIsProcessing(false);
alert('模拟支付成功!积分已到账。');
onBack();
}, 1500);
};
return (
<div className="fixed inset-0 z-[60] bg-[#0F1014] flex flex-col h-full overflow-hidden animate-in slide-in-from-bottom-10 duration-300 fade-in">
{/* Header */}
<div className="relative z-20 pt-safe-top px-4 py-3 flex items-center justify-between border-b border-white/5 bg-[#0F1014]/80 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} />
</button>
<h1 className="text-base font-bold text-white tracking-wider"></h1>
<div className="w-8"></div> {/* Spacer */}
</div>
<div className="flex-1 overflow-y-auto no-scrollbar pb-32">
{/* Current Balance Card */}
<div className="mx-4 mt-6 mb-8 relative h-32 rounded-2xl overflow-hidden shadow-[0_10px_30px_rgba(139,92,246,0.15)] group">
<div className="absolute inset-0 bg-gradient-to-br from-[#1C1F26] via-[#2D3039] to-[#1C1F26]"></div>
{/* Neon Accents */}
<div className="absolute top-0 right-0 w-32 h-32 bg-[#8B5CF6]/20 blur-[50px] rounded-full"></div>
<div className="absolute bottom-0 left-0 w-24 h-24 bg-[#F43F5E]/10 blur-[40px] rounded-full"></div>
<div className="relative z-10 p-6 flex flex-col justify-between h-full">
<div className="flex items-center gap-2 opacity-70">
<Gem size={14} className="text-[#8B5CF6]" />
<span className="text-xs font-bold text-[#E2E8F0] tracking-widest uppercase"></span>
</div>
<div className="flex items-end gap-3">
<span className="text-4xl font-bold text-white font-mono tracking-tighter drop-shadow-lg">2,450</span>
<span className="text-sm font-medium text-[#8B5CF6] mb-1.5"></span>
</div>
</div>
{/* Decorative Pattern */}
<div className="absolute right-4 top-1/2 -translate-y-1/2 opacity-5 pointer-events-none">
<Zap size={100} />
</div>
</div>
{/* Recharge Options Grid */}
<div className="px-4">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-4 flex items-center gap-2">
<Zap size={12} />
</h3>
<div className="grid grid-cols-2 gap-3">
{RECHARGE_OPTIONS.map((opt) => {
const isSelected = selectedOption === opt.id;
return (
<button
key={opt.id}
onClick={() => setSelectedOption(opt.id)}
className={`relative p-4 rounded-xl border flex flex-col items-start transition-all duration-300 active:scale-[0.98] ${
isSelected
? 'bg-[#8B5CF6]/10 border-[#8B5CF6] shadow-[0_0_15px_rgba(139,92,246,0.15)]'
: 'bg-[#1C1F26]/60 border-white/5 hover:border-white/10'
}`}
>
{opt.tag && (
<div className={`absolute -top-2.5 -right-2 px-2 py-0.5 rounded text-[9px] font-bold tracking-wider uppercase border shadow-sm ${
opt.tag === '热销'
? 'bg-[#F43F5E] text-white border-[#F43F5E]'
: 'bg-[#F59E0B] text-black border-[#F59E0B]'
}`}>
{opt.tag}
</div>
)}
<div className="flex items-center gap-1.5 mb-1">
<Zap size={14} className={isSelected ? 'text-[#8B5CF6]' : 'text-[#64748B]'} fill={isSelected ? "currentColor" : "none"} />
<span className={`text-lg font-bold font-mono ${isSelected ? 'text-white' : 'text-[#E2E8F0]'}`}>
{opt.points}
</span>
</div>
{opt.bonus && (
<span className="text-[10px] text-[#10B981] font-mono mb-2 block">
{opt.bonus}
</span>
)}
<span className={`text-sm font-medium mt-auto ${isSelected ? 'text-[#8B5CF6]' : 'text-[#94A3B8]'}`}>
{opt.price}
</span>
</button>
);
})}
</div>
</div>
{/* Payment Method (Visual Only) */}
<div className="px-4 mt-8">
<h3 className="text-xs font-bold text-[#64748B] uppercase tracking-widest mb-3"></h3>
<div className="space-y-2">
{PAYMENT_METHODS.map(method => (
<button
key={method.id}
onClick={() => setPaymentMethod(method.id)}
className={`w-full flex items-center justify-between p-3 rounded-xl border transition-all ${
paymentMethod === method.id
? 'bg-[#1C1F26] border-[#8B5CF6]/50'
: 'bg-transparent border-white/5 opacity-60'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded flex items-center justify-center font-bold text-white ${method.id === 'alipay' ? 'bg-[#1677FF]' : 'bg-[#07C160]'}`}>
{method.icon}
</div>
<span className="text-sm text-white">{method.name}</span>
</div>
{paymentMethod === method.id && <Check size={16} className="text-[#8B5CF6]" />}
</button>
))}
</div>
</div>
{/* Terms */}
<div className="px-6 mt-8 mb-4 flex items-start gap-2 text-[10px] text-[#64748B] leading-tight">
<ShieldCheck size={12} className="shrink-0 mt-0.5" />
<p>退</p>
</div>
</div>
{/* Bottom Action Bar */}
<div className="absolute bottom-0 left-0 w-full p-4 bg-[#0F1014]/90 backdrop-blur-xl border-t border-white/5 z-30">
<button
onClick={handlePay}
disabled={isProcessing}
className="w-full h-12 rounded-xl bg-gradient-to-r from-[#8B5CF6] to-[#6366f1] text-white font-bold tracking-wide shadow-[0_0_20px_rgba(139,92,246,0.3)] active:scale-[0.98] transition-transform flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
>
{isProcessing ? (
<span className="animate-pulse">...</span>
) : (
<>
<CreditCard size={18} />
{RECHARGE_OPTIONS.find(o => o.id === selectedOption)?.price}
</>
)}
</button>
</div>
</div>
);
};
export default TopUp;