commit a4e7898e94c8cd6cf0d01589f36fe6990155d8fc Author: liqupan Date: Wed Jan 28 19:10:19 2026 +0800 feat: app 端 ui 设计完成 diff --git a/wei-ai-demo/.gitignore b/wei-ai-demo/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/wei-ai-demo/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/wei-ai-demo/App.tsx b/wei-ai-demo/App.tsx new file mode 100644 index 0000000..80afe1c --- /dev/null +++ b/wei-ai-demo/App.tsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect } from 'react'; +import { HashRouter } from 'react-router-dom'; +import Layout from './components/Layout'; +import Interaction from './pages/Interaction'; +import Discovery from './pages/Discovery'; +import Library from './pages/Library'; +import ManualControl from './pages/ManualControl'; +import Profile from './pages/Profile'; +import Player from './pages/Player'; +import TopUp from './pages/TopUp'; +import DeviceManager from './pages/DeviceManager'; +import Subscription from './pages/Subscription'; +import PrivacySafety from './pages/PrivacySafety'; +import HelpFeedback from './pages/HelpFeedback'; +import Settings from './pages/Settings'; +import { UserTab, Scenario, Character, DeviceStatus } from './types'; + +const App: React.FC = () => { + const [currentTab, setCurrentTab] = useState(UserTab.Discovery); + const [activeScenario, setActiveScenario] = useState(null); + const [activeCharacter, setActiveCharacter] = useState(null); + const [showTopUp, setShowTopUp] = useState(false); + const [showDeviceManager, setShowDeviceManager] = useState(false); + const [showSubscription, setShowSubscription] = useState(false); + const [showPrivacy, setShowPrivacy] = useState(false); + const [showHelp, setShowHelp] = useState(false); + const [showSettings, setShowSettings] = useState(false); + + // Global Device State (Mock) + const [deviceStatus, setDeviceStatus] = useState({ + connected: true, + battery: 82, + temperature: 36.5, + signalStrength: 90, + currentMode: 'Idle' + }); + + // Mock battery drain + useEffect(() => { + const interval = setInterval(() => { + setDeviceStatus(prev => ({ + ...prev, + battery: Math.max(0, prev.battery - (prev.connected ? 0.05 : 0)), // Slow drain + signalStrength: prev.connected ? 85 + Math.random() * 10 : 0 + })); + }, 5000); + return () => clearInterval(interval); + }, []); + + const handleScenarioPlay = (scenario: Scenario) => { + if (!scenario.isLocked) { + setActiveScenario(scenario); + } else { + alert('Credits required to unlock this premium content.'); + } + }; + + const renderContent = () => { + switch (currentTab) { + case UserTab.Discovery: + return ; + case UserTab.Library: + return ; + case UserTab.Control: + return ; + case UserTab.Profile: + return ( + setShowTopUp(true)} + onOpenDeviceManager={() => setShowDeviceManager(true)} + onOpenSubscription={() => setShowSubscription(true)} + onOpenPrivacy={() => setShowPrivacy(true)} + onOpenHelp={() => setShowHelp(true)} + onOpenSettings={() => setShowSettings(true)} + /> + ); + default: + return ; + } + }; + + // Define Page Titles based on Tab + const getPageMeta = (tab: UserTab) => { + switch (tab) { + case UserTab.Discovery: + return { title: '专属推荐', subtitle: '基于偏好生成的私密匹配列表' }; + case UserTab.Library: + return { title: '剧本馆', subtitle: '沉浸式感官体验库' }; + case UserTab.Control: + return { title: '手动实验室', subtitle: '实时触觉反馈控制' }; + case UserTab.Profile: + return { title: '个人中心', subtitle: 'ID: 884-291-00X' }; + default: + return { title: 'Wei AI', subtitle: '' }; + } + }; + + const meta = getPageMeta(currentTab); + + return ( + + {activeScenario ? ( + setActiveScenario(null)} /> + ) : activeCharacter ? ( + setActiveCharacter(null)} /> + ) : showTopUp ? ( + setShowTopUp(false)} /> + ) : showDeviceManager ? ( + setShowDeviceManager(false)} /> + ) : showSubscription ? ( + setShowSubscription(false)} /> + ) : showPrivacy ? ( + setShowPrivacy(false)} /> + ) : showHelp ? ( + setShowHelp(false)} /> + ) : showSettings ? ( + setShowSettings(false)} /> + ) : ( + setShowDeviceManager(true)} + > + {renderContent()} + + )} + + ); +}; + +export default App; \ No newline at end of file diff --git a/wei-ai-demo/README.md b/wei-ai-demo/README.md new file mode 100644 index 0000000..cbe15f8 --- /dev/null +++ b/wei-ai-demo/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/19FJHfdgVSMTtGhAsRjk32-ezFzN3oYfB + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/wei-ai-demo/components/BottomNav.tsx b/wei-ai-demo/components/BottomNav.tsx new file mode 100644 index 0000000..3a712a2 --- /dev/null +++ b/wei-ai-demo/components/BottomNav.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Compass, PlayCircle, Radio, User } from 'lucide-react'; +import { UserTab } from '../types'; + +interface BottomNavProps { + currentTab: UserTab; + onTabChange: (tab: UserTab) => void; +} + +const BottomNav: React.FC = ({ currentTab, onTabChange }) => { + const navItems = [ + { id: UserTab.Discovery, icon: Compass, label: '发现' }, + { id: UserTab.Library, icon: PlayCircle, label: '剧本' }, + { id: UserTab.Control, icon: Radio, label: '操控' }, + { id: UserTab.Profile, icon: User, label: '我的' }, + ]; + + return ( +
+ {/* Main Bar Content - High Transparency Glass */} +
+
+ {navItems.map((item) => { + const isActive = currentTab === item.id; + return ( + + ); + })} +
+
+
+ ); +}; + +export default BottomNav; \ No newline at end of file diff --git a/wei-ai-demo/components/HardwareStatus.tsx b/wei-ai-demo/components/HardwareStatus.tsx new file mode 100644 index 0000000..41d3607 --- /dev/null +++ b/wei-ai-demo/components/HardwareStatus.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Zap, Bluetooth } from 'lucide-react'; +import { DeviceStatus } from '../types'; + +interface HardwareStatusProps { + status: DeviceStatus; + onClick?: () => void; + className?: string; +} + +const HardwareStatus: React.FC = ({ status, onClick, className = '' }) => { + return ( + + ); +}; + +export default HardwareStatus; \ No newline at end of file diff --git a/wei-ai-demo/components/Layout.tsx b/wei-ai-demo/components/Layout.tsx new file mode 100644 index 0000000..78d53e8 --- /dev/null +++ b/wei-ai-demo/components/Layout.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Hexagon } from 'lucide-react'; +import BottomNav from './BottomNav'; +import HardwareStatus from './HardwareStatus'; +import { UserTab, DeviceStatus } from '../types'; + +interface LayoutProps { + children: React.ReactNode; + currentTab: UserTab; + onTabChange: (tab: UserTab) => void; + title: string; + subtitle?: string; + deviceStatus: DeviceStatus; + onDeviceClick: () => void; +} + +const Layout: React.FC = ({ + children, + currentTab, + onTabChange, + title, + subtitle, + deviceStatus, + onDeviceClick +}) => { + return ( +
+ + {/* 1. Top Template Header (Super Clean) */} +
+
+ {/* Left: Branding & Title */} +
+
+ + Wei AI +
+

{title}

+
+ + {/* Right: Embedded Hardware Status */} +
+ +
+
+
+ + {/* 2. Middle Content (Scrollable) */} +
+ {/* Top fade mask for content scrolling under header */} +
+ {children} +
+ + {/* 3. Bottom Tab (Fixed) */} + +
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/wei-ai-demo/constants.ts b/wei-ai-demo/constants.ts new file mode 100644 index 0000000..9389e40 --- /dev/null +++ b/wei-ai-demo/constants.ts @@ -0,0 +1,124 @@ +import { Scenario, Message, Character } from './types'; + +// Palette - Updated for Lighter/Vibrant theme +export const COLORS = { + bg: '#2e1065', + card: 'rgba(255,255,255,0.1)', + primary: '#C084FC', // Lighter Purple + secondary: '#F472B6', // Lighter Pink + textMain: '#F8FAFC', + textMuted: '#CBD5E1', +}; + +// 使用 Bing Image Proxy 确保图片稳定加载且符合描述 +// 参数: w=宽度, h=高度, c=7(Smart Crop), q=关键词 +const getImg = (keyword: string) => + `https://tse1.mm.bing.net/th?q=${encodeURIComponent(keyword)}&w=600&h=900&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate`; + +const getCover = (keyword: string) => + `https://tse1.mm.bing.net/th?q=${encodeURIComponent(keyword)}&w=400&h=600&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate`; + +export const MOCK_CHARACTERS: Character[] = [ + { + id: 'c1', + name: 'Eva-09', + tagline: '私人仿生护理专员', + // 关键词: 动漫女孩 白色比基尼 银发 温柔 + avatar: getImg('anime girl white bikini silver hair gentle portrait masterpiece'), + description: '专为高压人群设计的仿生人型号,擅长通过精准的触觉反馈缓解神经紧张。', + tags: ['温顺', '医疗', '治愈'], + compatibility: 98, + status: 'online', + }, + { + id: 'c2', + name: 'Commander V', + tagline: '深空舰队指挥官', + // 关键词: 动漫女孩 黑色比基尼 军帽 强势 + avatar: getImg('anime girl black bikini military cap domineering expression dark hair'), + description: '性格强势,喜欢掌控一切。在连接中,你需要完全服从她的指令。', + tags: ['强势', '指令', '调教'], + compatibility: 85, + status: 'online', + }, + { + id: 'c3', + name: 'Yuki (故障版)', + tagline: '觉醒的虚拟偶像', + // 关键词: 动漫女孩 粉色比基尼 赛博朋克 故障风 + avatar: getImg('anime girl pink bikini cyberpunk neon colorful hair yandere'), + description: '核心代码出现异常逻辑,表现出极强的占有欲和不可预测的信号波动。', + tags: ['病娇', '不稳定', '高频'], + compatibility: 92, + status: 'busy', + }, + { + id: 'c4', + name: 'Secret X', + tagline: '未知信号源', + // 关键词: 动漫女孩 紫色比基尼 神秘 暗黑 + avatar: getImg('anime girl purple micro bikini mysterious dark glowing eyes sexy'), + description: '权限不足,请提升会员等级以解码该信号源。', + tags: ['神秘', '极乐'], + compatibility: 0, + status: 'offline', + isLocked: true, + }, +]; + +export const MOCK_SCENARIOS: Scenario[] = [ + { + id: '1', + title: '午夜办公室的加班', + category: '职场', + // 关键词: 动漫 办公室 夜晚 + cover: getCover('anime girl office lady lingerie night city window'), + duration: '12:30', + intensity: 'Medium', + isLocked: false, + tags: ['沉浸', 'ASMR'], + }, + { + id: '2', + title: '私人医生的检查', + category: '角色扮演', + // 关键词: 动漫 护士 诊疗室 + cover: getCover('anime nurse girl white bikini hospital room'), + duration: '18:00', + intensity: 'High', + isLocked: true, + tags: ['强互动', '语音'], + }, + { + id: '3', + title: '海边度假的偶遇', + category: '邻家', + // 关键词: 动漫 海滩 泳装 + cover: getCover('anime girl blue bikini running beach ocean sunny'), + duration: '25:00', + intensity: 'Low', + isLocked: false, + tags: ['纯爱', '剧情'], + }, + { + id: '4', + title: '赛博仿生人测试', + category: '科幻', + // 关键词: 动漫 科幻 实验室 机械女 + cover: getCover('anime cyborg girl metallic bikini sci-fi lab wires'), + duration: '10:00', + intensity: 'Extreme', + isLocked: true, + tags: ['硬核', '指令'], + }, +]; + +export const INITIAL_MESSAGES: Message[] = [ + { + id: '1', + text: '连接已建立,正在校准生物反馈信号...', + sender: 'ai', + type: 'text', + timestamp: Date.now() - 100000, + }, +]; \ No newline at end of file diff --git a/wei-ai-demo/index.html b/wei-ai-demo/index.html new file mode 100644 index 0000000..8605c6d --- /dev/null +++ b/wei-ai-demo/index.html @@ -0,0 +1,67 @@ + + + + + + Wei AI + + + + + + + +
+ + + \ No newline at end of file diff --git a/wei-ai-demo/index.tsx b/wei-ai-demo/index.tsx new file mode 100644 index 0000000..39e1012 --- /dev/null +++ b/wei-ai-demo/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/wei-ai-demo/metadata.json b/wei-ai-demo/metadata.json new file mode 100644 index 0000000..d1478e7 --- /dev/null +++ b/wei-ai-demo/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Wei AI - 赛博深空", + "description": "Premium AI-driven interactive hardware controller featuring a dark minimalist aesthetic, immersive scenarios, and precise haptic control.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/wei-ai-demo/package.json b/wei-ai-demo/package.json new file mode 100644 index 0000000..e35d75a --- /dev/null +++ b/wei-ai-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "wei-ai---赛博深空", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.3", + "react-dom": "^19.2.3", + "recharts": "^3.6.0", + "lucide-react": "^0.562.0", + "react-router-dom": "^7.11.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/wei-ai-demo/pages/DeviceManager.tsx b/wei-ai-demo/pages/DeviceManager.tsx new file mode 100644 index 0000000..dddc98c --- /dev/null +++ b/wei-ai-demo/pages/DeviceManager.tsx @@ -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 = ({ onBack }) => { + const [status, setStatus] = useState('scanning'); + const [scannedDevices, setScannedDevices] = useState([]); + 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 ( +
+ {/* Header */} +
+ +

设备管理

+
+
+ +
+ + {/* Device Visual Card */} +
+
+
+ + {/* Glowing Center */} +
+
+ +
+
+

Link-X Pro

+
+
+ 已连接 +
+
+
+ + {/* Status Grid */} +
+
+
+ + 剩余电量 +
+ 85% +
+
+
+
+
+
+ + 信号强度 +
+ -42dBm +
+
+
+
+
+
+
+
+ + {/* Actions */} +

设备控制

+
+ + + + + +
+ + +
+
+ ); + } + + // --- RENDER: Scanning View --- + return ( +
+ {/* Header */} +
+ +

搜索设备

+
+ {status === 'scanning' && } +
+
+ +
+ + {/* Radar Animation */} +
+ {/* Rings */} +
+
+
+ + {/* Active Scan Line */} + {status === 'scanning' && ( +
+ )} + + {/* Center Icon */} +
+ +
+ + {/* Status Text */} +
+

+ {status === 'scanning' ? 'SCANNING FOR SIGNALS...' : 'SCAN COMPLETE'} +

+
+
+ + {/* Device List */} +
+ {scannedDevices.map(device => ( + + ))} +
+ + {/* Help */} +
+ + 请确保设备已开机并处于配对模式 +
+
+ + {/* Full Screen Loading Overlay for Connection */} + {connectStep === 2 && ( +
+ +

连接成功

+
+ )} +
+ ); +}; + +export default DeviceManager; \ No newline at end of file diff --git a/wei-ai-demo/pages/Discovery.tsx b/wei-ai-demo/pages/Discovery.tsx new file mode 100644 index 0000000..48823ee --- /dev/null +++ b/wei-ai-demo/pages/Discovery.tsx @@ -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 = ({ 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 ( +
+ {/* Filter Bar */} +
+
+ {/* Scroll Container */} +
+ {filters.map(filter => { + const isActive = activeFilter === filter.id; + return ( + + ); + })} +
+ {/* Right Fade */} +
+
+
+ + {/* Grid Layout */} +
+ {filteredCharacters.map((char) => ( +
!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 */} + {char.name} + + {/* Gradient Overlay - Lighter/Purple based */} +
+ + {/* Top Left: Popularity Badge */} + {!char.isLocked && ( +
+ + + {char.compatibility}% + +
+ )} + + {/* Lock Icon */} + {char.isLocked && ( +
+ +
+ )} + + {/* Content info */} +
+
+

{char.name}

+
+ {char.tags.slice(0, 2).map((tag, i) => ( + + {tag} + + ))} +
+
+
+
+ ))} +
+ + {/* Empty State */} + {filteredCharacters.length === 0 && ( +
+

暂无匹配角色

+
+ )} +
+ ); +}; + +export default Discovery; \ No newline at end of file diff --git a/wei-ai-demo/pages/HelpFeedback.tsx b/wei-ai-demo/pages/HelpFeedback.tsx new file mode 100644 index 0000000..231c151 --- /dev/null +++ b/wei-ai-demo/pages/HelpFeedback.tsx @@ -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 = ({ onBack }) => { + const [openFaqIndex, setOpenFaqIndex] = useState(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 ( +
+ + {/* Header */} +
+ +

帮助与反馈

+
+
+ +
+ + {/* Quick Actions */} +
+ + +
+ + {/* FAQ Section */} +
+

+ 常见问题 +

+
+ {FAQS.map((faq, i) => { + const isOpen = openFaqIndex === i; + return ( +
+ +
+
+
{faq.a}
+
+
+
+ ); + })} +
+
+ + {/* Feedback Form */} +
+

+ 提交反馈 +

+
+