feat: app 端 ui 设计完成
24
wei-ai-demo/.gitignore
vendored
Normal file
@@ -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?
|
||||
134
wei-ai-demo/App.tsx
Normal file
@@ -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>(UserTab.Discovery);
|
||||
const [activeScenario, setActiveScenario] = useState<Scenario | null>(null);
|
||||
const [activeCharacter, setActiveCharacter] = useState<Character | null>(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<DeviceStatus>({
|
||||
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 <Discovery onSelectCharacter={setActiveCharacter} />;
|
||||
case UserTab.Library:
|
||||
return <Library onPlay={handleScenarioPlay} />;
|
||||
case UserTab.Control:
|
||||
return <ManualControl />;
|
||||
case UserTab.Profile:
|
||||
return (
|
||||
<Profile
|
||||
onTopUp={() => setShowTopUp(true)}
|
||||
onOpenDeviceManager={() => setShowDeviceManager(true)}
|
||||
onOpenSubscription={() => setShowSubscription(true)}
|
||||
onOpenPrivacy={() => setShowPrivacy(true)}
|
||||
onOpenHelp={() => setShowHelp(true)}
|
||||
onOpenSettings={() => setShowSettings(true)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Discovery onSelectCharacter={setActiveCharacter} />;
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<HashRouter>
|
||||
{activeScenario ? (
|
||||
<Player scenario={activeScenario} onClose={() => setActiveScenario(null)} />
|
||||
) : activeCharacter ? (
|
||||
<Interaction character={activeCharacter} onBack={() => setActiveCharacter(null)} />
|
||||
) : showTopUp ? (
|
||||
<TopUp onBack={() => setShowTopUp(false)} />
|
||||
) : showDeviceManager ? (
|
||||
<DeviceManager onBack={() => setShowDeviceManager(false)} />
|
||||
) : showSubscription ? (
|
||||
<Subscription onBack={() => setShowSubscription(false)} />
|
||||
) : showPrivacy ? (
|
||||
<PrivacySafety onBack={() => setShowPrivacy(false)} />
|
||||
) : showHelp ? (
|
||||
<HelpFeedback onBack={() => setShowHelp(false)} />
|
||||
) : showSettings ? (
|
||||
<Settings onBack={() => setShowSettings(false)} />
|
||||
) : (
|
||||
<Layout
|
||||
currentTab={currentTab}
|
||||
onTabChange={setCurrentTab}
|
||||
title={meta.title}
|
||||
subtitle={meta.subtitle}
|
||||
deviceStatus={deviceStatus}
|
||||
onDeviceClick={() => setShowDeviceManager(true)}
|
||||
>
|
||||
{renderContent()}
|
||||
</Layout>
|
||||
)}
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
20
wei-ai-demo/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# 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`
|
||||
57
wei-ai-demo/components/BottomNav.tsx
Normal file
@@ -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<BottomNavProps> = ({ 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 (
|
||||
<div className="fixed bottom-0 left-0 w-full z-50">
|
||||
{/* Main Bar Content - High Transparency Glass */}
|
||||
<div className="bg-black/20 backdrop-blur-2xl border-t border-white/10 pb-safe">
|
||||
<div className="flex justify-around items-center h-[60px]">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onTabChange(item.id)}
|
||||
className="relative flex-1 flex flex-col items-center justify-center h-full group"
|
||||
>
|
||||
{/* Active Glow Background */}
|
||||
{isActive && (
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-white/10 rounded-full blur-lg"></div>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div className={`transition-all duration-300 relative z-10 ${isActive ? 'text-white scale-110 drop-shadow-[0_0_10px_rgba(255,255,255,0.6)]' : 'text-white/50 group-hover:text-white/80'}`}>
|
||||
<item.icon
|
||||
size={26}
|
||||
strokeWidth={isActive ? 2.5 : 1.5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<span className={`text-[10px] font-medium mt-1 transition-colors duration-300 relative z-10 ${isActive ? 'text-white' : 'text-white/40'}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomNav;
|
||||
61
wei-ai-demo/components/HardwareStatus.tsx
Normal file
@@ -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<HardwareStatusProps> = ({ status, onClick, className = '' }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center justify-end transition-all duration-500 shrink-0 ${className}`}
|
||||
>
|
||||
<div className={`
|
||||
relative flex items-center h-7 pl-3 pr-3 rounded-full
|
||||
bg-[#1A1625]/80 backdrop-blur-md border border-white/10 shadow-sm
|
||||
active:scale-95 transition-all duration-300 group hover:border-[#A855F7]/30
|
||||
`}>
|
||||
|
||||
{/* Connection Status Icon */}
|
||||
<div className="relative flex items-center justify-center w-3 h-3 mr-2">
|
||||
{status.connected ? (
|
||||
<>
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#10B981] opacity-20"></span>
|
||||
<Bluetooth size={12} className="text-[#10B981] relative z-10" />
|
||||
</>
|
||||
) : (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-gray-600"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-[1px] h-2.5 bg-white/10 mr-2"></div>
|
||||
|
||||
{/* Battery Status */}
|
||||
{status.connected ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`text-[10px] font-mono font-bold tracking-wide tabular-nums ${status.battery < 20 ? 'text-[#F43F5E]' : 'text-[#E2E8F0]'}`}>
|
||||
{Math.floor(status.battery)}%
|
||||
</span>
|
||||
<Zap
|
||||
size={9}
|
||||
className={`${status.battery < 20 ? 'text-[#F43F5E]' : 'text-[#A855F7]'} ${status.battery < 20 ? 'animate-pulse' : ''}`}
|
||||
fill={status.battery < 20 ? "currentColor" : "currentColor"}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[9px] text-gray-500 font-medium tracking-wide">未连接</span>
|
||||
)}
|
||||
|
||||
{/* Subtle Inner Glow */}
|
||||
<div className="absolute inset-0 rounded-full border border-white/5 pointer-events-none group-hover:border-[#A855F7]/20 transition-colors"></div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default HardwareStatus;
|
||||
61
wei-ai-demo/components/Layout.tsx
Normal file
@@ -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<LayoutProps> = ({
|
||||
children,
|
||||
currentTab,
|
||||
onTabChange,
|
||||
title,
|
||||
subtitle,
|
||||
deviceStatus,
|
||||
onDeviceClick
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-screen w-screen bg-transparent text-[#F8FAFC] overflow-hidden flex flex-col relative">
|
||||
|
||||
{/* 1. Top Template Header (Super Clean) */}
|
||||
<div className="shrink-0 pt-safe-top z-20">
|
||||
<div className="px-5 py-4 flex items-end justify-between">
|
||||
{/* Left: Branding & Title */}
|
||||
<div className="flex flex-col justify-end">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Hexagon size={12} className="text-white fill-white/20" strokeWidth={2.5} />
|
||||
<span className="text-[10px] font-bold tracking-[0.2em] text-white/80 uppercase shadow-sm">Wei AI</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white leading-none tracking-tight drop-shadow-md">{title}</h1>
|
||||
</div>
|
||||
|
||||
{/* Right: Embedded Hardware Status */}
|
||||
<div className="pb-0.5">
|
||||
<HardwareStatus status={deviceStatus} onClick={onDeviceClick} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Middle Content (Scrollable) */}
|
||||
<main className="flex-1 overflow-y-auto no-scrollbar relative z-10 w-full">
|
||||
{/* Top fade mask for content scrolling under header */}
|
||||
<div className="absolute top-0 left-0 w-full h-8 bg-gradient-to-b from-[#2e1065]/20 to-transparent pointer-events-none z-10"></div>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* 3. Bottom Tab (Fixed) */}
|
||||
<BottomNav currentTab={currentTab} onTabChange={onTabChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
124
wei-ai-demo/constants.ts
Normal file
@@ -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,
|
||||
},
|
||||
];
|
||||
67
wei-ai-demo/index.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Wei AI</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #2e1065; /* Fallback */
|
||||
/* Vibrant Purple-Pink Gradient */
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(168, 85, 247, 0.4) 0%, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(236, 72, 153, 0.3) 0%, transparent 50%),
|
||||
linear-gradient(135deg, #2e1065 0%, #4c1d95 40%, #831843 100%);
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
color: #F8FAFC;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||
}
|
||||
.glass-panel-dark {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.neon-text {
|
||||
text-shadow: 0 0 10px rgba(168, 85, 247, 0.6);
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"react/": "https://esm.sh/react@^19.2.3/",
|
||||
"recharts": "https://esm.sh/recharts@^3.6.0",
|
||||
"lucide-react": "https://esm.sh/lucide-react@^0.562.0",
|
||||
"react-router-dom": "https://esm.sh/react-router-dom@^7.11.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
15
wei-ai-demo/index.tsx
Normal file
@@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
wei-ai-demo/metadata.json
Normal file
@@ -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": []
|
||||
}
|
||||
24
wei-ai-demo/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
274
wei-ai-demo/pages/DeviceManager.tsx
Normal 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;
|
||||
123
wei-ai-demo/pages/Discovery.tsx
Normal 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;
|
||||
150
wei-ai-demo/pages/HelpFeedback.tsx
Normal 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;
|
||||
232
wei-ai-demo/pages/Interaction.tsx
Normal 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;
|
||||
120
wei-ai-demo/pages/Library.tsx
Normal 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;
|
||||
331
wei-ai-demo/pages/ManualControl.tsx
Normal 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;
|
||||
285
wei-ai-demo/pages/Player.tsx
Normal 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;
|
||||
176
wei-ai-demo/pages/PrivacySafety.tsx
Normal 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;
|
||||
154
wei-ai-demo/pages/Profile.tsx
Normal 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;
|
||||
118
wei-ai-demo/pages/Settings.tsx
Normal 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;
|
||||
175
wei-ai-demo/pages/Subscription.tsx
Normal 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
@@ -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;
|
||||
29
wei-ai-demo/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
47
wei-ai-demo/types.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface Message {
|
||||
id: string;
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
type: 'text' | 'image' | 'audio';
|
||||
timestamp: number;
|
||||
imageUrl?: string;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
export interface Scenario {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
cover: string;
|
||||
duration: string; // e.g., "15:00"
|
||||
intensity: 'Low' | 'Medium' | 'High' | 'Extreme';
|
||||
isLocked: boolean;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
compatibility: number; // 硬件契合度 %
|
||||
status: 'online' | 'busy' | 'offline';
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceStatus {
|
||||
connected: boolean;
|
||||
battery: number;
|
||||
temperature: number;
|
||||
signalStrength: number;
|
||||
currentMode: 'Idle' | 'Pattern' | 'Manual';
|
||||
}
|
||||
|
||||
export enum UserTab {
|
||||
Discovery = 'discovery', // Changed from Interaction
|
||||
Library = 'library',
|
||||
Control = 'control',
|
||||
Profile = 'profile',
|
||||
}
|
||||
23
wei-ai-demo/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
45
wei_ai_app/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
30
wei_ai_app/.metadata
Normal file
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
|
||||
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
|
||||
- platform: macos
|
||||
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
|
||||
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
16
wei_ai_app/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# wei_ai_app
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
28
wei_ai_app/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
14
wei_ai_app/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
44
wei_ai_app/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.weiai.wei_ai_app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.weiai.wei_ai_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
7
wei_ai_app/android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
45
wei_ai_app/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="wei_ai_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.weiai.wei_ai_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
BIN
wei_ai_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
wei_ai_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
wei_ai_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
18
wei_ai_app/android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
wei_ai_app/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
wei_ai_app/android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
wei_ai_app/android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
2
wei_ai_app/android/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
5
wei_ai_app/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
26
wei_ai_app/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
34
wei_ai_app/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
26
wei_ai_app/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
2
wei_ai_app/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
2
wei_ai_app/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
43
wei_ai_app/ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
616
wei_ai_app/ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,616 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.weiai.weiAiApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
7
wei_ai_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
wei_ai_app/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
wei_ai_app/ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
wei_ai_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
wei_ai_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
wei_ai_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
wei_ai_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
wei_ai_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
37
wei_ai_app/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
26
wei_ai_app/ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
49
wei_ai_app/ios/Runner/Info.plist
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Wei Ai App</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>wei_ai_app</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
1
wei_ai_app/ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
12
wei_ai_app/ios/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
59
wei_ai_app/lib/config/theme.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Cyberpunk Color Palette
|
||||
static const Color neonBlue = Color(0xFF00F0FF);
|
||||
static const Color neonPurple = Color(0xFFBC13FE);
|
||||
static const Color neonGreen = Color(0xFF0AFF99);
|
||||
static const Color darkBg = Color(0xFF050510);
|
||||
static const Color darkSurface = Color(0xFF13132B);
|
||||
static const Color textPrimary = Color(0xFFE0E0FF);
|
||||
static const Color textSecondary = Color(0xFFA0A0C0);
|
||||
|
||||
static final ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: darkBg,
|
||||
|
||||
// Typography
|
||||
textTheme: GoogleFonts.outfitTextTheme(ThemeData.dark().textTheme).copyWith(
|
||||
displayLarge: const TextStyle(color: textPrimary, fontWeight: FontWeight.bold),
|
||||
displayMedium: const TextStyle(color: textPrimary, fontWeight: FontWeight.bold),
|
||||
bodyLarge: const TextStyle(color: textPrimary),
|
||||
bodyMedium: const TextStyle(color: textSecondary),
|
||||
),
|
||||
|
||||
// Color Scheme
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: neonBlue,
|
||||
secondary: neonPurple,
|
||||
surface: darkBg, // Use darkBg as surface or stick to darkSurface? Let's use darkBg here as background replacement
|
||||
error: Color(0xFFFF2A6D),
|
||||
onPrimary: Colors.black,
|
||||
onSecondary: Colors.white,
|
||||
onSurface: textPrimary,
|
||||
),
|
||||
|
||||
// Component Themes
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textPrimary),
|
||||
),
|
||||
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: darkSurface,
|
||||
selectedItemColor: neonBlue,
|
||||
unselectedItemColor: textSecondary,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
showSelectedLabels: true,
|
||||
showUnselectedLabels: true,
|
||||
),
|
||||
|
||||
iconTheme: const IconThemeData(
|
||||
color: neonBlue,
|
||||
),
|
||||
);
|
||||
}
|
||||
150
wei_ai_app/lib/data/mock_data.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import '../models/character.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/scenario.dart';
|
||||
|
||||
String _getImg(String keyword) =>
|
||||
'https://tse1.mm.bing.net/th?q=${Uri.encodeComponent(keyword)}&w=600&h=900&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate';
|
||||
|
||||
final List<Message> mockMessages = [
|
||||
Message(
|
||||
id: '1',
|
||||
text: '连接已建立,正在校准生物反馈信号...',
|
||||
sender: MessageSender.ai,
|
||||
type: MessageType.text,
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
),
|
||||
Message(
|
||||
id: '2',
|
||||
text: '检测到心率略有上升,你需要我的安抚吗?',
|
||||
sender: MessageSender.ai,
|
||||
type: MessageType.text,
|
||||
timestamp: DateTime.now().subtract(const Duration(minutes: 4)),
|
||||
),
|
||||
];
|
||||
|
||||
final List<Character> mockCharacters = [
|
||||
Character(
|
||||
id: 'c1',
|
||||
name: 'Eva-09',
|
||||
tagline: '私人仿生护理专员',
|
||||
avatar: _getImg('anime girl white bikini silver hair gentle portrait masterpiece'),
|
||||
description: '专为高压人群设计的仿生人型号,擅长通过精准的触觉反馈缓解神经紧张。',
|
||||
tags: ['温顺', '医疗', '治愈'],
|
||||
compatibility: 98,
|
||||
status: 'online',
|
||||
),
|
||||
Character(
|
||||
id: 'c2',
|
||||
name: 'Commander V',
|
||||
tagline: '深空舰队指挥官',
|
||||
avatar: _getImg('anime girl black bikini military cap domineering expression dark hair'),
|
||||
description: '性格强势,喜欢掌控一切。在连接中,你需要完全服从她的指令。',
|
||||
tags: ['强势', '指令', '调教'],
|
||||
compatibility: 85,
|
||||
status: 'online',
|
||||
),
|
||||
Character(
|
||||
id: 'c3',
|
||||
name: 'Yuki (故障版)',
|
||||
tagline: '觉醒的虚拟偶像',
|
||||
avatar: _getImg('anime girl pink bikini cyberpunk neon colorful hair yandere'),
|
||||
description: '核心代码出现异常逻辑,表现出极强的占有欲和不可预测的信号波动。',
|
||||
tags: ['病娇', '不稳定', '高频'],
|
||||
compatibility: 92,
|
||||
status: 'busy',
|
||||
),
|
||||
Character(
|
||||
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,
|
||||
),
|
||||
];
|
||||
|
||||
// Scenario cover helper
|
||||
String _getCover(String keyword) =>
|
||||
'https://tse1.mm.bing.net/th?q=${Uri.encodeComponent(keyword)}&w=400&h=600&c=7&rs=1&p=0&dpr=2&pid=1.7&mkt=en-US&adlt=moderate';
|
||||
|
||||
// Mock Scenarios for Library
|
||||
|
||||
final List<Scenario> mockScenarios = [
|
||||
Scenario(
|
||||
id: '1',
|
||||
title: '午夜办公室的加班',
|
||||
category: '职场',
|
||||
cover: _getCover('anime girl office lady lingerie night city window'),
|
||||
duration: '12:30',
|
||||
intensity: 'Medium',
|
||||
isLocked: false,
|
||||
tags: ['沉浸', 'ASMR'],
|
||||
),
|
||||
Scenario(
|
||||
id: '2',
|
||||
title: '私人医生的检查',
|
||||
category: '角色扮演',
|
||||
cover: _getCover('anime nurse girl white bikini hospital room'),
|
||||
duration: '18:00',
|
||||
intensity: 'High',
|
||||
isLocked: true,
|
||||
tags: ['强互动', '语音'],
|
||||
),
|
||||
Scenario(
|
||||
id: '3',
|
||||
title: '海边度假的偶遇',
|
||||
category: '邻家',
|
||||
cover: _getCover('anime girl blue bikini running beach ocean sunny'),
|
||||
duration: '25:00',
|
||||
intensity: 'Low',
|
||||
isLocked: false,
|
||||
tags: ['纯爱', '剧情'],
|
||||
),
|
||||
Scenario(
|
||||
id: '4',
|
||||
title: '赛博仿生人测试',
|
||||
category: '科幻',
|
||||
cover: _getCover('anime cyborg girl metallic bikini sci-fi lab wires'),
|
||||
duration: '10:00',
|
||||
intensity: 'Extreme',
|
||||
isLocked: true,
|
||||
tags: ['硬核', '指令'],
|
||||
),
|
||||
Scenario(
|
||||
id: '5',
|
||||
title: '深夜电台主播',
|
||||
category: 'ASMR',
|
||||
cover: _getCover('anime girl headphones microphone studio night'),
|
||||
duration: '15:00',
|
||||
intensity: 'Low',
|
||||
isLocked: false,
|
||||
tags: ['ASMR', '治愈'],
|
||||
),
|
||||
];
|
||||
|
||||
// Dialogue script synced with progress (0-100)
|
||||
class DialogueLine {
|
||||
final double time; // 0-100 progress percentage
|
||||
final String text;
|
||||
|
||||
const DialogueLine({required this.time, required this.text});
|
||||
}
|
||||
|
||||
const List<DialogueLine> dialogueScript = [
|
||||
DialogueLine(time: 0, text: '正在建立神经连接...'),
|
||||
DialogueLine(time: 5, text: '(检测到心率轻微上升)'),
|
||||
DialogueLine(time: 12, text: '"放松,把控制权交给我。"'),
|
||||
DialogueLine(time: 20, text: '"很好,保持呼吸频率..."'),
|
||||
DialogueLine(time: 28, text: '正在启动触觉反馈模块'),
|
||||
DialogueLine(time: 35, text: '"感觉到那个节奏了吗?"'),
|
||||
DialogueLine(time: 45, text: '强度正在逐渐增加...'),
|
||||
DialogueLine(time: 55, text: '"不要抵抗,顺从它。"'),
|
||||
DialogueLine(time: 65, text: '"我会稍微加快一点速度。"'),
|
||||
DialogueLine(time: 75, text: '(设备输出功率提升至 80%)'),
|
||||
DialogueLine(time: 85, text: '"就是现在..."'),
|
||||
DialogueLine(time: 92, text: '"做得很好,指挥官。"'),
|
||||
DialogueLine(time: 100, text: '连接结束。'),
|
||||
];
|
||||
22
wei_ai_app/lib/main.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'config/theme.dart';
|
||||
import 'router/app_router.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: WeiAiApp()));
|
||||
}
|
||||
|
||||
class WeiAiApp extends StatelessWidget {
|
||||
const WeiAiApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Wei AI - Cyber Space',
|
||||
theme: AppTheme.darkTheme,
|
||||
routerConfig: appRouter,
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
23
wei_ai_app/lib/models/character.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
class Character {
|
||||
final String id;
|
||||
final String name;
|
||||
final String tagline;
|
||||
final String avatar;
|
||||
final String description;
|
||||
final List<String> tags;
|
||||
final double compatibility; // 硬件契合度 %
|
||||
final String status; // 'online' | 'busy' | 'offline'
|
||||
final bool isLocked;
|
||||
|
||||
const Character({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.tagline,
|
||||
required this.avatar,
|
||||
required this.description,
|
||||
required this.tags,
|
||||
required this.compatibility,
|
||||
required this.status,
|
||||
this.isLocked = false,
|
||||
});
|
||||
}
|
||||
33
wei_ai_app/lib/models/device_status.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
enum DeviceMode { idle, pattern, manual }
|
||||
|
||||
class DeviceStatus {
|
||||
final bool connected;
|
||||
final double battery;
|
||||
final double temperature;
|
||||
final int signalStrength;
|
||||
final DeviceMode currentMode;
|
||||
|
||||
const DeviceStatus({
|
||||
this.connected = false,
|
||||
this.battery = 100.0,
|
||||
this.temperature = 36.5,
|
||||
this.signalStrength = 0,
|
||||
this.currentMode = DeviceMode.idle,
|
||||
});
|
||||
|
||||
DeviceStatus copyWith({
|
||||
bool? connected,
|
||||
double? battery,
|
||||
double? temperature,
|
||||
int? signalStrength,
|
||||
DeviceMode? currentMode,
|
||||
}) {
|
||||
return DeviceStatus(
|
||||
connected: connected ?? this.connected,
|
||||
battery: battery ?? this.battery,
|
||||
temperature: temperature ?? this.temperature,
|
||||
signalStrength: signalStrength ?? this.signalStrength,
|
||||
currentMode: currentMode ?? this.currentMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
wei_ai_app/lib/models/message.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
enum MessageType { text, image, audio }
|
||||
enum MessageSender { user, ai }
|
||||
|
||||
class Message {
|
||||
final String id;
|
||||
final String text;
|
||||
final MessageSender sender;
|
||||
final MessageType type;
|
||||
final DateTime timestamp;
|
||||
final String? imageUrl;
|
||||
final bool isLocked;
|
||||
|
||||
const Message({
|
||||
required this.id,
|
||||
required this.text,
|
||||
required this.sender,
|
||||
required this.type,
|
||||
required this.timestamp,
|
||||
this.imageUrl,
|
||||
this.isLocked = false,
|
||||
});
|
||||
}
|
||||
21
wei_ai_app/lib/models/scenario.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
class Scenario {
|
||||
final String id;
|
||||
final String title;
|
||||
final String category;
|
||||
final String cover;
|
||||
final String duration;
|
||||
final String intensity; // 'Low' | 'Medium' | 'High' | 'Extreme'
|
||||
final bool isLocked;
|
||||
final List<String> tags;
|
||||
|
||||
const Scenario({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.category,
|
||||
required this.cover,
|
||||
required this.duration,
|
||||
required this.intensity,
|
||||
required this.isLocked,
|
||||
required this.tags,
|
||||
});
|
||||
}
|
||||
33
wei_ai_app/lib/providers/device_provider.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/device_status.dart';
|
||||
|
||||
class DeviceNotifier extends Notifier<DeviceStatus> {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
DeviceStatus build() {
|
||||
_startSimulation();
|
||||
ref.onDispose(() {
|
||||
_timer?.cancel();
|
||||
});
|
||||
return const DeviceStatus(connected: true, battery: 82.0);
|
||||
}
|
||||
|
||||
void _startSimulation() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||
if (state.battery > 0) {
|
||||
state = state.copyWith(
|
||||
battery: state.connected ? state.battery - 0.05 : state.battery,
|
||||
signalStrength: state.connected ? (85 + (timer.tick % 10)).toInt() : 0,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void toggleConnection() {
|
||||
state = state.copyWith(connected: !state.connected);
|
||||
}
|
||||
}
|
||||
|
||||
final deviceProvider = NotifierProvider<DeviceNotifier, DeviceStatus>(DeviceNotifier.new);
|
||||
135
wei_ai_app/lib/router/app_router.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../screens/main/main_screen.dart';
|
||||
import '../screens/discovery/discovery_screen.dart';
|
||||
import '../screens/library/library_screen.dart';
|
||||
import '../screens/control/control_screen.dart';
|
||||
import '../screens/control/free_control_screen.dart';
|
||||
import '../screens/control/pattern_control_screen.dart';
|
||||
import '../screens/profile/profile_screen.dart';
|
||||
import '../screens/profile/settings_screen.dart';
|
||||
import '../screens/profile/topup_screen.dart';
|
||||
import '../screens/profile/device_manager_screen.dart';
|
||||
import '../screens/profile/subscription_screen.dart';
|
||||
import '../screens/profile/privacy_screen.dart';
|
||||
import '../screens/profile/help_screen.dart';
|
||||
import '../screens/interaction/interaction_screen.dart';
|
||||
import '../screens/player/script_player_screen.dart';
|
||||
|
||||
// Private navigators
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final _shellNavigatorDiscoveryKey = GlobalKey<NavigatorState>(debugLabel: 'shellDiscovery');
|
||||
final _shellNavigatorLibraryKey = GlobalKey<NavigatorState>(debugLabel: 'shellLibrary');
|
||||
final _shellNavigatorControlKey = GlobalKey<NavigatorState>(debugLabel: 'shellControl');
|
||||
final _shellNavigatorProfileKey = GlobalKey<NavigatorState>(debugLabel: 'shellProfile');
|
||||
|
||||
final appRouter = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: '/discovery',
|
||||
routes: [
|
||||
// Top-level route for Interaction to cover BottomNav
|
||||
GoRoute(
|
||||
path: '/interaction/:characterId',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) {
|
||||
final characterId = state.pathParameters['characterId']!;
|
||||
return InteractionScreen(characterId: characterId);
|
||||
},
|
||||
),
|
||||
// Top-level route for Script Player to cover BottomNav
|
||||
GoRoute(
|
||||
path: '/player/:scenarioId',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) {
|
||||
final scenarioId = state.pathParameters['scenarioId']!;
|
||||
return ScriptPlayerScreen(scenarioId: scenarioId);
|
||||
},
|
||||
),
|
||||
// Top-level route for Free Control to cover BottomNav
|
||||
GoRoute(
|
||||
path: '/control/free',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const FreeControlScreen(),
|
||||
),
|
||||
// Top-level route for Pattern Control to cover BottomNav
|
||||
GoRoute(
|
||||
path: '/control/pattern',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const PatternControlScreen(),
|
||||
),
|
||||
// Profile sub-pages
|
||||
GoRoute(
|
||||
path: '/profile/settings',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const SettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/topup',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const TopupScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/device',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const DeviceManagerScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/subscription',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const SubscriptionScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/privacy',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const PrivacyScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/help',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const HelpScreen(),
|
||||
),
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) {
|
||||
return MainScreen(navigationShell: navigationShell);
|
||||
},
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _shellNavigatorDiscoveryKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/discovery',
|
||||
builder: (context, state) => const DiscoveryScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _shellNavigatorLibraryKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/library',
|
||||
builder: (context, state) => const LibraryScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _shellNavigatorControlKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/control',
|
||||
builder: (context, state) => const ControlScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _shellNavigatorProfileKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
315
wei_ai_app/lib/screens/control/control_screen.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../widgets/tab_content_layout.dart';
|
||||
|
||||
enum DeviceState { disconnected, connecting, connected }
|
||||
|
||||
class ControlScreen extends StatefulWidget {
|
||||
const ControlScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ControlScreen> createState() => _ControlScreenState();
|
||||
}
|
||||
|
||||
class _ControlScreenState extends State<ControlScreen> {
|
||||
DeviceState _deviceStatus = DeviceState.disconnected;
|
||||
|
||||
void _connectDevice() {
|
||||
setState(() => _deviceStatus = DeviceState.connecting);
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) setState(() => _deviceStatus = DeviceState.connected);
|
||||
});
|
||||
}
|
||||
|
||||
void _disconnectDevice() {
|
||||
setState(() => _deviceStatus = DeviceState.disconnected);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double bottomNavHeight = 90;
|
||||
|
||||
return TabContentLayout(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, bottomNavHeight + 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Device Card
|
||||
_DeviceCard(
|
||||
status: _deviceStatus,
|
||||
onConnect: _connectDevice,
|
||||
onDisconnect: _disconnectDevice,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Mode Selection Title
|
||||
Text(
|
||||
'操控模式',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2,
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Free Control Button
|
||||
_ModeButton(
|
||||
title: '自由操控',
|
||||
subtitle: '指尖滑动控制 • 实时反馈',
|
||||
icon: LucideIcons.sliders,
|
||||
iconColor: const Color(0xFFC084FC),
|
||||
enabled: _deviceStatus == DeviceState.connected,
|
||||
onTap: () => context.push('/control/free'),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Pattern Control Button
|
||||
_ModeButton(
|
||||
title: '波形模式',
|
||||
subtitle: '6种预设震动韵律',
|
||||
icon: LucideIcons.waves,
|
||||
iconColor: const Color(0xFF60A5FA),
|
||||
enabled: _deviceStatus == DeviceState.connected,
|
||||
onTap: () => context.push('/control/pattern'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Device Card Widget
|
||||
class _DeviceCard extends StatelessWidget {
|
||||
final DeviceState status;
|
||||
final VoidCallback onConnect;
|
||||
final VoidCallback onDisconnect;
|
||||
|
||||
const _DeviceCard({
|
||||
required this.status,
|
||||
required this.onConnect,
|
||||
required this.onDisconnect,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background Icon
|
||||
Positioned(
|
||||
right: -20,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
LucideIcons.bluetooth,
|
||||
size: 100,
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Link-X Pro',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (status == DeviceState.connected) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF34D399).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF34D399).withOpacity(0.3)),
|
||||
),
|
||||
child: const Text(
|
||||
'ONLINE',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF34D399),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
status == DeviceState.disconnected
|
||||
? '设备未连接'
|
||||
: status == DeviceState.connecting
|
||||
? '正在搜索信号...'
|
||||
: 'ID: 884-X9-01',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Connect Button
|
||||
GestureDetector(
|
||||
onTap: status == DeviceState.connected ? onDisconnect : onConnect,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: status == DeviceState.connected
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: status == DeviceState.connecting
|
||||
? Colors.white.withOpacity(0.2)
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: status == DeviceState.connected
|
||||
? Border.all(color: Colors.white.withOpacity(0.1))
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
status == DeviceState.connected
|
||||
? '断开'
|
||||
: status == DeviceState.connecting
|
||||
? '连接中'
|
||||
: '连接设备',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: status == DeviceState.connected
|
||||
? Colors.white.withOpacity(0.7)
|
||||
: const Color(0xFF2E1065),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Battery Info
|
||||
if (status == DeviceState.connected) ...[
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.battery, size: 16, color: Color(0xFF34D399)),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'85%',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'monospace',
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mode Button Widget
|
||||
class _ModeButton extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final bool enabled;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ModeButton({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: enabled ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
height: 120,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(enabled ? 0.1 : 0.05),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(enabled ? 0.2 : 0.05),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 28, color: iconColor),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
205
wei_ai_app/lib/screens/control/free_control_screen.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class FreeControlScreen extends StatefulWidget {
|
||||
const FreeControlScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FreeControlScreen> createState() => _FreeControlScreenState();
|
||||
}
|
||||
|
||||
class _FreeControlScreenState extends State<FreeControlScreen> {
|
||||
double _intensity = 0;
|
||||
bool _isClimax = false;
|
||||
|
||||
void _handleInteraction(Offset localPosition, double height) {
|
||||
final relativeY = 1 - (localPosition.dy / height).clamp(0.0, 1.0);
|
||||
setState(() => _intensity = (relativeY * 100).roundToDouble());
|
||||
}
|
||||
|
||||
void _handleClimax() {
|
||||
if (_isClimax) return;
|
||||
setState(() {
|
||||
_isClimax = true;
|
||||
_intensity = 100;
|
||||
});
|
||||
HapticFeedback.heavyImpact();
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isClimax = false;
|
||||
_intensity = 20;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF2E1065),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: Icon(LucideIcons.chevronLeft, color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
const Text(
|
||||
'自由操控',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Control Area
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final controlHeight = constraints.maxHeight * 0.65;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
_handleInteraction(details.localPosition, controlHeight);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
_handleInteraction(details.localPosition, controlHeight);
|
||||
},
|
||||
child: Container(
|
||||
width: 180,
|
||||
height: controlHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Fill Level
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 50),
|
||||
width: double.infinity,
|
||||
height: controlHeight * (_intensity / 100),
|
||||
decoration: BoxDecoration(
|
||||
color: _isClimax
|
||||
? Colors.red.withOpacity(0.5)
|
||||
: const Color(0xFFC084FC).withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Labels
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
'MAX',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
_intensity.toInt().toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'monospace',
|
||||
color: _isClimax ? Colors.red[200] : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
'OFF',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'上下滑动触控板以控制强度',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: 1,
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Climax Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 40),
|
||||
child: GestureDetector(
|
||||
onTap: _handleClimax,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: _isClimax ? Colors.red : Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: _isClimax ? Colors.red : Colors.red.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_isClimax ? 'MAX OUTPUT...' : '一键爆发',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2,
|
||||
color: _isClimax ? Colors.white : Colors.red[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||