Files
deerflow-code/web-code/pages/ResultDetail.tsx
T
2026-05-27 19:43:15 +08:00

544 lines
26 KiB
TypeScript

import React, { useState } from 'react';
import {
FileText,
ArrowLeft,
ArrowRight,
Download,
Edit3,
Send,
CheckCircle,
Clock,
Bot,
Target,
Users,
TrendingUp,
MessageSquare,
Megaphone,
Shield,
ChevronDown,
ChevronRight,
RefreshCw,
MoreHorizontal,
Share2,
Printer,
History,
AlertCircle,
Check,
X,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
interface ResultDetailProps {
taskId?: string;
onNavigate: (page: PageType, taskId?: string) => void;
onBack?: () => void;
}
// 模拟任务详情数据
const mockTaskDetail = {
id: 'task-001',
title: '五四青年节宣传活动策略',
category: '规划任务',
subCategory: '叙事策略',
status: 'completed',
createdAt: '2026-03-19 10:30',
updatedAt: '2026-03-19 15:30',
completedAt: '2026-03-19 15:30',
reportType: 'strategy',
agentCount: 6,
description: '针对五四青年节的品牌传播策略方案',
scene: '宣传策略生成',
duration: '5小时32分',
conclusion: {
main: '五四青年节宣传活动应聚焦"真实、有态度的青年故事"这一叙事方向,以18-25岁基层青年为核心受众,优先在短视频平台建立品牌认知。',
recommended: [
'以真实人物故事为核心叙事内容',
'采用平等对话而非说教式的传播语气',
'优先布局抖音、小红书等短视频平台',
'建立UGC激励机制促进用户参与',
],
notRecommended: [
'过度包装的精致化内容',
'单向输出的宣传式传播',
'纯文字为主的长图文形式',
],
pendingIssues: [
'叙事角度:事件叙事 vs 人物叙事(待进一步验证)',
'渠道策略:短视频为主 vs 社交媒体为主',
'预算在内容制作和达人投放的分配比例',
],
},
strategyChain: [
{ id: 1, name: '目标', icon: Target, color: 'blue', status: 'completed', result: '提升品牌在Z世代用户中的认知度' },
{ id: 2, name: '受众', icon: Users, color: 'cyan', status: 'completed', result: '18-25岁基层青年' },
{ id: 3, name: '行为', icon: TrendingUp, color: 'green', status: 'completed', result: '主动参与话题讨论、生成UGC内容' },
{ id: 4, name: '驱动', icon: MessageSquare, color: 'yellow', status: 'completed', result: '社交认同与自我表达' },
{ id: 5, name: '叙事', icon: Megaphone, color: 'purple', status: 'completed', result: '真实、有态度的青年故事' },
{ id: 6, name: '风险', icon: Shield, color: 'red', status: 'completed', result: 'UGC质量不稳定、话题热度维持' },
],
versionHistory: [
{ version: 'V3', time: '2026-03-19 15:30', changes: ['叙事由"事件主导"改为"人物+事件结合"', '风险项已修正'] },
{ version: 'V2', time: '2026-03-19 14:20', changes: ['受众由"全体青年"收窄为"基层青年"', '目标行为已明确'] },
{ version: 'V1', time: '2026-03-19 10:30', changes: ['初始版本生成'] },
],
agentOpinions: [
{ agentName: 'GoalAgent', role: '目标分析专家', opinion: '建议聚焦提升品牌认知度这一核心目标,短期内不追求直接转化', adopted: true, color: 'blue' },
{ agentName: 'AudienceAgent', role: '受众分析专家', opinion: '核心受众应为18-25岁基层青年群体,注重"真实感"和"态度感"', adopted: true, color: 'cyan' },
{ agentName: 'NarrativeAgent', role: '叙事策略专家', opinion: '应采用"真实、有态度的青年故事"叙事框架', adopted: true, color: 'purple' },
{ agentName: 'RiskAgent', role: '风险评估专家', opinion: '需注意UGC质量不稳定和话题热度维持的风险', adopted: true, color: 'red' },
],
disputes: [
{
issue: '叙事角度选择',
description: '人物叙事 vs 事件叙事',
viewpoints: [
{ agent: 'NarrativeAgent', opinion: '人物叙事更有情感穿透力' },
{ agent: 'GoalAgent', opinion: '事件叙事更聚焦目标达成' },
],
status: '待确认',
},
],
};
const ResultDetail: React.FC<ResultDetailProps> = ({ taskId, onNavigate, onBack }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 主题颜色类
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-gray-200';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const [activeTab, setActiveTab] = useState<'conclusion' | 'process' | 'opinions' | 'history'>('conclusion');
const [expandedSection, setExpandedSection] = useState<string | null>('goal');
const [showReportMenu, setShowReportMenu] = useState(false);
const task = mockTaskDetail;
const toggleSection = (section: string) => {
setExpandedSection(expandedSection === section ? null : section);
};
const getColorClasses = (color: string) => {
const colors: Record<string, { bg: string; text: string }> = {
blue: { bg: isDark ? 'bg-blue-500/20' : 'bg-blue-50', text: isDark ? 'text-blue-400' : 'text-blue-600' },
cyan: { bg: isDark ? 'bg-cyan-500/20' : 'bg-cyan-50', text: isDark ? 'text-cyan-400' : 'text-cyan-600' },
green: { bg: isDark ? 'bg-green-500/20' : 'bg-green-50', text: isDark ? 'text-green-400' : 'text-green-600' },
yellow: { bg: isDark ? 'bg-yellow-500/20' : 'bg-yellow-50', text: isDark ? 'text-yellow-400' : 'text-yellow-600' },
purple: { bg: isDark ? 'bg-purple-500/20' : 'bg-purple-50', text: isDark ? 'text-purple-400' : 'text-purple-600' },
red: { bg: isDark ? 'bg-red-500/20' : 'bg-red-50', text: isDark ? 'text-red-400' : 'text-red-600' },
};
return colors[color] || colors.blue;
};
const handleBack = () => {
if (onBack) {
onBack();
} else {
onNavigate('result-management');
}
};
return (
<div className="space-y-6">
{/* 顶部信息栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<button
onClick={handleBack}
className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
>
<ArrowLeft className="w-4 h-4" />
返回
</button>
<div className={`h-8 w-px ${isDark ? 'bg-blue-800/30' : 'bg-gray-200'}`} />
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center`}>
<FileText className="w-5 h-5 text-blue-400" />
</div>
<div>
<h1 className={`${textPrimary} font-bold text-lg`}>{task.title}</h1>
<p className={`${textMuted} text-sm`}>{task.subCategory} · {task.status === 'completed' ? '已完成' : '进行中'}</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Edit3 className="w-4 h-4" />
编辑
</button>
<button className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<History className="w-4 h-4" />
历史版本
</button>
<button className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Download className="w-4 h-4" />
导出报告
</button>
<div className="relative">
<button
onClick={() => setShowReportMenu(!showReportMenu)}
className={`flex items-center gap-2 px-4 py-2 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}
>
<Send className="w-4 h-4" />
呈报
<ChevronDown className="w-4 h-4" />
</button>
{showReportMenu && (
<div className={`absolute right-0 top-full mt-2 w-48 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl shadow-xl py-2 z-10`}>
<button className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm ${textPrimary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-50'} transition-colors`}>
<Share2 className="w-4 h-4" />
分享链接
</button>
<button className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm ${textPrimary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-50'} transition-colors`}>
<Printer className="w-4 h-4" />
打印
</button>
<button className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm ${textPrimary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-50'} transition-colors`}>
<Send className="w-4 h-4" />
发送至邮箱
</button>
</div>
)}
</div>
</div>
</div>
{/* 基本信息 */}
<div className={`grid grid-cols-5 gap-4 p-4 ${inputBg} rounded-lg`}>
<div className="text-center">
<p className={`${textMuted} text-xs mb-1`}>使用场景</p>
<p className={`${textPrimary} font-medium text-sm`}>{task.scene}</p>
</div>
<div className="text-center">
<p className={`${textMuted} text-xs mb-1`}>运行时长</p>
<p className={`${textPrimary} font-medium text-sm`}>{task.duration}</p>
</div>
<div className="text-center">
<p className={`${textMuted} text-xs mb-1`}>参与智能体</p>
<p className={`${textPrimary} font-medium text-sm`}>{task.agentCount}个</p>
</div>
<div className="text-center">
<p className={`${textMuted} text-xs mb-1`}>当前版本</p>
<p className={`${isDark ? 'text-cyan-400' : 'text-cyan-600'} font-medium text-sm`}>V{task.versionHistory.length}</p>
</div>
<div className="text-center">
<p className={`${textMuted} text-xs mb-1`}>完成时间</p>
<p className={`${textPrimary} font-medium text-sm`}>{task.completedAt}</p>
</div>
</div>
</div>
{/* Tab切换 */}
<div className={`flex items-center gap-1 ${cardBg} p-1 rounded-lg w-fit`}>
<button
onClick={() => setActiveTab('conclusion')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'conclusion'
? `${btnPrimary} text-white`
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
}`}
>
会商结论
</button>
<button
onClick={() => setActiveTab('process')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'process'
? `${btnPrimary} text-white`
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
}`}
>
策略链
</button>
<button
onClick={() => setActiveTab('opinions')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'opinions'
? `${btnPrimary} text-white`
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
}`}
>
观点汇总
</button>
<button
onClick={() => setActiveTab('history')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'history'
? `${btnPrimary} text-white`
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
}`}
>
版本历史
</button>
</div>
{/* 内容区 */}
{activeTab === 'conclusion' && (
<div className="grid grid-cols-12 gap-6">
{/* 会商结论 */}
<div className="col-span-8 space-y-6">
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
<CheckCircle className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
本次会商结论
</h3>
<div className={`p-4 ${isDark ? 'bg-green-500/10 border-green-500/30' : 'bg-green-50 border-green-200'} rounded-lg`}>
<p className={`${isDark ? 'text-green-300' : 'text-green-800'} leading-relaxed`}>{task.conclusion.main}</p>
</div>
</div>
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
<Check className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
推荐采用方向
</h3>
<div className="space-y-2">
{task.conclusion.recommended.map((item, index) => (
<div key={index} className={`flex items-center gap-3 p-3 ${isDark ? 'bg-green-500/10 border-green-500/30' : 'bg-green-50 border-green-200'} rounded-lg`}>
<div className={`w-6 h-6 rounded-full ${isDark ? 'bg-green-500/20' : 'bg-green-100'} flex items-center justify-center`}>
<Check className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
</div>
<p className={`${isDark ? 'text-green-300' : 'text-green-800'} text-sm`}>{item}</p>
</div>
))}
</div>
</div>
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
<X className={`w-4 h-4 ${isDark ? 'text-red-400' : 'text-red-600'}`} />
不建议采用方向
</h3>
<div className="space-y-2">
{task.conclusion.notRecommended.map((item, index) => (
<div key={index} className={`flex items-center gap-3 p-3 ${isDark ? 'bg-red-500/10 border-red-500/30' : 'bg-red-50 border-red-200'} rounded-lg`}>
<div className={`w-6 h-6 rounded-full ${isDark ? 'bg-red-500/20' : 'bg-red-100'} flex items-center justify-center`}>
<X className={`w-4 h-4 ${isDark ? 'text-red-400' : 'text-red-600'}`} />
</div>
<p className={`${isDark ? 'text-red-300' : 'text-red-800'} text-sm`}>{item}</p>
</div>
))}
</div>
</div>
</div>
{/* 侧边栏 */}
<div className="col-span-4 space-y-6">
<div className={`${cardBg} backdrop-blur-xl ${isDark ? 'border-yellow-800/30' : 'border-yellow-200'} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
<AlertCircle className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`} />
待确认问题
</h3>
<div className="space-y-2">
{task.conclusion.pendingIssues.map((item, index) => (
<div key={index} className={`p-3 ${isDark ? 'bg-yellow-500/10 border-yellow-500/30' : 'bg-yellow-50 border-yellow-200'} rounded-lg`}>
<p className={`${isDark ? 'text-yellow-300' : 'text-yellow-800'} text-sm`}>{item}</p>
</div>
))}
</div>
</div>
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4`}>操作</h3>
<div className="space-y-2">
<button className={`w-full flex items-center gap-3 px-4 py-3 ${btnSecondary} rounded-xl text-sm transition-colors`}>
<RefreshCw className="w-4 h-4" />
重新发起会商
</button>
<button className={`w-full flex items-center gap-3 px-4 py-3 ${btnPrimary} text-white rounded-xl text-sm transition-colors`}>
<FileText className="w-4 h-4" />
生成最终方案
</button>
</div>
</div>
</div>
</div>
)}
{activeTab === 'process' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
{/* 策略链可视化 */}
<h3 className={`${textPrimary} font-semibold text-sm mb-4`}>策略链结果</h3>
<div className="flex items-center justify-between mb-6">
{task.strategyChain.map((item, index) => {
const Icon = item.icon;
const colors = getColorClasses(item.color);
return (
<React.Fragment key={item.id}>
<div className="flex flex-col items-center">
<div className={`w-14 h-14 rounded-xl bg-${item.color}-500/20 border-2 border-${item.color}-500/50 flex items-center justify-center mb-2`}>
<Icon className={`w-7 h-7 text-${item.color}-400`} />
</div>
<span className={`text-${item.color}-400 text-sm font-medium`}>{item.name}</span>
<span className={`${textMuted} text-xs mt-1 flex items-center gap-1`}>
<CheckCircle className="w-3 h-3 text-green-400" /> 已确认
</span>
</div>
{index < task.strategyChain.length - 1 && (
<ArrowRight className="w-6 h-6 text-slate-600 mb-6" />
)}
</React.Fragment>
);
})}
</div>
{/* 详细结果可折叠展开 */}
<div className={`${inputBg} rounded-xl overflow-hidden border ${cardBorder}`}>
{task.strategyChain.map((section) => {
const Icon = section.icon;
const colors = getColorClasses(section.color);
const isExpanded = expandedSection === section.name;
return (
<div key={section.id} className={`border-b last:border-0 ${cardBorder}`}>
<button
onClick={() => toggleSection(section.name)}
className="w-full flex items-center justify-between p-4 hover:bg-slate-700/20 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg bg-${section.color}-500/20 flex items-center justify-center`}>
<Icon className={`w-5 h-5 text-${section.color}-400`} />
</div>
<div className="text-left">
<p className={`${textPrimary} font-medium`}>{section.name}</p>
<p className={`text-${section.color}-400 text-sm`}>{section.result}</p>
</div>
</div>
{isExpanded ? (
<ChevronDown className={`w-5 h-5 ${textSecondary}`} />
) : (
<ChevronRight className={`w-5 h-5 ${textSecondary}`} />
)}
</button>
{isExpanded && (
<div className="px-4 pb-4 pt-0 ml-13">
<div className={`p-4 ${cardBg} rounded-lg`}>
<p className={`${textSecondary} text-sm`}>详细分析内容将在此处展示,包含智能体推理过程和依据。</p>
</div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
{activeTab === 'opinions' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
<Bot className={`w-4 h-4 ${isDark ? 'text-cyan-400' : 'text-cyan-600'}`} />
各智能体观点汇总
</h3>
<div className="space-y-4">
{task.agentOpinions.map((agent) => {
const colors = getColorClasses(agent.color);
return (
<div key={agent.agentName} className={`p-4 ${inputBg} ${cardBorder} rounded-xl`}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg ${colors.bg} flex items-center justify-center`}>
<Bot className={`w-5 h-5 ${colors.text}`} />
</div>
<div>
<div className="flex items-center gap-2">
<p className={`text-sm font-medium ${colors.text}`}>{agent.agentName}</p>
{agent.adopted ? (
<span className={`px-1.5 py-0.5 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700'} rounded text-xs`}>
已采纳
</span>
) : (
<span className={`px-1.5 py-0.5 ${isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-200 text-gray-600'} rounded text-xs`}>
未采纳
</span>
)}
</div>
<p className={`${textMuted} text-xs`}>{agent.role}</p>
</div>
</div>
</div>
<div className="pl-13">
<p className={`${textSecondary} text-sm`}>{agent.opinion}</p>
</div>
</div>
);
})}
{/* 分歧点 */}
{task.disputes.length > 0 && (
<div className={`p-4 ${isDark ? 'bg-yellow-500/10 border-yellow-500/30' : 'bg-yellow-50 border-yellow-200'} rounded-xl`}>
<h4 className={`${textPrimary} font-medium text-sm mb-3 flex items-center gap-2`}>
<AlertCircle className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`} />
争议点
</h4>
{task.disputes.map((dispute, idx) => (
<div key={idx} className="space-y-2">
<p className={`${textPrimary} text-sm font-medium`}>{dispute.issue}</p>
<p className={`${textMuted} text-xs`}>{dispute.description}</p>
{dispute.viewpoints.map((vp, i) => (
<div key={i} className={`p-2 ${isDark ? 'bg-yellow-500/5' : 'bg-yellow-50'} rounded-lg`}>
<span className={`${textMuted} text-xs mr-2`}>{vp.agent}:</span>
<span className={`${isDark ? 'text-yellow-300' : 'text-yellow-800'} text-xs`}>{vp.opinion}</span>
</div>
))}
</div>
))}
</div>
)}
</div>
</div>
)}
{activeTab === 'history' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<div className="flex items-center justify-between mb-4">
<h3 className={`${textPrimary} font-semibold text-sm`}>版本迭代记录</h3>
<button className="flex items-center gap-1 text-blue-400 text-sm hover:text-blue-300 transition-colors">
<RefreshCw className="w-4 h-4" />
对比
</button>
</div>
<div className="space-y-3">
{task.versionHistory.map((version, index) => (
<div
key={version.version}
className={`p-4 rounded-xl border ${
index === 0
? isDark ? 'bg-cyan-500/10 border-cyan-500/30' : 'bg-cyan-50 border-cyan-200'
: `${inputBg} border-slate-700/30`
}`}
>
<div className="flex items-center justify-between mb-2">
<span className={`font-bold ${
index === 0 ? (isDark ? 'text-cyan-400' : 'text-cyan-600') : textSecondary
}`}>
{version.version}
</span>
<span className={`${textMuted} text-xs`}>{version.time}</span>
</div>
<ul className="space-y-1">
{version.changes.map((change, i) => (
<li key={i} className={`${textPrimary} text-sm flex items-start gap-2`}>
<span className={textSecondary}>•</span>
{change}
</li>
))}
</ul>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default ResultDetail;