Files
happy-life-star/life-script/src/views/TimelineView.jsx
T
2025-12-24 15:20:58 +08:00

353 lines
12 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;