前端重构实现
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Map, Loader2 } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GlassCard, GlassButton } from '../components/ui';
|
||||
import useStore from '../store/useStore';
|
||||
import { generatePath } from '../services/ai';
|
||||
|
||||
/**
|
||||
* PathView 组件
|
||||
* 实现路径视图,基于剧本生成可执行的人生路径
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onGoToScript - 跳转到剧本视图回调
|
||||
*/
|
||||
const PathView = ({ onGoToScript }) => {
|
||||
const { getSelectedScript, selectedPath, setPath, loadPath, selectedScriptId } = useStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const selectedScript = getSelectedScript();
|
||||
|
||||
// 加载已有路径
|
||||
useEffect(() => {
|
||||
if (selectedScriptId) {
|
||||
loadPath(selectedScriptId).catch(() => {
|
||||
// 后端不可用时忽略错误
|
||||
});
|
||||
}
|
||||
}, [selectedScriptId, loadPath]);
|
||||
|
||||
/**
|
||||
* 处理路径生成
|
||||
*/
|
||||
const handleGenerate = async () => {
|
||||
if (!selectedScript) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const path = await generatePath(selectedScript.content);
|
||||
await setPath(path, selectedScriptId);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate path:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析路径内容为步骤数组
|
||||
*/
|
||||
const parsePathSteps = (path) => {
|
||||
if (!path) return [];
|
||||
|
||||
return path
|
||||
.split(/【/)
|
||||
.filter(s => s.trim())
|
||||
.map((s, index) => {
|
||||
const parts = s.split(/】/);
|
||||
return {
|
||||
title: parts[0] || '',
|
||||
content: parts[1] || '',
|
||||
index: index + 1
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const pathSteps = parsePathSteps(selectedPath);
|
||||
|
||||
// 无剧本时显示提示
|
||||
if (!selectedScript) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-32 opacity-30 text-center">
|
||||
<Map className="w-16 h-16 mb-4" />
|
||||
<p className="font-serif italic text-xl">先生成剧本,方能洞察路径。</p>
|
||||
<GlassButton
|
||||
onClick={onGoToScript}
|
||||
className="mt-6 px-6 py-2 rounded-full text-xs"
|
||||
>
|
||||
去生成剧本
|
||||
</GlassButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-12 pb-20">
|
||||
{/* 标题区域 */}
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h3 className="text-4xl font-serif">实现路径</h3>
|
||||
<p className="text-sm text-white/30 mt-2">
|
||||
基于《{selectedScript.theme}》,拆解达成目标的每一步。
|
||||
</p>
|
||||
</div>
|
||||
<GlassButton
|
||||
onClick={handleGenerate}
|
||||
disabled={isLoading}
|
||||
className="px-8 py-3 rounded-full text-sm font-bold bg-blue-400/5 text-blue-300 border-blue-400/20"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
规划中...
|
||||
</>
|
||||
) : (
|
||||
selectedPath ? '重新推演' : '开启人生导航'
|
||||
)}
|
||||
</GlassButton>
|
||||
</div>
|
||||
|
||||
{/* 路径步骤展示 */}
|
||||
<div className="space-y-6">
|
||||
{pathSteps.length > 0 ? (
|
||||
pathSteps.map((step, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||
>
|
||||
<GlassCard className="border-l-4 border-l-blue-400/40 bg-blue-400/[0.01]" padding="lg">
|
||||
<h5 className="text-blue-200 font-bold mb-4 flex items-center gap-3">
|
||||
<span className="w-6 h-6 rounded-full bg-blue-400/20 text-[10px] flex items-center justify-center">
|
||||
{step.index}
|
||||
</span>
|
||||
{step.title}
|
||||
</h5>
|
||||
<div className="text-white/60 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{step.content}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</motion.div>
|
||||
))
|
||||
) : (
|
||||
<div className="py-20 text-center text-white/20 italic font-serif">
|
||||
等待开启人生导航...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PathView;
|
||||
@@ -0,0 +1,214 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Settings, Settings2 } from 'lucide-react';
|
||||
import Modal from '../components/Modal';
|
||||
import { GlassButton, GlassInput } from '../components/ui';
|
||||
import useStore from '../store/useStore';
|
||||
|
||||
/**
|
||||
* ProfileModal 组件
|
||||
* 用户资料模态框,支持查看和编辑模式
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - 是否打开
|
||||
* @param {Function} props.onClose - 关闭回调
|
||||
*/
|
||||
const ProfileModal = ({ isOpen, onClose }) => {
|
||||
const { registrationData, lifeEvents, scripts, updateRegistration, saveUserProfile, clear } = useStore();
|
||||
|
||||
// 编辑模式状态
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 编辑表单状态
|
||||
const [editForm, setEditForm] = useState({
|
||||
nickname: registrationData.nickname,
|
||||
profession: registrationData.profession || '',
|
||||
mbti: registrationData.mbti,
|
||||
zodiac: registrationData.zodiac,
|
||||
hobbies: registrationData.hobbies?.join(', ') || ''
|
||||
});
|
||||
|
||||
// 同步 registrationData 到 editForm
|
||||
useEffect(() => {
|
||||
setEditForm({
|
||||
nickname: registrationData.nickname,
|
||||
profession: registrationData.profession || '',
|
||||
mbti: registrationData.mbti,
|
||||
zodiac: registrationData.zodiac,
|
||||
hobbies: registrationData.hobbies?.join(', ') || ''
|
||||
});
|
||||
}, [registrationData]);
|
||||
|
||||
/**
|
||||
* 处理保存
|
||||
*/
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
updateRegistration({
|
||||
nickname: editForm.nickname,
|
||||
profession: editForm.profession,
|
||||
mbti: editForm.mbti,
|
||||
zodiac: editForm.zodiac,
|
||||
hobbies: editForm.hobbies.split(',').map(s => s.trim()).filter(s => s)
|
||||
});
|
||||
await saveUserProfile();
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
// 即使后端保存失败,本地已更新
|
||||
setIsEditing(false);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理取消编辑
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
setEditForm({
|
||||
nickname: registrationData.nickname,
|
||||
profession: registrationData.profession || '',
|
||||
mbti: registrationData.mbti,
|
||||
zodiac: registrationData.zodiac,
|
||||
hobbies: registrationData.hobbies?.join(', ') || ''
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理清除数据
|
||||
*/
|
||||
const handleClear = () => {
|
||||
if (confirm('确定要删除所有记录吗?此操作不可逆。')) {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染查看模式
|
||||
*/
|
||||
const renderViewMode = () => (
|
||||
<div className="animate-fade-in space-y-8">
|
||||
{/* 用户头像和基本信息 */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-orange-400/20 to-orange-600/20 flex items-center justify-center text-3xl border border-white/10">
|
||||
{(registrationData.nickname || '人').charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-2xl font-serif text-white/90">
|
||||
{registrationData.nickname || '旅行者'}
|
||||
</h4>
|
||||
<p className="text-[10px] text-white/30 uppercase tracking-[0.2em] mt-1">
|
||||
{registrationData.mbti || '-'} | {registrationData.zodiac || '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-white/[0.02] rounded-2xl border border-white/5 text-center">
|
||||
<div className="text-lg font-serif text-orange-200">{lifeEvents.length}</div>
|
||||
<div className="text-[9px] text-white/30 uppercase tracking-widest mt-1">生命足迹</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white/[0.02] rounded-2xl border border-white/5 text-center">
|
||||
<div className="text-lg font-serif text-blue-200">{scripts.length}</div>
|
||||
<div className="text-[9px] text-white/30 uppercase tracking-widest mt-1">天命卷轴</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="space-y-4 pt-4 border-t border-white/5">
|
||||
<GlassButton
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="w-full py-4 text-sm font-bold flex gap-3 items-center justify-center"
|
||||
>
|
||||
<Settings className="w-4 h-4" /> 编辑资料
|
||||
</GlassButton>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="w-full py-4 text-[10px] text-red-400/40 hover:text-red-400 uppercase tracking-widest transition-colors"
|
||||
>
|
||||
清除数据并退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* 渲染编辑模式
|
||||
*/
|
||||
const renderEditMode = () => (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 编辑标题 */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-12 h-12 rounded-full bg-orange-200/10 flex items-center justify-center">
|
||||
<Settings2 className="text-orange-200 w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-serif">个人设定</h4>
|
||||
<p className="text-xs text-white/40">在这里调整你的人生航向基础信息</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 编辑表单 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<GlassInput
|
||||
label="昵称"
|
||||
placeholder="你想被如何称呼?"
|
||||
value={editForm.nickname}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, nickname: v }))}
|
||||
/>
|
||||
<GlassInput
|
||||
label="职业"
|
||||
placeholder="你当下的社会锚点"
|
||||
value={editForm.profession}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, profession: v }))}
|
||||
/>
|
||||
<GlassInput
|
||||
label="MBTI"
|
||||
placeholder="性格色彩"
|
||||
value={editForm.mbti}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, mbti: v }))}
|
||||
/>
|
||||
<GlassInput
|
||||
label="星座"
|
||||
placeholder="星辰指引"
|
||||
value={editForm.zodiac}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, zodiac: v }))}
|
||||
/>
|
||||
</div>
|
||||
<GlassInput
|
||||
label="兴趣爱好"
|
||||
placeholder="让灵魂起舞的事物"
|
||||
value={editForm.hobbies}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, hobbies: v }))}
|
||||
/>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-4 mt-8 pt-6 border-t border-white/5">
|
||||
<GlassButton
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
className="flex-1 py-3 bg-orange-200/10 text-orange-100 font-bold tracking-widest"
|
||||
>
|
||||
保存修改
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
onClick={handleCancel}
|
||||
className="px-6 py-3 text-white/40"
|
||||
>
|
||||
返回
|
||||
</GlassButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
{isEditing ? renderEditMode() : renderViewMode()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileModal;
|
||||
@@ -0,0 +1,219 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { UserCog, PenTool, Sparkles, BookOpen, Loader2 } from 'lucide-react';
|
||||
import { GlassCard, GlassButton, GlassInput, GlassSelect } from '../components/ui';
|
||||
import useStore from '../store/useStore';
|
||||
import { generateEpicScript } from '../services/ai';
|
||||
import { scriptStyles, scriptLengths } from '../utils/constants';
|
||||
|
||||
/**
|
||||
* ScriptView 组件
|
||||
* 爽文剧本视图,包含角色设定、创作需求和剧本展示
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onOpenProfile - 打开用户资料模态框回调
|
||||
*/
|
||||
const ScriptView = ({ onOpenProfile }) => {
|
||||
const {
|
||||
registrationData,
|
||||
lifeEvents,
|
||||
scripts,
|
||||
selectedScriptId,
|
||||
addScript,
|
||||
setSelectedScriptId,
|
||||
getSelectedScript,
|
||||
loadScripts
|
||||
} = useStore();
|
||||
|
||||
// 加载剧本列表
|
||||
useEffect(() => {
|
||||
loadScripts().catch(() => {
|
||||
// 后端不可用时忽略错误
|
||||
});
|
||||
}, [loadScripts]);
|
||||
|
||||
// 表单状态
|
||||
const [theme, setTheme] = useState('');
|
||||
const [style, setStyle] = useState(scriptStyles[0].value);
|
||||
const [length, setLength] = useState(scriptLengths[0].value);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* 处理剧本生成
|
||||
*/
|
||||
const handleGenerate = async () => {
|
||||
if (!theme) {
|
||||
alert('请输入主题');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const content = await generateEpicScript(
|
||||
{ theme, style, length, character: registrationData },
|
||||
lifeEvents
|
||||
);
|
||||
|
||||
addScript({ theme, style, length, content });
|
||||
setTheme('');
|
||||
} catch (error) {
|
||||
console.error('Failed to generate script:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化剧本内容,高亮【标题】
|
||||
*/
|
||||
const formatScriptContent = (content) => {
|
||||
if (!content) return '';
|
||||
return content.replace(
|
||||
/【([^】]+)】/g,
|
||||
'<div class="mt-8 mb-4 text-orange-100 font-bold text-lg border-l-2 border-orange-400 pl-4">【$1】</div>'
|
||||
);
|
||||
};
|
||||
|
||||
const selectedScript = getSelectedScript();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* 左侧面板 */}
|
||||
<div className="lg:col-span-4 space-y-6">
|
||||
{/* 角色设定卡片 */}
|
||||
<GlassCard className="border-white/10 space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-white/5">
|
||||
<UserCog className="w-4 h-4 text-orange-200" />
|
||||
<h4 className="text-sm font-bold tracking-widest text-white/80 uppercase">角色设定</h4>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-4 text-[11px]">
|
||||
<div>
|
||||
<label className="text-white/20 block">昵称</label>
|
||||
<span className="text-white/70">{registrationData.nickname || '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-white/20 block">星座</label>
|
||||
<span className="text-white/70">{registrationData.zodiac || '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-white/20 block">MBTI</label>
|
||||
<span className="text-white/70">{registrationData.mbti || '-'}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-white/20 block">兴趣爱好</label>
|
||||
<span className="text-white/70">
|
||||
{registrationData.hobbies?.join(', ') || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onOpenProfile}
|
||||
className="w-full py-2 text-[10px] text-orange-200/50 hover:text-orange-200 border border-white/5 rounded-xl transition-all"
|
||||
>
|
||||
修改人设
|
||||
</button>
|
||||
</GlassCard>
|
||||
|
||||
{/* 创作需求表单 */}
|
||||
<GlassCard className="border-white/10 space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-white/5">
|
||||
<PenTool className="w-4 h-4 text-orange-200" />
|
||||
<h4 className="text-sm font-bold tracking-widest text-white/80 uppercase">创作需求</h4>
|
||||
</div>
|
||||
<GlassInput
|
||||
label="剧本主题"
|
||||
placeholder="例如:我在职场逆袭了"
|
||||
value={theme}
|
||||
onChange={setTheme}
|
||||
/>
|
||||
<GlassSelect
|
||||
label="叙事风格"
|
||||
options={scriptStyles}
|
||||
value={style}
|
||||
onChange={setStyle}
|
||||
/>
|
||||
<GlassSelect
|
||||
label="剧本篇幅"
|
||||
options={scriptLengths}
|
||||
value={length}
|
||||
onChange={setLength}
|
||||
/>
|
||||
<GlassButton
|
||||
onClick={handleGenerate}
|
||||
disabled={isLoading}
|
||||
className="w-full py-4 bg-orange-200/5 text-orange-200 font-bold text-sm tracking-widest border-orange-200/20"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
编撰中...
|
||||
</>
|
||||
) : (
|
||||
'开启天命编撰'
|
||||
)}
|
||||
</GlassButton>
|
||||
</GlassCard>
|
||||
|
||||
{/* 历史卷轴列表 */}
|
||||
<div className="space-y-4">
|
||||
<h5 className="text-[10px] text-white/20 uppercase tracking-widest font-bold px-2">
|
||||
历史卷轴
|
||||
</h5>
|
||||
<div className="space-y-2 max-h-[25vh] overflow-y-auto custom-scrollbar">
|
||||
{scripts.length > 0 ? (
|
||||
scripts.map((script) => (
|
||||
<div
|
||||
key={script.id}
|
||||
onClick={() => setSelectedScriptId(script.id)}
|
||||
className={`
|
||||
p-3 glass-card text-left cursor-pointer hover:bg-white/5 border-white/5 transition-all
|
||||
${script.id === selectedScriptId ? 'border-orange-200/30 bg-orange-200/5' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="text-[11px] text-white/80 truncate">{script.theme}</div>
|
||||
<div className="text-[9px] text-white/30 flex justify-between mt-1">
|
||||
<span>{script.style} | {script.length}</span>
|
||||
<span>{script.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center text-xs text-white/10 py-4 italic">暂无卷轴</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧剧本展示区 */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="h-full">
|
||||
{selectedScript ? (
|
||||
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in" padding="lg">
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<div className="flex justify-between items-center mb-8 pb-4 border-b border-white/5">
|
||||
<div>
|
||||
<h4 className="text-2xl font-serif text-orange-200">{selectedScript.theme}</h4>
|
||||
<p className="text-[10px] text-white/30 mt-1 uppercase tracking-widest">
|
||||
{selectedScript.style}篇 · {selectedScript.length}卷
|
||||
</p>
|
||||
</div>
|
||||
<BookOpen className="text-white/20" />
|
||||
</div>
|
||||
<div
|
||||
className="text-white/70 leading-loose whitespace-pre-wrap space-y-6 text-sm"
|
||||
dangerouslySetInnerHTML={{ __html: formatScriptContent(selectedScript.content) }}
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center opacity-20 py-32">
|
||||
<Sparkles className="w-20 h-20 mb-6" />
|
||||
<p className="text-xl font-serif">请在左侧设定需求,开启你的天命爽文</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptView;
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Wind, Sparkles } from 'lucide-react';
|
||||
import { GlassCard, GlassButton, GlassInput, GlassTextarea } from '../components/ui';
|
||||
import Modal from '../components/Modal';
|
||||
import useStore from '../store/useStore';
|
||||
import { analyzeLifeEvent } from '../services/ai';
|
||||
|
||||
/**
|
||||
* TimelineView 组件
|
||||
* 生命长河视图,显示和管理生命事件
|
||||
*/
|
||||
const TimelineView = () => {
|
||||
const { lifeEvents, addLifeEvent, loadLifeEvents } = useStore();
|
||||
|
||||
// 加载生命事件
|
||||
useEffect(() => {
|
||||
loadLifeEvents().catch(() => {
|
||||
// 后端不可用时忽略错误
|
||||
});
|
||||
}, [loadLifeEvents]);
|
||||
|
||||
// 模态框状态
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 表单状态
|
||||
const [eventForm, setEventForm] = useState({
|
||||
title: '',
|
||||
time: '',
|
||||
content: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理表单提交
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!eventForm.title || !eventForm.time || !eventForm.content) {
|
||||
alert('请完整填写记录。');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// 调用 AI 分析
|
||||
const aiFeedback = await analyzeLifeEvent(eventForm);
|
||||
|
||||
// 添加事件
|
||||
addLifeEvent({
|
||||
...eventForm,
|
||||
aiFeedback
|
||||
});
|
||||
|
||||
// 重置表单并关闭模态框
|
||||
setEventForm({ title: '', time: '', content: '' });
|
||||
setIsModalOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze event:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 按时间倒序排列事件
|
||||
*/
|
||||
const sortedEvents = [...lifeEvents].sort(
|
||||
(a, b) => new Date(b.time) - new Date(a.time)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 标题区域 */}
|
||||
<div className="flex justify-between items-end mb-12">
|
||||
<div>
|
||||
<h3 className="text-4xl font-serif text-white/90">生命长河</h3>
|
||||
<p className="text-sm text-white/30 mt-2">塑造你的每一刻,都被星辰见证。</p>
|
||||
</div>
|
||||
<GlassButton
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="px-6 py-3 rounded-full text-sm font-bold flex items-center gap-2 bg-orange-200/5 text-orange-200 border-orange-200/20 shadow-lg"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> 记录足迹
|
||||
</GlassButton>
|
||||
</div>
|
||||
|
||||
{/* 时间线容器 */}
|
||||
<div className="relative pl-8">
|
||||
{sortedEvents.length > 0 && <div className="timeline-line" />}
|
||||
|
||||
<div className="space-y-10">
|
||||
{sortedEvents.length > 0 ? (
|
||||
sortedEvents.map((event) => (
|
||||
<div key={event.id} className="relative group">
|
||||
{/* 时间线点 */}
|
||||
<div className="timeline-dot absolute left-[-39px] top-6 z-10" />
|
||||
|
||||
{/* 事件卡片 */}
|
||||
<GlassCard className="border-white/5 hover:border-orange-200/20 transition-all duration-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
|
||||
<span className="text-[10px] font-mono tracking-widest text-white/30 uppercase">
|
||||
{event.time}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 leading-relaxed mb-6">
|
||||
{event.content}
|
||||
</p>
|
||||
|
||||
{/* AI 反馈区域 */}
|
||||
<div className="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles className="w-3 h-3 text-orange-200" />
|
||||
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">
|
||||
引路人洞察
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs italic text-white/50 leading-loose">
|
||||
{event.aiFeedback}
|
||||
</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center opacity-30">
|
||||
<Wind className="w-12 h-12 mb-4" />
|
||||
<p className="font-serif italic text-lg">此间尚无回响,等待你执笔...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 添加事件模态框 */}
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} title="记录足迹">
|
||||
<div className="space-y-6">
|
||||
<GlassInput
|
||||
label="事件标题"
|
||||
placeholder="给这段经历起个名字"
|
||||
value={eventForm.title}
|
||||
onChange={(v) => setEventForm(prev => ({ ...prev, title: v }))}
|
||||
/>
|
||||
<GlassInput
|
||||
label="发生时间"
|
||||
type="date"
|
||||
value={eventForm.time}
|
||||
onChange={(v) => setEventForm(prev => ({ ...prev, time: v }))}
|
||||
/>
|
||||
<GlassTextarea
|
||||
label="经历详情"
|
||||
placeholder="当时发生了什么?你的感受如何?"
|
||||
value={eventForm.content}
|
||||
onChange={(v) => setEventForm(prev => ({ ...prev, content: v }))}
|
||||
rows={5}
|
||||
/>
|
||||
<GlassButton
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? '正在共鸣生命轨迹...' : '开启 AI 疗愈'}
|
||||
</GlassButton>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineView;
|
||||
Reference in New Issue
Block a user