353 lines
12 KiB
React
353 lines
12 KiB
React
import { useState, useEffect } from 'react';
|
||
import { Plus, Wind, Sparkles, Pencil, Trash2 } from 'lucide-react';
|
||
import { GlassCard, GlassButton, GlassInput, GlassTextarea } from '../components/ui';
|
||
import Modal from '../components/Modal';
|
||
import useStore from '../store/useStore';
|
||
|
||
/**
|
||
* 格式化 AI 反馈内容的组件
|
||
*/
|
||
const FeedbackContent = ({ content }) => {
|
||
if (!content) return null;
|
||
|
||
// 检查是否为结构化格式 (包含分隔符 --- 和标题标识 ####)
|
||
const isStructured = content.includes('---') && content.includes('####');
|
||
|
||
if (!isStructured) {
|
||
return (
|
||
<p className="text-xs italic text-white/50 leading-loose whitespace-pre-wrap">
|
||
{content}
|
||
</p>
|
||
);
|
||
}
|
||
|
||
// 解析结构化内容
|
||
const sections = content.split('---')
|
||
.map(s => s.trim())
|
||
.filter(s => s && s.length > 0);
|
||
|
||
return (
|
||
<div className="space-y-5 mt-2">
|
||
{sections.map((section, index) => {
|
||
// 移除 #### 前缀
|
||
const cleanSection = section.replace(/^####\s*/, '');
|
||
|
||
// 提取标题 (通常在 【】 中)
|
||
const titleMatch = cleanSection.match(/【(.*?)】/);
|
||
const title = titleMatch ? titleMatch[1] : '';
|
||
|
||
// 提取正文
|
||
let body = cleanSection;
|
||
if (titleMatch) {
|
||
body = cleanSection.replace(titleMatch[0], '').trim();
|
||
}
|
||
|
||
if (!title && !body) return null;
|
||
|
||
return (
|
||
<div key={index} className="text-xs leading-relaxed">
|
||
{title && (
|
||
<h5 className="text-orange-100 font-bold mb-2 flex items-center gap-2 text-[11px] tracking-wide">
|
||
{title}
|
||
</h5>
|
||
)}
|
||
<div className="text-white/60 pl-3 border-l-2 border-orange-200/10 space-y-1">
|
||
{body.split('\n').map((line, i) => {
|
||
const trimmedLine = line.trim();
|
||
if (!trimmedLine) return null;
|
||
// 简单的列表项处理
|
||
if (trimmedLine.startsWith('*') || trimmedLine.startsWith('-')) {
|
||
return (
|
||
<div key={i} className="flex gap-2 pl-1">
|
||
<span className="text-orange-200/40">•</span>
|
||
<span>{trimmedLine.substring(1).trim()}</span>
|
||
</div>
|
||
);
|
||
}
|
||
return <p key={i}>{trimmedLine}</p>;
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* TimelineView 组件
|
||
* 人生轨迹视图,显示和管理生命事件
|
||
*/
|
||
const TimelineView = () => {
|
||
const { lifeEvents, addLifeEvent, updateLifeEvent, deleteLifeEvent, loadLifeEvents } = useStore();
|
||
|
||
// 加载生命事件
|
||
useEffect(() => {
|
||
loadLifeEvents().catch(() => {
|
||
// 后端不可用时忽略错误
|
||
});
|
||
}, [loadLifeEvents]);
|
||
|
||
// 模态框状态
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
|
||
// 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID)
|
||
const [editingEventId, setEditingEventId] = useState(null);
|
||
|
||
// 删除确认状态
|
||
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
||
|
||
// 表单状态
|
||
const [eventForm, setEventForm] = useState({
|
||
title: '',
|
||
time: '',
|
||
content: ''
|
||
});
|
||
|
||
/**
|
||
* 打开新增模态框
|
||
*/
|
||
const openAddModal = () => {
|
||
setEditingEventId(null);
|
||
setEventForm({ title: '', time: '', content: '' });
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
/**
|
||
* 打开编辑模态框
|
||
* @param {Object} event - 要编辑的事件
|
||
*/
|
||
const openEditModal = (event) => {
|
||
setEditingEventId(event.id);
|
||
setEventForm({
|
||
title: event.title || '',
|
||
time: event.time || '',
|
||
content: event.content || ''
|
||
});
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
/**
|
||
* 关闭模态框
|
||
*/
|
||
const closeModal = () => {
|
||
setIsModalOpen(false);
|
||
setEditingEventId(null);
|
||
setEventForm({ title: '', time: '', content: '' });
|
||
};
|
||
|
||
/**
|
||
* 处理表单提交(新增或编辑)
|
||
*/
|
||
const handleSubmit = async () => {
|
||
if (!eventForm.title || !eventForm.time || !eventForm.content) {
|
||
alert('请完整填写记录。');
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
|
||
try {
|
||
if (editingEventId) {
|
||
// 编辑模式:调用更新接口
|
||
await updateLifeEvent({
|
||
id: editingEventId,
|
||
...eventForm
|
||
});
|
||
} else {
|
||
// 新增模式:调用添加接口
|
||
await addLifeEvent({
|
||
...eventForm
|
||
});
|
||
}
|
||
|
||
// 重置表单并关闭模态框
|
||
closeModal();
|
||
} catch (error) {
|
||
console.error('Failed to save event:', error);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理删除确认
|
||
* @param {string} id - 事件ID
|
||
*/
|
||
const handleDeleteConfirm = async (id) => {
|
||
try {
|
||
await deleteLifeEvent(id);
|
||
setDeleteConfirmId(null);
|
||
} catch (error) {
|
||
console.error('Failed to delete event:', error);
|
||
alert('删除失败,请稍后重试');
|
||
setDeleteConfirmId(null);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 按事件时间倒序排列(最新的在最上面)
|
||
* 空日期的事件排在最后
|
||
*/
|
||
const sortedEvents = [...lifeEvents].sort((a, b) => {
|
||
// 如果两个都没有时间,保持原顺序
|
||
if (!a.time && !b.time) return 0;
|
||
// 没有时间的排在后面
|
||
if (!a.time) return 1;
|
||
if (!b.time) return -1;
|
||
// 按时间倒序(最新的在前)
|
||
return 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={openAddModal}
|
||
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 relative">
|
||
{/* 操作按钮 */}
|
||
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||
<button
|
||
onClick={() => openEditModal(event)}
|
||
className="p-2 rounded-full bg-white/5 hover:bg-orange-200/10 text-white/30 hover:text-orange-200 transition-all"
|
||
title="修改足迹"
|
||
>
|
||
<Pencil className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => setDeleteConfirmId(event.id)}
|
||
className="p-2 rounded-full bg-white/5 hover:bg-red-400/10 text-white/30 hover:text-red-400 transition-all"
|
||
title="删除足迹"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-start mb-4 pr-20">
|
||
<div className="flex items-center gap-3">
|
||
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
|
||
{event.tags && event.tags.length > 0 && (
|
||
<span className="text-[9px] px-2 py-1 rounded-full bg-orange-200/10 text-orange-200/60 uppercase tracking-wider">
|
||
{event.tags[0] === 'childhood' ? '童年' : event.tags[0] === 'joy' ? '高光' : event.tags[0] === 'low' ? '低谷' : event.tags[0]}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<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 反馈区域 - 仅在有反馈时显示 */}
|
||
{event.aiFeedback && (
|
||
<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-4">
|
||
<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>
|
||
<FeedbackContent content={event.aiFeedback} />
|
||
</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={closeModal} title={editingEventId ? '修改足迹' : '记录足迹'}>
|
||
<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>
|
||
|
||
{/* 删除确认模态框 */}
|
||
<Modal isOpen={!!deleteConfirmId} onClose={() => setDeleteConfirmId(null)} title="确认删除" maxWidth="sm">
|
||
<div className="space-y-6">
|
||
<p className="text-white/70 text-sm">
|
||
确定要删除这段人生足迹吗?此操作不可恢复,相关的 AI 洞察也将被删除。
|
||
</p>
|
||
<div className="flex gap-4">
|
||
<GlassButton
|
||
onClick={() => setDeleteConfirmId(null)}
|
||
className="flex-1"
|
||
>
|
||
取消
|
||
</GlassButton>
|
||
<GlassButton
|
||
variant="primary"
|
||
onClick={() => handleDeleteConfirm(deleteConfirmId)}
|
||
className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20"
|
||
>
|
||
确认删除
|
||
</GlassButton>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default TimelineView;
|