Files
happy-life-star/course-web/src/components/views/PathView.jsx
T
2025-12-21 17:44:59 +08:00

359 lines
14 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 React, { useState, useEffect } from 'react';
import { useStoreData } from '../../hooks/useStoreData';
import { Store } from '../../utils/store';
import { AI } from '../../utils/aiLogic';
import {
Map,
Ghost,
Wand2,
RefreshCw,
MapPin,
ChevronDown,
Target,
Activity,
Package,
Repeat,
Edit3,
Trash2,
Check,
ArrowRight
} from 'lucide-react';
import { Button } from '../ui/Button';
import { GlassCard } from '../ui/GlassCard';
import { Select } from '../ui/Input';
export function PathView({ onSwitchToScript }) {
const data = useStoreData();
const [selectedScriptId, setSelectedScriptId] = useState('');
const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [expandedStep, setExpandedStep] = useState(null);
const [editedPath, setEditedPath] = useState(null);
// Initialize selectedScriptId when scripts are available
useEffect(() => {
if (data.generatedScripts.length > 0 && !selectedScriptId) {
setSelectedScriptId(data.generatedScripts[0].id);
}
}, [data.generatedScripts, selectedScriptId]);
// Find current path based on selected script
const currentPath = data.paths?.find(p => p.scriptId === selectedScriptId);
// Update local edited state when path changes
useEffect(() => {
if (currentPath) {
setEditedPath(JSON.parse(JSON.stringify(currentPath))); // Deep copy
} else {
setEditedPath(null);
}
}, [currentPath]);
const handleGenerate = async () => {
if (currentPath && !window.confirm('重新生成将覆盖现有路径规划,确定吗?')) return;
const script = data.generatedScripts.find(s => s.id === selectedScriptId);
if (!script) return;
setLoading(true);
try {
const newPath = await AI.generatePath(script, data.userProfile);
Store.addPath(newPath);
setIsEditing(false);
} catch (e) {
console.error(e);
alert('规划失败,请重试');
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (window.confirm('确定要删除这个路径规划吗?')) {
Store.deletePath(currentPath.id);
// Sync to backend
try {
const allPaths = Store.get().paths;
const profileRes = await userApi.getCurrentUser();
if (profileRes.data) {
await userApi.updateUserProfile({
id: profileRes.data.id,
paths: JSON.stringify(allPaths)
});
}
} catch (err) {
console.error("Failed to sync path deletion to backend", err);
}
setIsEditing(false);
}
};
const handleSaveEdit = async () => {
if (editedPath) {
Store.updatePath(editedPath.id, { steps: editedPath.steps });
// Sync to backend
try {
const allPaths = Store.get().paths;
const profileRes = await userApi.getCurrentUser();
if (profileRes.data) {
await userApi.updateUserProfile({
id: profileRes.data.id,
paths: JSON.stringify(allPaths)
});
}
} catch (err) {
console.error("Failed to sync path update to backend", err);
}
setIsEditing(false);
}
};
const handleStepEdit = (stepIndex, field, value) => {
if (!editedPath) return;
const newSteps = [...editedPath.steps];
newSteps[stepIndex] = { ...newSteps[stepIndex], [field]: value };
setEditedPath({ ...editedPath, steps: newSteps });
};
if (data.generatedScripts.length === 0) {
return (
<div className="max-w-4xl mx-auto md:p-12 pb-24">
<header className="mb-8">
<h2 className="text-3xl font-bold text-white mb-2 flex items-center gap-2">
<span className="bg-green-100/10 p-2 rounded-lg text-aurora-green">
<Map className="w-6 h-6 icon-glow" />
</span>
实现路径
</h2>
<p className="text-gray-400">将幻想落地为行动AI为你定制专属计划</p>
</header>
<div className="p-16 text-center text-gray-400 glass-card rounded-2xl border-dashed border-2 border-white/10">
<Ghost className="w-16 h-16 mx-auto mb-4 opacity-30" />
<p className="mb-4">你需要先有一个剧本才能生成通往它的路径</p>
<button
className="text-primary font-bold hover:underline flex items-center justify-center gap-1 mx-auto"
onClick={onSwitchToScript}
>
去生成剧本 <ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto md:p-12 pb-24">
<header className="mb-8">
<h2 className="text-3xl font-bold text-white mb-2 flex items-center gap-2">
<span className="bg-green-100/10 p-2 rounded-lg text-aurora-green">
<Map className="w-6 h-6 icon-glow" />
</span>
实现路径
</h2>
<p className="text-gray-400">将幻想落地为行动AI为你定制专属计划</p>
</header>
<GlassCard className="mb-8 p-6 rounded-2xl border-l-4 border-accent flex flex-col md:flex-row items-center gap-4 shadow-sm">
<div className="flex-1 w-full">
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">
选择目标剧本
</label>
<div className="relative">
<Select
value={selectedScriptId}
onChange={(e) => setSelectedScriptId(e.target.value)}
className="w-full"
>
{data.generatedScripts.map(s => (
<option key={s.id} value={s.id}>{s.title}</option>
))}
</Select>
</div>
</div>
<Button
onClick={handleGenerate}
isLoading={loading}
className="w-full md:w-auto mt-4 md:mt-0"
>
{currentPath ? (
<>
<RefreshCw className="w-4 h-4 mr-2" /> 重新生成
</>
) : (
<>
<Wand2 className="w-4 h-4 mr-2" /> 生成路径
</>
)}
</Button>
</GlassCard>
<div className="transition-all duration-300 min-h-[300px]">
{!currentPath ? (
<div className="text-center py-20 opacity-50">
<div className="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 border border-white/10">
<MapPin className="w-8 h-8 text-gray-400" />
</div>
<p className="text-gray-400">尚未生成路径点击上方按钮开始规划</p>
</div>
) : (
<>
<div className="flex justify-between items-center mb-6 animate-fade-in">
<div className="text-sm text-gray-400">
规划生成于 {new Date(currentPath.createdAt).toLocaleDateString()}
</div>
<div className="flex gap-2">
<button
onClick={isEditing ? handleSaveEdit : () => setIsEditing(true)}
className={`py-1.5 px-4 text-xs flex items-center gap-2 rounded-lg border transition-colors ${
isEditing
? 'bg-primary text-white border-primary hover:bg-primary/90'
: 'border-white/20 text-gray-400 hover:text-primary hover:border-primary bg-transparent'
}`}
>
{isEditing ? (
<><Check className="w-3 h-3" /> 保存</>
) : (
<><Edit3 className="w-3 h-3" /> 编辑</>
)}
</button>
<button
onClick={handleDelete}
className="py-1.5 px-4 text-xs flex items-center gap-2 rounded-lg border border-red-500/30 text-red-400 hover:bg-red-500/10 hover:border-red-400 bg-transparent"
>
<Trash2 className="w-3 h-3" /> 删除
</button>
</div>
</div>
<div className="space-y-6 pb-12">
{(isEditing ? editedPath.steps : currentPath.steps).map((step, idx) => (
<GlassCard
key={idx}
className="relative p-0 overflow-hidden group hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
{/* Progress Line */}
<div className="absolute left-8 top-0 bottom-0 w-0.5 bg-white/5 z-0"></div>
{/* Header */}
<div
className="relative z-10 p-6 cursor-pointer flex items-center justify-between"
onClick={() => setExpandedStep(expandedStep === idx ? null : idx)}
>
<div className="flex items-center gap-5">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-green-400 text-white flex items-center justify-center font-bold text-lg shadow-lg shadow-primary/30 shrink-0">
{idx + 1}
</div>
<div>
<h4 className="font-bold text-lg text-white">{step.phase}</h4>
<span className="text-xs text-primary font-medium bg-primary/10 px-2 py-0.5 rounded-full border border-primary/20">
{step.time}
</span>
</div>
</div>
<ChevronDown
className={`w-5 h-5 text-gray-400 transition-transform duration-300 ${
expandedStep === idx ? 'rotate-180' : ''
}`}
/>
</div>
{/* Details (Collapsible) */}
<div
className={`relative z-10 px-6 pb-6 pt-0 border-t border-white/5 pl-[4.5rem] transition-all duration-300 overflow-hidden ${
expandedStep === idx ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0 py-0'
}`}
>
<div className="space-y-5 pt-4">
{/* Core Content */}
<div>
<h5 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 flex items-center gap-1">
<Target className="w-3 h-3" /> 核心策略
</h5>
<div
contentEditable={isEditing}
onBlur={(e) => handleStepEdit(idx, 'content', e.currentTarget.textContent)}
className={`p-3 rounded-lg text-sm text-gray-300 leading-relaxed border transition-colors ${
isEditing
? 'bg-white/10 border-primary/50 ring-2 ring-primary/20'
: 'bg-white/5 border-white/5'
}`}
>
{step.content}
</div>
</div>
{/* Actions */}
<div>
<h5 className="text-xs font-bold text-blue-400 uppercase tracking-wider mb-2 flex items-center gap-1">
<Activity className="w-3 h-3" /> 关键行动
</h5>
<div
contentEditable={isEditing}
onBlur={(e) => handleStepEdit(idx, 'action', e.currentTarget.textContent)}
className={`p-3 rounded-lg text-sm text-gray-300 transition-colors ${
isEditing
? 'bg-white/10 border border-primary/50 ring-2 ring-primary/20'
: 'bg-blue-500/10 border border-blue-500/20'
}`}
>
{step.action}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Resources */}
<div>
<h5 className="text-xs font-bold text-amber-400 uppercase tracking-wider mb-2 flex items-center gap-1">
<Package className="w-3 h-3" /> 所需资源
</h5>
<div
contentEditable={isEditing}
onBlur={(e) => handleStepEdit(idx, 'resources', e.currentTarget.textContent)}
className={`p-3 rounded-lg text-xs text-gray-400 leading-relaxed transition-colors ${
isEditing
? 'bg-white/10 border border-primary/50 ring-2 ring-primary/20'
: 'bg-amber-500/10 border border-amber-500/20'
}`}
>
{step.resources}
</div>
</div>
{/* Habits */}
<div>
<h5 className="text-xs font-bold text-green-400 uppercase tracking-wider mb-2 flex items-center gap-1">
<Repeat className="w-3 h-3" /> 养成习惯
</h5>
<div
contentEditable={isEditing}
onBlur={(e) => handleStepEdit(idx, 'habit', e.currentTarget.textContent)}
className={`p-3 rounded-lg text-xs text-gray-400 leading-relaxed transition-colors ${
isEditing
? 'bg-white/10 border border-primary/50 ring-2 ring-primary/20'
: 'bg-green-500/10 border border-green-500/20'
}`}
>
{step.habit}
</div>
</div>
</div>
</div>
</div>
</GlassCard>
))}
</div>
<div className="text-center text-gray-500 text-xs italic mt-8">
"路虽远,行则将至。"
</div>
</>
)}
</div>
</div>
);
}