前端重构实现

This commit is contained in:
2025-12-22 16:38:06 +08:00
parent cd6d995d5a
commit 26574e3db7
54 changed files with 8976 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Header, Sidebar } from '../components/layout';
import TimelineView from '../views/TimelineView';
import ScriptView from '../views/ScriptView';
import PathView from '../views/PathView';
import ProfileModal from '../views/ProfileModal';
/**
* DashboardPage 组件
* 仪表盘主页面,包含侧边栏导航和内容区
*/
const DashboardPage = () => {
// 当前激活的视图
const [activeView, setActiveView] = useState('timeline');
// 用户资料模态框状态
const [isProfileOpen, setIsProfileOpen] = useState(false);
/**
* 处理视图切换
*/
const handleViewChange = (view) => {
setActiveView(view);
};
/**
* 渲染当前视图内容
*/
const renderView = () => {
switch (activeView) {
case 'timeline':
return <TimelineView />;
case 'script':
return <ScriptView onOpenProfile={() => setIsProfileOpen(true)} />;
case 'path':
return <PathView onGoToScript={() => setActiveView('script')} />;
default:
return <TimelineView />;
}
};
return (
<>
{/* 头部 */}
<Header
showNav
onProfileClick={() => setIsProfileOpen(true)}
/>
{/* 主内容区 */}
<div className="glass-card w-full h-full overflow-hidden">
<div className="grid grid-cols-1 md:grid-cols-12 h-full">
{/* 侧边栏 */}
<Sidebar
activeView={activeView}
onViewChange={handleViewChange}
/>
{/* 内容区 */}
<section className="md:col-span-9 p-8 overflow-y-auto custom-scrollbar relative">
<AnimatePresence mode="wait">
<motion.div
key={activeView}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className="h-full"
>
{renderView()}
</motion.div>
</AnimatePresence>
</section>
</div>
</div>
{/* 用户资料模态框 */}
<ProfileModal
isOpen={isProfileOpen}
onClose={() => setIsProfileOpen(false)}
/>
</>
);
};
export default DashboardPage;
+145
View File
@@ -0,0 +1,145 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { GlassCard, GlassInput, GlassButton } from '../components/ui';
import useStore from '../store/useStore';
import useCountdown from '../hooks/useCountdown';
/**
* LoginPage 组件
* 登录页面,包含手机号和验证码输入
*/
const LoginPage = () => {
const navigate = useNavigate();
const { login, getSmsCode, setLogin, loading } = useStore();
// 表单状态
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// 倒计时
const { countdown, isActive, start } = useCountdown(60);
/**
* 处理获取验证码
*/
const handleGetCode = async () => {
if (phone.length !== 11) {
alert('请输入正确的手机号');
return;
}
try {
start();
// 调用后端获取验证码
await getSmsCode(phone);
alert('验证码已发送');
} catch (error) {
// 后端不可用时,使用模拟验证码
alert('验证码已发送 (模拟验证码: 888888)');
}
};
/**
* 处理登录提交
*/
const handleSubmit = async () => {
if (phone.length !== 11) {
alert('请输入正确的手机号');
return;
}
if (code.length !== 6) {
alert('请输入6位验证码');
return;
}
setIsSubmitting(true);
try {
// 尝试调用后端登录
await login(phone, code);
navigate('/onboarding');
} catch (error) {
// 后端不可用时,使用本地验证
if (code === '888888') {
setLogin(true, phone);
navigate('/onboarding');
} else {
alert('验证失败,请检查手机号或验证码');
}
} finally {
setIsSubmitting(false);
}
};
return (
<div className="w-full h-full flex items-center justify-center p-6 animate-fade-in">
<GlassCard className="max-w-md w-full space-y-8 border-white/5 shadow-2xl" padding="lg">
{/* 标题区域 */}
<div className="text-center space-y-2">
<h2 className="text-3xl font-serif tracking-wider text-white/90">
欢迎回来
</h2>
<p className="text-sm text-white/40 italic">
开启你的数字生命档案
</p>
</div>
{/* 表单区域 */}
<div className="space-y-4">
{/* 手机号输入 */}
<GlassInput
label="手机号码"
type="tel"
placeholder="输入手机号"
value={phone}
onChange={setPhone}
maxLength={11}
className="text-center tracking-[0.1em]"
/>
{/* 验证码输入 */}
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<GlassInput
label="验证码"
type="text"
placeholder="六位验证码"
value={code}
onChange={setCode}
maxLength={6}
className="text-center"
/>
</div>
<div className="flex items-end">
<button
onClick={handleGetCode}
disabled={isActive || loading}
className="w-full h-[46px] rounded-2xl border border-white/5 bg-white/5 text-[10px] uppercase tracking-tighter hover:bg-white/10 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isActive ? `${countdown}S` : '获取'}
</button>
</div>
</div>
</div>
{/* 登录按钮 */}
<GlassButton
variant="primary"
onClick={handleSubmit}
loading={isSubmitting || loading}
className="w-full"
>
开启旅程
</GlassButton>
{/* 协议文字 */}
<p className="text-[10px] text-center text-white/20 px-4 leading-relaxed">
登录即代表同意用户协议隐私政策我们将妥善保管您的生命数据
</p>
</GlassCard>
</div>
);
};
export default LoginPage;
+158
View File
@@ -0,0 +1,158 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowRight, Check } from 'lucide-react';
import { GlassCard, GlassInput, GlassTextarea, GlassButton } from '../components/ui';
import { PromptTagGroup } from '../components/PromptTag';
import useStore from '../store/useStore';
import { inspirationClusters } from '../utils/constants';
/**
* OnboardingPage 组件
* 5步入站流程页面
*/
const OnboardingPage = () => {
const navigate = useNavigate();
const {
currentStep,
setCurrentStep,
registrationData,
updateRegistration,
saveUserProfile,
setView,
loading
} = useStore();
const [formData, setFormData] = useState(registrationData);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
setFormData(registrationData);
}, [registrationData]);
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const updateNestedField = (parent, field, value) => {
setFormData(prev => ({
...prev,
[parent]: { ...prev[parent], [field]: value }
}));
};
const handleTagClick = (type, text) => {
const currentText = formData[type]?.text || '';
updateNestedField(type, 'text', currentText + text);
};
const saveStepData = () => {
updateRegistration(formData);
};
const handleNext = async () => {
saveStepData();
if (currentStep < 5) {
setCurrentStep(currentStep + 1);
} else {
setIsSaving(true);
try {
await saveUserProfile();
} catch (error) {
console.error('保存档案失败:', error);
} finally {
setIsSaving(false);
setView('dashboard');
navigate('/dashboard');
}
}
};
const handlePrev = () => {
saveStepData();
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const renderStep1 = () => (
<div className="animate-fade-in space-y-8">
<div className="mb-6">
<h2 className="text-4xl font-serif mb-3">你是谁</h2>
<p className="text-white/40 italic text-sm">定义你生命坐标的初始属性</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
<GlassInput label="称呼" placeholder="例如:林中鹿" value={formData.nickname} onChange={(v) => updateField('nickname', v)} />
<GlassInput label="性别" placeholder="自由填写" value={formData.gender} onChange={(v) => updateField('gender', v)} />
<GlassInput label="MBTI" placeholder="如:INFJ" value={formData.mbti} onChange={(v) => updateField('mbti', v)} />
<GlassInput label="星座" placeholder="星辰指引" value={formData.zodiac} onChange={(v) => updateField('zodiac', v)} />
</div>
<GlassInput
label="兴趣爱好"
placeholder="用逗号分隔你的热爱"
value={Array.isArray(formData.hobbies) ? formData.hobbies.join(',') : formData.hobbies}
onChange={(v) => updateField('hobbies', v.split(',').map(s => s.trim()).filter(s => s))}
/>
</div>
);
const renderMemoryStep = (type, title, label) => (
<div className="animate-fade-in space-y-6">
<div>
<h2 className="text-4xl font-serif mb-3">{title}</h2>
<p className="text-white/40 italic text-sm">回望足迹这些瞬间如何塑造了此时的你</p>
</div>
<div className="space-y-4">
<GlassInput label={`${label}的日期`} type="date" value={formData[type]?.date || ''} onChange={(v) => updateNestedField(type, 'date', v)} className="max-w-xs" />
<GlassTextarea label="详细描述" placeholder="描述那段时光发生的点滴..." value={formData[type]?.text || ''} onChange={(v) => updateNestedField(type, 'text', v)} rows={5} />
<PromptTagGroup tags={inspirationClusters[type]} onTagClick={(text) => handleTagClick(type, text)} />
</div>
</div>
);
const renderStep5 = () => (
<div className="animate-fade-in space-y-8">
<div className="mb-6">
<h2 className="text-4xl font-serif mb-3">未来想成为谁</h2>
<p className="text-white/40 italic text-sm">勾勒你对理想生活的全部向往</p>
</div>
<GlassTextarea label="对未来的憧憬" placeholder="你想成为一个什么样的人?" value={formData.future?.vision || ''} onChange={(v) => updateNestedField('future', 'vision', v)} rows={4} />
<GlassTextarea label="理想生活状态" placeholder="你的理想清晨与傍晚是怎样的?" value={formData.future?.ideal || ''} onChange={(v) => updateNestedField('future', 'ideal', v)} rows={4} />
</div>
);
const renderStepContent = () => {
switch (currentStep) {
case 1: return renderStep1();
case 2: return renderMemoryStep('childhood', '那段纯真的时光', '童年记忆');
case 3: return renderMemoryStep('joy', '光芒闪耀的时刻', '开心的经历');
case 4: return renderMemoryStep('low', '在暗夜中潜行', '沮丧与低谷');
case 5: return renderStep5();
default: return null;
}
};
return (
<GlassCard className="w-full h-full flex flex-col justify-between overflow-hidden relative" padding="lg">
<div className="flex-1 flex flex-col justify-center max-w-2xl mx-auto w-full overflow-y-auto custom-scrollbar">
{renderStepContent()}
</div>
<div className="flex items-center justify-between mt-10 max-w-2xl mx-auto w-full border-t border-white/5 pt-8">
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((step) => (
<div key={step} className={`h-1 rounded-full transition-all duration-500 ${step === currentStep ? 'w-8 bg-orange-200' : 'w-3 bg-white/10'}`} />
))}
</div>
<div className="flex gap-4">
{currentStep > 1 && (
<button onClick={handlePrev} className="text-white/40 px-6 py-2 text-sm hover:text-white transition-colors">返回</button>
)}
<GlassButton onClick={handleNext} loading={isSaving || loading} className="px-8 py-3 rounded-full text-orange-200 font-bold tracking-widest text-sm shadow-xl shadow-orange-900/10">
{currentStep === 5 ? (<>开启人生 <Check className="w-4 h-4 ml-2" /></>) : (<>继续 <ArrowRight className="w-4 h-4 ml-2" /></>)}
</GlassButton>
</div>
</div>
</GlassCard>
);
};
export default OnboardingPage;