人生轨迹代码初始化
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useStoreData } from '../hooks/useStoreData';
|
||||
import { Store } from '../utils/store';
|
||||
import { AudioEngine } from '../utils/audioEngine';
|
||||
import { TimelineView } from '../components/views/TimelineView';
|
||||
import { ScriptView } from '../components/views/ScriptView';
|
||||
import { PathView } from '../components/views/PathView';
|
||||
import {
|
||||
Compass,
|
||||
BookOpen,
|
||||
Film,
|
||||
Map,
|
||||
Music,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
RotateCcw,
|
||||
Menu,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export function DashboardPage() {
|
||||
const data = useStoreData();
|
||||
const [activeTab, setActiveTab] = useState('timeline');
|
||||
const [isMusicPlaying, setIsMusicPlaying] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
// Initialize Audio Engine state based on store
|
||||
useEffect(() => {
|
||||
// Sync initial state if needed
|
||||
if (!data.audioMuted) {
|
||||
// AudioEngine manages its own state
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMusicToggle = async () => {
|
||||
try {
|
||||
const playing = AudioEngine.toggle();
|
||||
setIsMusicPlaying(playing);
|
||||
} catch (e) {
|
||||
console.error("Audio failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (window.confirm('确定要清空所有数据重新开始吗?这将回到注册页面。')) {
|
||||
Store.reset();
|
||||
// App.jsx will handle the redirect because store data changes
|
||||
}
|
||||
};
|
||||
|
||||
const NavButton = ({ tab, icon: Icon, label, mobileLabel }) => (
|
||||
<button
|
||||
onClick={() => { setActiveTab(tab); setIsMobileMenuOpen(false); }}
|
||||
className={clsx(
|
||||
"w-full text-left px-4 py-3 rounded-xl flex items-center gap-3 transition-all duration-300 relative overflow-hidden group",
|
||||
activeTab === tab
|
||||
? "bg-primary/20 text-primary font-bold shadow-[0_0_15px_rgba(16,185,129,0.1)] border border-primary/20"
|
||||
: "text-gray-400 hover:text-white hover:bg-white/5 border border-transparent"
|
||||
)}
|
||||
>
|
||||
<Icon className={clsx("w-5 h-5 transition-transform", activeTab === tab ? "text-primary" : "text-gray-400 group-hover:text-white", activeTab !== tab && "group-hover:scale-110")} />
|
||||
<span className="hidden md:inline">{label}</span>
|
||||
<span className="md:hidden">{mobileLabel || label}</span>
|
||||
{activeTab === tab && (
|
||||
<div className="absolute right-0 top-0 bottom-0 w-1 bg-primary shadow-[0_0_10px_#10b981]"></div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col md:flex-row transition-all duration-500 font-sans text-gray-100 bg-deep-sea overflow-hidden">
|
||||
{/* Ambient Background */}
|
||||
<div className="fixed inset-0 pointer-events-none z-0">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_50%_50%,rgba(16,185,129,0.05),transparent_50%)]"></div>
|
||||
<div className="absolute top-[-20%] left-[-10%] w-[50%] h-[50%] bg-primary/5 rounded-full blur-[120px] animate-pulse-slow"></div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden bg-black/20 backdrop-blur-xl border-b border-white/10 p-4 flex justify-between items-center z-50 relative">
|
||||
<div className="flex items-center gap-2 font-bold text-lg">
|
||||
<Compass className="text-primary w-6 h-6 animate-spin-slow" />
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-accent">人生轨迹</span>
|
||||
</div>
|
||||
<button onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}>
|
||||
{isMobileMenuOpen ? <X className="text-white" /> : <Menu className="text-white" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar (Desktop) / Drawer (Mobile) */}
|
||||
<nav className={clsx(
|
||||
"bg-black/20 backdrop-blur-xl border-r border-white/10 w-full md:w-72 flex-shrink-0 flex flex-col justify-between z-40 fixed md:relative h-[calc(100vh-64px)] md:h-screen top-16 md:top-0 left-0 transition-transform duration-300",
|
||||
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
|
||||
)}>
|
||||
{/* Background Decoration */}
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-primary/5 pointer-events-none"></div>
|
||||
|
||||
<div className="p-6 overflow-y-auto relative z-10">
|
||||
<h1 className="hidden md:flex text-2xl font-bold tracking-wider mb-8 items-center gap-3 text-transparent bg-clip-text bg-gradient-to-r from-primary to-white">
|
||||
<Compass className="text-primary stroke-2 animate-spin-slow" /> 人生轨迹
|
||||
</h1>
|
||||
|
||||
{/* User Card */}
|
||||
<div className="flex items-center gap-4 mb-8 p-4 bg-white/5 rounded-2xl backdrop-blur-sm border border-white/10 hover:bg-white/10 transition-colors cursor-default group">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-blue-600 flex items-center justify-center text-white font-bold text-xl shadow-inner relative overflow-hidden group-hover:scale-105 transition-transform shrink-0 border border-white/20">
|
||||
{data.userProfile.nickname?.[0] || 'U'}
|
||||
<div className="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<div className="font-bold text-white truncate">{data.userProfile.nickname || '旅人'}</div>
|
||||
<div className="text-xs text-primary flex items-center gap-1.5 mt-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_5px_rgba(16,185,129,0.5)] animate-pulse"></span>
|
||||
{data.userProfile.mbti} · {data.userProfile.zodiac || '未知'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="space-y-2">
|
||||
<NavButton tab="timeline" icon={BookOpen} label="时空日记" mobileLabel="日记" />
|
||||
<NavButton tab="script" icon={Film} label="剧本生成器" mobileLabel="剧本" />
|
||||
<NavButton tab="path" icon={Map} label="实现路径" mobileLabel="路径" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 text-xs text-gray-500 border-t border-white/5 space-y-4 bg-black/40 backdrop-blur-md md:bg-transparent relative z-10">
|
||||
{/* Music Player */}
|
||||
<button
|
||||
onClick={handleMusicToggle}
|
||||
className={clsx(
|
||||
"flex items-center justify-between w-full px-4 py-3 rounded-xl bg-white/5 hover:bg-white/10 transition-all border group",
|
||||
isMusicPlaying ? "border-primary/30 shadow-[0_0_10px_rgba(16,185,129,0.1)]" : "border-white/5"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3 text-gray-300">
|
||||
<div className={clsx("w-8 h-8 rounded-full flex items-center justify-center transition-colors", isMusicPlaying ? "bg-primary/20 text-primary" : "bg-white/10 text-gray-400")}>
|
||||
{isMusicPlaying ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />}
|
||||
</div>
|
||||
<span>疗愈背景音</span>
|
||||
</div>
|
||||
{isMusicPlaying && (
|
||||
<div className="flex gap-0.5 h-3 items-end">
|
||||
<span className="w-0.5 bg-primary animate-music-wave-1"></span>
|
||||
<span className="w-0.5 bg-primary animate-music-wave-2"></span>
|
||||
<span className="w-0.5 bg-primary animate-music-wave-3"></span>
|
||||
<span className="w-0.5 bg-primary animate-music-wave-4"></span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="flex items-center gap-2 hover:text-red-400 transition-colors w-full px-4 py-2 rounded hover:bg-red-500/10 justify-center"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" /> 重置所有数据
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 overflow-y-auto h-[calc(100vh-64px)] md:h-screen relative z-10 scroll-smooth p-4 md:p-0 custom-scrollbar">
|
||||
{activeTab === 'timeline' && <TimelineView />}
|
||||
{activeTab === 'script' && <ScriptView onSwitchToPath={() => setActiveTab('path')} />}
|
||||
{activeTab === 'path' && <PathView onSwitchToScript={() => setActiveTab('script')} />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Compass, ArrowRight, X, Phone, Lock, Loader2 } from 'lucide-react';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import request from '../utils/request';
|
||||
import clsx from 'clsx';
|
||||
|
||||
// PncyssD Prototype: Landing Page
|
||||
export function LandingPage({ onStart }) {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Login Form State
|
||||
const [phone, setPhone] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) setIsLoggedIn(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (countdown > 0) {
|
||||
timer = setInterval(() => setCountdown(c => c - 1), 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [countdown]);
|
||||
|
||||
const handleGetCode = async () => {
|
||||
if (!phone) return alert('请输入手机号');
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) return alert('手机号格式不正确');
|
||||
|
||||
try {
|
||||
// Backend: /auth/sms-code?phone=... (AuthController.java)
|
||||
// Note: "Business type" is not required by the current backend implementation.
|
||||
const res = await request.get('/auth/sms-code', { params: { phone } });
|
||||
|
||||
if (res.code === 200) {
|
||||
setCountdown(60);
|
||||
// Display backend message or dev code
|
||||
const msg = res.data?.message || '验证码已发送';
|
||||
if (res.data?.code) {
|
||||
alert(`【测试模式】${msg}\n验证码: ${res.data.code}`);
|
||||
} else {
|
||||
alert(msg);
|
||||
}
|
||||
} else {
|
||||
console.warn('SMS Code Error:', res);
|
||||
alert(res.message || '发送失败,请稍后重试');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get SMS code:', e);
|
||||
const errorMsg = e.response?.data?.message || '网络连接异常,请检查您的网络设置';
|
||||
alert(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!phone || !code) return alert('请填写完整信息');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request.post('/auth/login', { phone, smsCode: code });
|
||||
if (res.code === 200) {
|
||||
const { accessToken } = res.data;
|
||||
localStorage.setItem('token', accessToken);
|
||||
setIsLoggedIn(true);
|
||||
setShowLoginModal(false);
|
||||
// Clear sensitive data
|
||||
setPhone('');
|
||||
setCode('');
|
||||
} else {
|
||||
alert(res.message || '登录失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('登录异常,请检查网络');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center text-center p-6 relative overflow-hidden">
|
||||
{/* Background & Effects */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-[100px] animate-pulse-slow"></div>
|
||||
|
||||
<div className="relative z-10 space-y-8 max-w-lg mx-auto w-full">
|
||||
<div className="mb-4 inline-block p-4 rounded-full bg-white/5 border border-white/10 shadow-2xl animate-float">
|
||||
<Compass className="w-16 h-16 text-primary icon-spin-slow" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white tracking-tight landing-title">
|
||||
人生轨迹
|
||||
<span className="block text-xl md:text-2xl font-light mt-4 text-primary/80">Life Trajectory</span>
|
||||
</h1>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-col items-center justify-center gap-4 mt-12 w-full max-w-xs mx-auto">
|
||||
{isLoggedIn ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={onStart}
|
||||
className="w-full animate-in fade-in zoom-in duration-500"
|
||||
>
|
||||
开启旅程 <ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform ml-2" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => setShowLoginModal(true)}
|
||||
className="w-full shadow-[0_0_20px_rgba(16,185,129,0.3)] hover:shadow-[0_0_30px_rgba(16,185,129,0.5)] transition-all"
|
||||
>
|
||||
登录账号
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Modal */}
|
||||
{showLoginModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="relative w-full max-w-md bg-[#0f172a] border border-white/10 rounded-2xl shadow-2xl p-8 overflow-hidden">
|
||||
{/* Modal Background */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 pointer-events-none"></div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowLoginModal(false)}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-primary rounded-full"></span>
|
||||
欢迎回来
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1 text-left">
|
||||
<label className="text-xs font-bold text-gray-400 uppercase tracking-wider ml-1">手机号</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="请输入手机号"
|
||||
className="pl-10"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-left">
|
||||
<label className="text-xs font-bold text-gray-400 uppercase tracking-wider ml-1">验证码</label>
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="请输入验证码"
|
||||
className="pl-10"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-28 whitespace-nowrap"
|
||||
onClick={handleGetCode}
|
||||
disabled={countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full mt-6 py-3 text-lg"
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <Loader2 className="w-5 h-5 animate-spin mx-auto" /> : '登 录'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { useState } from 'react';
|
||||
import { User, Lock, Eye, EyeOff, ArrowRight, ArrowLeft } from 'lucide-react';
|
||||
import { GlassCard } from '../components/ui/GlassCard';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Checkbox } from '../components/ui/Checkbox';
|
||||
import { Store } from '../utils/store';
|
||||
|
||||
export function LoginPage({ onLoginSuccess, onBack, onSignUp }) {
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
// Mock login delay
|
||||
setTimeout(() => {
|
||||
if (!formData.username || !formData.password) {
|
||||
setError('请输入用户名和密码');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple mock validation
|
||||
if (formData.password.length < 6) {
|
||||
setError('密码长度不能少于6位');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success
|
||||
Store.updateProfile({
|
||||
nickname: formData.username,
|
||||
// In a real app, we'd store a token
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
onLoginSuccess();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 relative z-10 animate-fade-in">
|
||||
|
||||
<div className="absolute top-8 left-8">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-white/60 hover:text-white transition-colors group"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>返回首页</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<GlassCard className="w-full max-w-md p-8 md:p-10 relative overflow-hidden">
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-primary/20 rounded-full blur-3xl pointer-events-none"></div>
|
||||
<div className="absolute -bottom-10 -left-10 w-40 h-40 bg-aurora-green/20 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-bold text-white mb-2 tracking-tight">欢迎回来</h2>
|
||||
<p className="text-gray-400">登录您的 Emotion Museum 账号</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-300 ml-1">用户名 / 邮箱</label>
|
||||
<div className="relative group">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-primary transition-colors" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入您的账号"
|
||||
className="pl-12 w-full"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({...formData, username: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-300 ml-1">密码</label>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-primary transition-colors" />
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="请输入您的密码"
|
||||
className="pl-12 pr-12 w-full"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({...formData, password: e.target.value})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors focus:outline-none"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Checkbox
|
||||
label="记住我"
|
||||
checked={formData.rememberMe}
|
||||
onChange={(checked) => setFormData({...formData, rememberMe: checked})}
|
||||
/>
|
||||
<button type="button" className="text-sm text-primary hover:text-aurora-green transition-colors">
|
||||
忘记密码?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-200 text-sm flex items-center gap-2 animate-shake">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-500"></span>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{!isLoading && (
|
||||
<>
|
||||
立即登录 <ArrowRight className="w-4 h-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 text-center text-sm text-gray-400">
|
||||
还没有账号?
|
||||
<button
|
||||
onClick={onSignUp}
|
||||
className="text-primary hover:text-aurora-green font-bold ml-1 hover:underline transition-all"
|
||||
>
|
||||
立即注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Store } from '../utils/store';
|
||||
import { ArrowLeft, ArrowRight, Check, X, Sparkles, Star, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input, Select, Textarea } from '../components/ui/Input';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const ZODIAC_SIGNS = [
|
||||
"白羊座", "金牛座", "双子座", "巨蟹座",
|
||||
"狮子座", "处女座", "天秤座", "天蝎座",
|
||||
"射手座", "摩羯座", "水瓶座", "双鱼座"
|
||||
];
|
||||
|
||||
const MBTI_TYPES = ['INTJ','INTP','ENTJ','ENTP','INFJ','INFP','ENFJ','ENFP','ISTJ','ISFJ','ESTJ','ESFJ','ISTP','ISFP','ESTP','ESFP'];
|
||||
|
||||
export function OnboardingPage({ onFinish }) {
|
||||
const [step, setStep] = useState(0);
|
||||
const [formData, setFormData] = useState(Store.get().userProfile);
|
||||
const [toast, setToast] = useState(null); // { msg, type }
|
||||
|
||||
const showToast = (msg, type = 'error') => {
|
||||
setToast({ msg, type });
|
||||
setTimeout(() => setToast(null), 2500);
|
||||
};
|
||||
|
||||
const updateFormData = (key, value) => {
|
||||
setFormData(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const updateHistory = (type, field, value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
history: {
|
||||
...prev.history,
|
||||
[type]: {
|
||||
...prev.history?.[type],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (step === 0) {
|
||||
if (!formData.nickname?.trim()) { showToast('请填写昵称'); return; }
|
||||
if (!formData.mbti) { showToast('请选择MBTI人格类型'); return; }
|
||||
} else if (step === 4) {
|
||||
if (!formData.futureVision?.trim()) { showToast('写下一句对未来的期许吧'); return; }
|
||||
Store.updateProfile(formData);
|
||||
Store.completeOnboarding();
|
||||
onFinish();
|
||||
return;
|
||||
}
|
||||
setStep(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handlePrev = () => setStep(prev => prev - 1);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col relative overflow-hidden transition-colors duration-1000">
|
||||
{/* Background Blobs */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/10 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 animate-pulse-slow"></div>
|
||||
<div className="absolute bottom-0 left-0 w-80 h-80 bg-secondary/20 rounded-full blur-[80px] translate-y-1/3 -translate-x-1/3 animate-pulse-slow" style={{ animationDelay: '2s' }}></div>
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className={clsx(
|
||||
"fixed top-10 left-1/2 -translate-x-1/2 px-6 py-3 rounded-full shadow-xl z-50 animate-fade-in flex items-center gap-2",
|
||||
toast.type === 'error' ? 'bg-red-500/90 text-white' : 'bg-green-500/90 text-white'
|
||||
)}>
|
||||
{toast.type === 'error' ? <AlertCircle className="w-4 h-4" /> : <CheckCircle className="w-4 h-4" />}
|
||||
{toast.msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="relative z-10 px-6 py-8 flex justify-between items-center">
|
||||
<div className="flex gap-1">
|
||||
{[0,1,2,3,4].map(i => (
|
||||
<div key={i} className={clsx(
|
||||
"h-1 rounded-full transition-all duration-500",
|
||||
i <= step ? 'w-8 bg-primary shadow-[0_0_10px_rgba(42,157,143,0.5)]' : 'w-2 bg-white/10'
|
||||
)}></div>
|
||||
))}
|
||||
</div>
|
||||
<button className="text-white/40 hover:text-white transition-colors" onClick={() => { if(confirm('退出注册将不保存进度,确定吗?')) window.location.reload() }}>
|
||||
<X className="w-6 h-6 hover:rotate-90 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col justify-center px-6 pb-24 max-w-xl mx-auto w-full relative z-10">
|
||||
{step === 0 && (
|
||||
<div className="animate-fade-in space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-2xl font-bold text-white">你是谁?</h2>
|
||||
<p className="text-gray-400 text-sm mt-2">让我们先认识一下彼此。</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-400 mb-1 ml-1">昵称 <span className="text-red-400">*</span></label>
|
||||
<Input
|
||||
placeholder="你希望世界如何称呼你?"
|
||||
value={formData.nickname || ''}
|
||||
onChange={e => updateFormData('nickname', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-400 mb-1 ml-1">性别</label>
|
||||
<Select value={formData.gender || 'secret'} onChange={e => updateFormData('gender', e.target.value)}>
|
||||
<option value="male">男</option>
|
||||
<option value="female">女</option>
|
||||
<option value="secret">保密</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-400 mb-1 ml-1">星座</label>
|
||||
<Select value={formData.zodiac || ''} onChange={e => updateFormData('zodiac', e.target.value)}>
|
||||
<option value="" disabled>选择星座</option>
|
||||
{ZODIAC_SIGNS.map(z => <option key={z} value={z}>{z}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-400 mb-1 ml-1">MBTI 人格 <span className="text-red-400">*</span></label>
|
||||
<Select value={formData.mbti || ''} onChange={e => updateFormData('mbti', e.target.value)}>
|
||||
<option value="" disabled>选择你的类型</option>
|
||||
{MBTI_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-400 mb-1 ml-1">兴趣爱好 (用逗号分隔)</label>
|
||||
<Input
|
||||
placeholder="例如:摄影, 登山, 编程"
|
||||
value={(formData.hobbies || []).join(', ')}
|
||||
onChange={e => updateFormData('hobbies', e.target.value.split(/[,,]/).map(s => s.trim()))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="animate-fade-in space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<span className="text-amber-300 text-xs tracking-widest uppercase mb-2 block">Part 1 / 3</span>
|
||||
<h2 className="text-2xl font-bold text-white">你是如何成为了现在的自己?</h2>
|
||||
<p className="text-gray-400 text-sm mt-2">闭上眼,回到开始的地方。你的童年是怎么度过的?</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 p-6 rounded-2xl border border-amber-500/20 shadow-lg shadow-amber-500/5">
|
||||
<label className="block text-xs font-bold text-amber-300/80 mb-2">大概的时间</label>
|
||||
<Input type="date" className="mb-4" value={formData.history?.childhood?.date || ''} onChange={e => updateHistory('childhood', 'date', e.target.value)} />
|
||||
|
||||
<label className="block text-xs font-bold text-amber-300/80 mb-2">记忆中的画面</label>
|
||||
<Textarea rows={5} placeholder="比如:外婆的蒲扇,炎热的下午,或者第一次离家..." value={formData.history?.childhood?.content || ''} onChange={e => updateHistory('childhood', 'content', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="animate-fade-in space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<span className="text-primary text-xs tracking-widest uppercase mb-2 block">Part 2 / 3</span>
|
||||
<h2 className="text-2xl font-bold text-white">闪闪发光的日子</h2>
|
||||
<p className="text-gray-400 text-sm mt-2">在这个过程中,让你感到非常开心的经历是什么?</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 p-6 rounded-2xl border border-primary/20 shadow-lg shadow-primary/5">
|
||||
<label className="block text-xs font-bold text-primary/80 mb-2">发生日期</label>
|
||||
<Input type="date" className="mb-4" value={formData.history?.peak?.date || ''} onChange={e => updateHistory('peak', 'date', e.target.value)} />
|
||||
|
||||
<label className="block text-xs font-bold text-primary/80 mb-2">那发生了什么?</label>
|
||||
<Textarea rows={5} placeholder="比如:收到心仪的offer,一次完美的旅行,或者被理解的瞬间..." value={formData.history?.peak?.content || ''} onChange={e => updateHistory('peak', 'content', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="animate-fade-in space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<span className="text-blue-400 text-xs tracking-widest uppercase mb-2 block">Part 3 / 3</span>
|
||||
<h2 className="text-2xl font-bold text-white">风雨兼程</h2>
|
||||
<p className="text-gray-400 text-sm mt-2">一段十分沮丧和低谷的时光,你是如何度过的?</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 p-6 rounded-2xl border border-blue-400/20 shadow-lg shadow-blue-400/5">
|
||||
<label className="block text-xs font-bold text-blue-400/80 mb-2">发生日期</label>
|
||||
<Input type="date" className="mb-4" value={formData.history?.valley?.date || ''} onChange={e => updateHistory('valley', 'date', e.target.value)} />
|
||||
|
||||
<label className="block text-xs font-bold text-blue-400/80 mb-2">当时的感受与成长</label>
|
||||
<Textarea rows={5} placeholder="那个挑战是什么?现在回看,它带给了你什么?" value={formData.history?.valley?.content || ''} onChange={e => updateHistory('valley', 'content', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<div className="animate-fade-in space-y-6">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-2xl font-bold text-white">你未来想成为怎样的人?</h2>
|
||||
<p className="text-gray-400 text-sm mt-2">对自己的憧憬以及对理想生活状态的憧憬。</p>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<Sparkles className="absolute -top-6 -left-4 text-accent w-8 h-8 animate-pulse-slow" />
|
||||
<Textarea rows={4} className="text-lg leading-relaxed text-center group-hover:border-accent/50 transition-colors" placeholder="三年后,我希望过着这样的生活..." value={formData.futureVision || ''} onChange={e => updateFormData('futureVision', e.target.value)} />
|
||||
<Star className="absolute -bottom-6 -right-4 text-accent w-6 h-6 animate-pulse-slow" style={{ animationDelay: '1s' }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Controls */}
|
||||
<div className="fixed bottom-0 left-0 w-full p-6 bg-deep-sea/80 backdrop-blur-lg border-t border-white/5 flex justify-between items-center z-20">
|
||||
{step > 0 ? (
|
||||
<button onClick={handlePrev} className="text-gray-400 hover:text-white flex items-center gap-2 transition-colors group">
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" /> 上一步
|
||||
</button>
|
||||
) : <div></div>}
|
||||
|
||||
<Button onClick={handleNext} className="flex items-center gap-2 px-8 group hover:scale-105 active:scale-95 shadow-lg shadow-primary/20">
|
||||
<span>{step === 4 ? '完成创建' : '下一步'}</span>
|
||||
{step === 4 ? <Check className="w-4 h-4" /> : <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user