增加修改和删除功能
This commit is contained in:
@@ -139,7 +139,7 @@ function App() {
|
||||
<Background />
|
||||
|
||||
{/* 主容器 */}
|
||||
<main className="relative z-10 min-h-screen flex flex-col items-center justify-center p-4 md:p-8">
|
||||
<main className="relative z-10 h-screen flex flex-col items-center justify-center p-4 md:p-8 overflow-hidden">
|
||||
<AnimatedRoutes />
|
||||
</main>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -42,7 +42,10 @@ const Modal = ({
|
||||
</Dialog.Overlay>
|
||||
|
||||
{/* 内容区 */}
|
||||
<Dialog.Content asChild>
|
||||
<Dialog.Content
|
||||
asChild
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
@@ -56,21 +59,21 @@ const Modal = ({
|
||||
>
|
||||
{/* 关闭按钮 */}
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
<button
|
||||
className="absolute top-6 right-6 text-white/40 hover:text-white transition-colors"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
|
||||
|
||||
{/* 标题 */}
|
||||
{title && (
|
||||
<Dialog.Title className="text-2xl font-serif mb-6">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="max-h-[70vh] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;600&family=Noto+Sans+SC:wght@300;400;500&display=swap');
|
||||
/* 使用国内镜像加载字体,避免 Google Fonts 访问超时 */
|
||||
@import url('https://fonts.loli.net/css2?family=Noto+Serif+SC:wght@300;600&family=Noto+Sans+SC:wght@300;400;500&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
|
||||
@@ -40,23 +40,23 @@ const DashboardPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 头部 */}
|
||||
<Header
|
||||
showNav
|
||||
onProfileClick={() => setIsProfileOpen(true)}
|
||||
<div className="flex flex-col h-screen max-h-screen overflow-hidden">
|
||||
{/* 头部 - 固定高度 */}
|
||||
<Header
|
||||
showNav
|
||||
onProfileClick={() => setIsProfileOpen(true)}
|
||||
/>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div className="glass-card w-full h-full overflow-hidden">
|
||||
{/* 主内容区 - 占据剩余高度 */}
|
||||
<div className="glass-card flex-1 overflow-hidden">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 h-full">
|
||||
{/* 侧边栏 */}
|
||||
<Sidebar
|
||||
activeView={activeView}
|
||||
onViewChange={handleViewChange}
|
||||
{/* 侧边栏 - 固定不滚动 */}
|
||||
<Sidebar
|
||||
activeView={activeView}
|
||||
onViewChange={handleViewChange}
|
||||
/>
|
||||
|
||||
{/* 内容区 */}
|
||||
{/* 内容区 - 独立滚动 */}
|
||||
<section className="md:col-span-9 p-8 overflow-y-auto custom-scrollbar relative">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
@@ -65,7 +65,6 @@ const DashboardPage = () => {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||
className="h-full"
|
||||
>
|
||||
{renderView()}
|
||||
</motion.div>
|
||||
@@ -75,11 +74,11 @@ const DashboardPage = () => {
|
||||
</div>
|
||||
|
||||
{/* 用户资料模态框 */}
|
||||
<ProfileModal
|
||||
isOpen={isProfileOpen}
|
||||
onClose={() => setIsProfileOpen(false)}
|
||||
<ProfileModal
|
||||
isOpen={isProfileOpen}
|
||||
onClose={() => setIsProfileOpen(false)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@ const transformToBackendFormat = (frontendData) => {
|
||||
content,
|
||||
isSelected,
|
||||
character,
|
||||
events
|
||||
events,
|
||||
regenerateContent = false
|
||||
} = frontendData;
|
||||
|
||||
// 解析内容生成标题和各部分
|
||||
@@ -191,7 +192,8 @@ const transformToBackendFormat = (frontendData) => {
|
||||
plotJson: content ? { fullContent: content } : null,
|
||||
isSelected,
|
||||
characterInfo,
|
||||
lifeEventsSummary
|
||||
lifeEventsSummary,
|
||||
regenerateContent
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -270,6 +270,39 @@ const useStore = create(
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新生命事件
|
||||
* @param {Object} event - 事件数据(必须包含 id)
|
||||
* @returns {Promise<Object>} 更新后的事件
|
||||
*/
|
||||
updateLifeEvent: async (event) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await lifeEventService.updateEvent(event);
|
||||
if (response.data) {
|
||||
const updatedEvent = lifeEventService.transformToFrontendFormat(response.data);
|
||||
set((state) => ({
|
||||
lifeEvents: state.lifeEvents.map(e =>
|
||||
e.id === updatedEvent.id ? updatedEvent : e
|
||||
),
|
||||
loading: false
|
||||
}));
|
||||
return updatedEvent;
|
||||
}
|
||||
set({ loading: false });
|
||||
return null;
|
||||
} catch (error) {
|
||||
set({ loading: false, error: error.message });
|
||||
// 降级到本地更新
|
||||
set((state) => ({
|
||||
lifeEvents: state.lifeEvents.map(e =>
|
||||
e.id === event.id ? { ...e, ...event } : e
|
||||
)
|
||||
}));
|
||||
return event;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除生命事件
|
||||
*/
|
||||
@@ -283,10 +316,8 @@ const useStore = create(
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ loading: false });
|
||||
// 降级到本地删除
|
||||
set((state) => ({
|
||||
lifeEvents: state.lifeEvents.filter(e => e.id !== id)
|
||||
}));
|
||||
console.error('删除生命事件失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -369,6 +400,39 @@ const useStore = create(
|
||||
return state.scripts.find(s => s.id === state.selectedScriptId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新剧本
|
||||
* @param {Object} script - 剧本数据(必须包含 id)
|
||||
* @returns {Promise<Object>} 更新后的剧本
|
||||
*/
|
||||
updateScript: async (script) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await epicScriptService.updateScript(script);
|
||||
if (response.data) {
|
||||
const updatedScript = epicScriptService.transformToFrontendFormat(response.data);
|
||||
set((state) => ({
|
||||
scripts: state.scripts.map(s =>
|
||||
s.id === updatedScript.id ? updatedScript : s
|
||||
),
|
||||
loading: false
|
||||
}));
|
||||
return updatedScript;
|
||||
}
|
||||
set({ loading: false });
|
||||
return null;
|
||||
} catch (error) {
|
||||
set({ loading: false, error: error.message });
|
||||
// 降级到本地更新
|
||||
set((state) => ({
|
||||
scripts: state.scripts.map(s =>
|
||||
s.id === script.id ? { ...s, ...script } : s
|
||||
)
|
||||
}));
|
||||
return script;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除剧本
|
||||
*/
|
||||
@@ -380,24 +444,16 @@ const useStore = create(
|
||||
const newScripts = state.scripts.filter(s => s.id !== id);
|
||||
return {
|
||||
scripts: newScripts,
|
||||
selectedScriptId: state.selectedScriptId === id
|
||||
? (newScripts[0]?.id || null)
|
||||
selectedScriptId: state.selectedScriptId === id
|
||||
? (newScripts[0]?.id || null)
|
||||
: state.selectedScriptId,
|
||||
loading: false
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
set({ loading: false });
|
||||
// 降级到本地删除
|
||||
set((state) => {
|
||||
const newScripts = state.scripts.filter(s => s.id !== id);
|
||||
return {
|
||||
scripts: newScripts,
|
||||
selectedScriptId: state.selectedScriptId === id
|
||||
? (newScripts[0]?.id || null)
|
||||
: state.selectedScriptId
|
||||
};
|
||||
});
|
||||
console.error('删除剧本失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -427,12 +483,12 @@ const useStore = create(
|
||||
*/
|
||||
setPath: async (pathContent, scriptId) => {
|
||||
set({ selectedPath: pathContent });
|
||||
|
||||
|
||||
if (scriptId) {
|
||||
try {
|
||||
// 检查是否已有路径
|
||||
const existingPath = await lifePathService.getPathByScriptId(scriptId).catch(() => null);
|
||||
|
||||
|
||||
if (existingPath?.data?.id) {
|
||||
// 更新
|
||||
await lifePathService.updatePath({
|
||||
@@ -453,6 +509,25 @@ const useStore = create(
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除路径
|
||||
* @param {string} scriptId - 剧本ID
|
||||
*/
|
||||
deletePath: async (scriptId) => {
|
||||
if (!scriptId) return;
|
||||
|
||||
try {
|
||||
const existingPath = await lifePathService.getPathByScriptId(scriptId).catch(() => null);
|
||||
if (existingPath?.data?.id) {
|
||||
await lifePathService.deletePath(existingPath.data.id);
|
||||
}
|
||||
set({ selectedPath: null });
|
||||
} catch {
|
||||
// 忽略错误,本地已清除
|
||||
set({ selectedPath: null });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Map, Loader2 } from 'lucide-react';
|
||||
import { Map, Loader2, Trash2 } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GlassCard, GlassButton } from '../components/ui';
|
||||
import Modal from '../components/Modal';
|
||||
import useStore from '../store/useStore';
|
||||
import { generatePath } from '../services/ai';
|
||||
|
||||
@@ -12,8 +13,9 @@ import { generatePath } from '../services/ai';
|
||||
* @param {Function} props.onGoToScript - 跳转到剧本视图回调
|
||||
*/
|
||||
const PathView = ({ onGoToScript }) => {
|
||||
const { getSelectedScript, selectedPath, setPath, loadPath, selectedScriptId } = useStore();
|
||||
const { getSelectedScript, selectedPath, setPath, loadPath, deletePath, selectedScriptId } = useStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const selectedScript = getSelectedScript();
|
||||
|
||||
@@ -44,6 +46,18 @@ const PathView = ({ onGoToScript }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理删除路径
|
||||
*/
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deletePath(selectedScriptId);
|
||||
setShowDeleteConfirm(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete path:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析路径内容为步骤数组
|
||||
*/
|
||||
@@ -82,61 +96,98 @@ const PathView = ({ onGoToScript }) => {
|
||||
}
|
||||
|
||||
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 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>
|
||||
<div className="flex gap-3">
|
||||
{selectedPath && (
|
||||
<GlassButton
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="px-4 py-3 rounded-full text-sm bg-red-400/5 text-red-300 border-red-400/20 hover:bg-red-400/10"
|
||||
title="删除路径"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</GlassButton>
|
||||
)}
|
||||
<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>
|
||||
<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" />
|
||||
规划中...
|
||||
</>
|
||||
|
||||
{/* 路径步骤展示 */}
|
||||
<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>
|
||||
))
|
||||
) : (
|
||||
selectedPath ? '重新推演' : '开启人生导航'
|
||||
<div className="py-20 text-center text-white/20 italic font-serif">
|
||||
等待开启人生导航...
|
||||
</div>
|
||||
)}
|
||||
</GlassButton>
|
||||
</div>
|
||||
</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 }}
|
||||
{/* 删除确认模态框 */}
|
||||
<Modal isOpen={showDeleteConfirm} onClose={() => setShowDeleteConfirm(false)} title="确认删除" maxWidth="sm">
|
||||
<div className="space-y-6">
|
||||
<p className="text-white/70 text-sm">
|
||||
确定要删除当前的实现路径吗?此操作不可恢复。
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<GlassButton
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
className="flex-1"
|
||||
>
|
||||
<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">
|
||||
等待开启人生导航...
|
||||
取消
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
variant="primary"
|
||||
onClick={handleDelete}
|
||||
className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20"
|
||||
>
|
||||
确认删除
|
||||
</GlassButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { UserCog, PenTool, Sparkles, BookOpen, Loader2 } from 'lucide-react';
|
||||
import { UserCog, PenTool, Sparkles, BookOpen, Loader2, Pencil, Trash2 } from 'lucide-react';
|
||||
import { GlassCard, GlassButton, GlassInput, GlassSelect } from '../components/ui';
|
||||
import Modal from '../components/Modal';
|
||||
import useStore from '../store/useStore';
|
||||
import { scriptStyles, scriptLengths } from '../utils/constants';
|
||||
|
||||
@@ -11,12 +12,14 @@ import { scriptStyles, scriptLengths } from '../utils/constants';
|
||||
* @param {Function} props.onOpenProfile - 打开用户资料模态框回调
|
||||
*/
|
||||
const ScriptView = ({ onOpenProfile }) => {
|
||||
const {
|
||||
registrationData,
|
||||
lifeEvents,
|
||||
scripts,
|
||||
selectedScriptId,
|
||||
addScript,
|
||||
const {
|
||||
registrationData,
|
||||
lifeEvents,
|
||||
scripts,
|
||||
selectedScriptId,
|
||||
addScript,
|
||||
updateScript,
|
||||
deleteScript,
|
||||
setSelectedScriptId,
|
||||
getSelectedScript,
|
||||
loadScripts
|
||||
@@ -35,6 +38,18 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
const [length, setLength] = useState(scriptLengths[0].value);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 编辑模态框状态
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingScript, setEditingScript] = useState(null);
|
||||
const [editForm, setEditForm] = useState({
|
||||
theme: '',
|
||||
style: '',
|
||||
length: ''
|
||||
});
|
||||
|
||||
// 删除确认状态
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
||||
|
||||
/**
|
||||
* 处理剧本生成
|
||||
*/
|
||||
@@ -45,17 +60,17 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
// 直接调用后端创建接口,由后端调用AI生成
|
||||
await addScript({
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
await addScript({
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
character: registrationData,
|
||||
events: lifeEvents
|
||||
});
|
||||
|
||||
|
||||
setTheme('');
|
||||
} catch (error) {
|
||||
console.error('Failed to generate script:', error);
|
||||
@@ -64,6 +79,73 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开编辑模态框
|
||||
* @param {Object} script - 要编辑的剧本
|
||||
*/
|
||||
const openEditModal = (script) => {
|
||||
setEditingScript(script);
|
||||
setEditForm({
|
||||
theme: script.theme || '',
|
||||
style: script.style || scriptStyles[0].value,
|
||||
length: script.length || scriptLengths[0].value
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭编辑模态框
|
||||
*/
|
||||
const closeEditModal = () => {
|
||||
setIsEditModalOpen(false);
|
||||
setEditingScript(null);
|
||||
setEditForm({ theme: '', style: '', length: '' });
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理编辑提交
|
||||
*/
|
||||
const handleEditSubmit = async () => {
|
||||
if (!editForm.theme) {
|
||||
alert('请输入主题');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await updateScript({
|
||||
id: editingScript.id,
|
||||
theme: editForm.theme,
|
||||
style: editForm.style,
|
||||
length: editForm.length,
|
||||
character: registrationData,
|
||||
events: lifeEvents,
|
||||
regenerateContent: true // 标记需要重新生成AI内容
|
||||
});
|
||||
closeEditModal();
|
||||
} catch (error) {
|
||||
console.error('Failed to update script:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理删除确认
|
||||
* @param {string} id - 剧本ID
|
||||
*/
|
||||
const handleDeleteConfirm = async (id) => {
|
||||
try {
|
||||
await deleteScript(id);
|
||||
setDeleteConfirmId(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete script:', error);
|
||||
alert('删除失败,请稍后重试');
|
||||
setDeleteConfirmId(null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化剧本内容,高亮【标题】
|
||||
*/
|
||||
@@ -165,16 +247,35 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
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
|
||||
p-3 glass-card text-left cursor-pointer hover:bg-white/5 border-white/5 transition-all relative group
|
||||
${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 className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); openEditModal(script); }}
|
||||
className="p-1.5 rounded-full bg-white/5 hover:bg-orange-200/10 text-white/30 hover:text-orange-200 transition-all"
|
||||
title="编辑"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setDeleteConfirmId(script.id); }}
|
||||
className="p-1.5 rounded-full bg-white/5 hover:bg-red-400/10 text-white/30 hover:text-red-400 transition-all"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div onClick={() => setSelectedScriptId(script.id)}>
|
||||
<div className="text-[11px] text-white/80 truncate pr-12">{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>
|
||||
</div>
|
||||
))
|
||||
@@ -189,9 +290,18 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
<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">
|
||||
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in relative group" padding="lg">
|
||||
{/* 编辑按钮 */}
|
||||
<button
|
||||
onClick={() => openEditModal(selectedScript)}
|
||||
className="absolute top-4 right-4 p-2 rounded-full bg-white/5 hover:bg-orange-200/10 text-white/30 hover:text-orange-200 transition-all opacity-0 group-hover:opacity-100"
|
||||
title="修改剧本"
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<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 className="flex justify-between items-center mb-8 pb-4 border-b border-white/5 pr-10">
|
||||
<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">
|
||||
@@ -200,7 +310,7 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
</div>
|
||||
<BookOpen className="text-white/20" />
|
||||
</div>
|
||||
<div
|
||||
<div
|
||||
className="text-white/70 leading-loose whitespace-pre-wrap space-y-6 text-sm"
|
||||
dangerouslySetInnerHTML={{ __html: formatScriptContent(selectedScript.content) }}
|
||||
/>
|
||||
@@ -214,6 +324,62 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 编辑剧本模态框 */}
|
||||
<Modal isOpen={isEditModalOpen} onClose={closeEditModal} title="修改剧本">
|
||||
<div className="space-y-6">
|
||||
<GlassInput
|
||||
label="剧本主题"
|
||||
placeholder="例如:我在职场逆袭了"
|
||||
value={editForm.theme}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, theme: v }))}
|
||||
/>
|
||||
<GlassSelect
|
||||
label="叙事风格"
|
||||
options={scriptStyles}
|
||||
value={editForm.style}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, style: v }))}
|
||||
/>
|
||||
<GlassSelect
|
||||
label="剧本篇幅"
|
||||
options={scriptLengths}
|
||||
value={editForm.length}
|
||||
onChange={(v) => setEditForm(prev => ({ ...prev, length: v }))}
|
||||
/>
|
||||
<GlassButton
|
||||
variant="primary"
|
||||
onClick={handleEditSubmit}
|
||||
loading={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? '正在重新编撰...' : '重新生成剧本'}
|
||||
</GlassButton>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认模态框 */}
|
||||
<Modal isOpen={!!deleteConfirmId} onClose={() => setDeleteConfirmId(null)} title="确认删除" maxWidth="sm">
|
||||
<div className="space-y-6">
|
||||
<p className="text-white/70 text-sm">
|
||||
确定要删除这个剧本吗?此操作不可恢复,关联的实现路径也将被删除。
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Wind, Sparkles } from 'lucide-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';
|
||||
@@ -79,7 +79,7 @@ const FeedbackContent = ({ content }) => {
|
||||
* 人生轨迹视图,显示和管理生命事件
|
||||
*/
|
||||
const TimelineView = () => {
|
||||
const { lifeEvents, addLifeEvent, loadLifeEvents } = useStore();
|
||||
const { lifeEvents, addLifeEvent, updateLifeEvent, deleteLifeEvent, loadLifeEvents } = useStore();
|
||||
|
||||
// 加载生命事件
|
||||
useEffect(() => {
|
||||
@@ -87,11 +87,17 @@ const TimelineView = () => {
|
||||
// 后端不可用时忽略错误
|
||||
});
|
||||
}, [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: '',
|
||||
@@ -100,7 +106,39 @@ const TimelineView = () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理表单提交
|
||||
* 打开新增模态框
|
||||
*/
|
||||
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) {
|
||||
@@ -109,23 +147,45 @@ const TimelineView = () => {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
// 直接调用后端添加事件,由后端调用AI进行疗愈分析
|
||||
await addLifeEvent({
|
||||
...eventForm
|
||||
});
|
||||
|
||||
if (editingEventId) {
|
||||
// 编辑模式:调用更新接口
|
||||
await updateLifeEvent({
|
||||
id: editingEventId,
|
||||
...eventForm
|
||||
});
|
||||
} else {
|
||||
// 新增模式:调用添加接口
|
||||
await addLifeEvent({
|
||||
...eventForm
|
||||
});
|
||||
}
|
||||
|
||||
// 重置表单并关闭模态框
|
||||
setEventForm({ title: '', time: '', content: '' });
|
||||
setIsModalOpen(false);
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error('Failed to add event:', 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 按事件时间倒序排列(最新的在最上面)
|
||||
* 空日期的事件排在最后
|
||||
@@ -149,7 +209,7 @@ const TimelineView = () => {
|
||||
<p className="text-sm text-white/30 mt-2">塑造你的每一刻,都被星辰见证。</p>
|
||||
</div>
|
||||
<GlassButton
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
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" /> 记录足迹
|
||||
@@ -159,17 +219,35 @@ const TimelineView = () => {
|
||||
{/* 时间线容器 */}
|
||||
<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">
|
||||
<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 && (
|
||||
@@ -185,7 +263,7 @@ const TimelineView = () => {
|
||||
<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">
|
||||
@@ -211,8 +289,8 @@ const TimelineView = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 添加事件模态框 */}
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} title="记录足迹">
|
||||
{/* 添加/编辑事件模态框 */}
|
||||
<Modal isOpen={isModalOpen} onClose={closeModal} title={editingEventId ? '修改足迹' : '记录足迹'}>
|
||||
<div className="space-y-6">
|
||||
<GlassInput
|
||||
label="事件标题"
|
||||
@@ -243,6 +321,30 @@ const TimelineView = () => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user