代码修改

This commit is contained in:
fengxiangdebaozi committed 2026-05-27 19:43:15 +08:00
1 parent 8f5af46ec2
commit 8f75e12871
168 files changed
+76575

No files matched your search

+877
View File
@@ -0,0 +1,877 @@
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
ArrowLeft,
Save,
RotateCcw,
Power,
Trash2,
Copy,
ChevronDown,
ChevronRight,
Bot,
Target,
Brain,
Database,
Box,
Route,
FileOutput,
Sparkles,
Lightbulb,
MessageSquare,
Play,
Send,
Plus,
X,
Check,
AlertCircle,
Wrench,
Layers,
Zap,
Clock,
} from 'lucide-react';
interface AgentDetailProps {
agentId: string | null;
onBack: () => void;
}
interface ExpandedSection {
basic: boolean;
responsibility: boolean;
skills: boolean;
datasource: boolean;
function: boolean;
thinking: boolean;
output: boolean;
}
const AgentDetail: React.FC<AgentDetailProps> = ({ agentId, 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-red-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 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 btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
const [isEnabled, setIsEnabled] = useState(true);
const [expandedSections, setExpandedSections] = useState<ExpandedSection>({
basic: true,
responsibility: true,
skills: true,
datasource: true,
function: true,
thinking: true,
output: true,
});
const [chatMessages, setChatMessages] = useState<{role: 'user' | 'agent'; content: string}[]>([
{ role: 'agent', content: '核心受众应聚焦18-25岁基层青年群体,原因是该群体社交媒体活跃度高、注重性价比、偏好真实有态度的品牌叙事。' }
]);
const [chatInput, setChatInput] = useState('');
const [aiSuggestion, setAiSuggestion] = useState('');
const [showGeneratedConfig, setShowGeneratedConfig] = useState(false);
const toggleSection = (section: keyof ExpandedSection) => {
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
};
const skills = [
{ name: '受众分层', type: '分析类', enabled: true, trigger: '自动', priority: 1 },
{ name: '核心受众识别', type: '推断类', enabled: true, trigger: '自动', priority: 1 },
{ name: '受众画像抽取', type: '抽取类', enabled: true, trigger: '条件触发', priority: 2 },
{ name: '阻力因素提炼', type: '分析类', enabled: true, trigger: '手动', priority: 3 },
];
const dataSources = [
{ name: '宣传知识库', type: '知识库', status: 'connected', permission: '只读', trigger: '自动', access: '品牌案例、活动方法论、历史策略' },
{ name: 'Y系统', type: '业务系统', status: 'connected', permission: '只读', trigger: '条件触发', access: '用户标签、传播数据' },
{ name: 'A系统', type: '业务系统', status: 'disconnected', permission: '无', trigger: '不可用', access: '—' },
{ name: '市场数据库', type: '外部数据源', status: 'connected', permission: '只读', trigger: '手动', access: '市场报告、用户研究摘要' },
];
const functions = [
{ name: '信息抽取器', type: '抽取', enabled: true, trigger: '自动', usage: '提取任务中的受众相关信息' },
{ name: '标签聚类器', type: '分析', enabled: true, trigger: '条件触发', usage: '辅助进行受众分层' },
{ name: '案例检索器', type: '检索', enabled: true, trigger: '手动', usage: '调用历史案例进行参照' },
{ name: '结构化输出器', type: '格式化', enabled: true, trigger: '自动', usage: '输出标准JSON字段' },
];
const thinkingSteps = [
{ step: 1, name: '识别任务中的目标对象', desc: '提取任务中明确或隐含的目标受众信息', required: true, skill: '受众分层', func: '信息抽取器', ds: '无' },
{ step: 2, name: '判断是否需要分层', desc: '根据任务目标判断是否区分核心/次级/影响者受众', required: true, skill: '核心受众识别', func: '标签聚类器', ds: 'Y系统' },
{ step: 3, name: '提取核心受众特征', desc: '分析年龄、身份、关切与行为特征', required: true, skill: '画像抽取', func: '结构化输出器', ds: '宣传知识库' },
{ step: 4, name: '输出结构化结果', desc: '形成标准受众分析输出', required: true, skill: '—', func: '结构化输出器', ds: '—' },
];
const outputFields = [
{ name: 'core_audience', type: '文本', required: true },
{ name: 'secondary_audience', type: '文本', required: false },
{ name: 'key_traits', type: '数组', required: true },
{ name: 'concerns', type: '数组', required: false },
{ name: 'barriers', type: '数组', required: false },
{ name: 'trust_sources', type: '数组', required: false },
];
const aiOptimizations = [
{ issue: '当前已配置"受众画像抽取"技能,但未配置对应输出字段', canFix: true },
{ issue: '思考路径中"判断是否需要分层"步骤未绑定功能调用', canFix: true },
{ issue: '已绑定A系统,但当前无任何步骤使用该数据源', canFix: false },
];
const executionDetails = {
dataSources: ['宣传知识库', 'Y系统'],
skills: ['受众分层', '核心受众识别', '画像抽取'],
functions: ['信息抽取器', '标签聚类器', '结构化输出器'],
outputs: ['core_audience', 'key_traits', 'barriers'],
};
const handleSendMessage = () => {
if (chatInput.trim()) {
setChatMessages([...chatMessages, { role: 'user', content: chatInput }]);
setChatInput('');
setTimeout(() => {
setChatMessages(prev => [...prev, {
role: 'agent',
content: '核心受众应聚焦18-25岁基层青年群体,原因是该群体社交媒体活跃度高、注重性价比、偏好真实有态度的品牌叙事。'
}]);
}, 1000);
}
};
const handleGenerateConfig = () => {
setAiSuggestion('正在分析职责、技能、数据源...');
setTimeout(() => {
setShowGeneratedConfig(true);
setAiSuggestion('');
}, 1500);
};
const SectionHeader = ({
title,
subtitle,
icon: Icon,
section,
children
}: {
title: string;
subtitle: string;
icon: React.ElementType;
section: keyof ExpandedSection;
children: React.ReactNode;
}) => (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl`}>
<button
onClick={() => toggleSection(section)}
className={`w-full flex items-center justify-between p-4 ${isDark ? 'hover:bg-slate-800/50' : 'hover:bg-gray-100'} transition-colors sticky top-0 z-10 ${cardBg} backdrop-blur-xl`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center`}>
<Icon className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
</div>
<div className="text-left">
<h3 className={`${textPrimary} font-semibold`}>{title}</h3>
<p className={`${textMuted} text-xs`}>{subtitle}</p>
</div>
</div>
{expandedSections[section] ? (
<ChevronDown className={`w-5 h-5 ${textSecondary}`} />
) : (
<ChevronRight className={`w-5 h-5 ${textSecondary}`} />
)}
</button>
{expandedSections[section] && (
<div className="px-4 pb-4">
{children}
</div>
)}
</div>
);
return (
<div className="h-[calc(100vh-140px)] flex flex-col overflow-hidden">
{/* 顶部操作栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} p-4 flex-shrink-0`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button onClick={onBack} className={`p-2 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-200'} rounded-lg transition-colors`}>
<ArrowLeft className={`w-5 h-5 ${textSecondary}`} />
</button>
<div>
<p className={`${textSecondary} text-xs`}>智能体能力配置</p>
<div className="flex items-center gap-2 mt-0.5">
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<span className={`${textPrimary} font-semibold`}>AudienceAgent</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
isEnabled
? isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600'
: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-200 text-gray-500'
}`}>
{isEnabled ? '启用' : '停用'}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button className={`flex items-center gap-2 px-4 py-2 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
<Save className="w-4 h-4" />
保存
</button>
<button className={`flex items-center gap-2 px-4 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<RotateCcw className="w-4 h-4" />
重置
</button>
<button
onClick={() => setIsEnabled(!isEnabled)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm transition-colors ${
isEnabled
? isDark ? 'bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500/30 text-amber-400' : 'bg-amber-100 hover:bg-amber-200 border border-amber-200 text-amber-600'
: isDark ? 'bg-green-500/20 hover:bg-green-500/30 border border-green-500/30 text-green-400' : 'bg-green-100 hover:bg-green-200 border border-green-200 text-green-600'
}`}
>
<Power className="w-4 h-4" />
{isEnabled ? '停用' : '启用'}
</button>
<button className={`flex items-center gap-2 px-4 py-2 ${btnSecondary} ${isDark ? 'hover:text-red-400' : 'hover:text-red-500'} rounded-lg text-sm transition-colors`}>
<Trash2 className="w-4 h-4" />
删除
</button>
<button className={`flex items-center gap-2 px-4 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Copy className="w-4 h-4" />
复制
</button>
</div>
</div>
</div>
{/* 主内容区:三栏布局 */}
<div className="flex-1 grid grid-cols-12 gap-4 mt-4 min-h-0">
{/* 左侧主配置区 (65%) - 带自定义滚动条 */}
<div className="col-span-8 flex flex-col gap-4 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-transparent hover:scrollbar-thumb-slate-500">
{/* 模块1:基本信息 */}
<SectionHeader
title="基本信息"
subtitle="定义智能体的基本身份、用途和交互属性"
icon={Bot}
section="basic"
>
<div className="space-y-4 pt-2">
<div className="grid grid-cols-2 gap-4">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>智能体名称</label>
<input
type="text"
defaultValue="AudienceAgent"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>智能体编码</label>
<input
type="text"
defaultValue="AGENT_AUDIENCE_001"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>智能体分类</label>
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}>
<option>分析类</option>
<option>编排类</option>
<option>叙事类</option>
<option>校验类</option>
<option>成稿类</option>
</select>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>默认交互模式</label>
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}>
<option>分析式</option>
<option>对话式</option>
<option>指令式</option>
</select>
</div>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>智能体说明</label>
<textarea
rows={2}
defaultValue="负责分析核心受众特征,输出受众画像和行为特征,为后续策略制定提供数据支持"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>专业标签</label>
<div className={`flex flex-wrap gap-2 p-3 ${inputBg} ${inputBorder} rounded-lg min-h-[46px]`}>
{['受众分析', '用户画像', '分层识别'].map((tag) => (
<span key={tag} className={`px-2 py-1 ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600'} rounded text-xs flex items-center gap-1`}>
{tag}
<button className={`hover:${isDark ? 'text-blue-300' : 'text-red-400'}`}><X className="w-3 h-3" /></button>
</span>
))}
<button className={`px-2 py-1 ${textSecondary} hover:${textPrimary} text-xs flex items-center gap-1`}>
<Plus className="w-3 h-3" /> 添加
</button>
</div>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>适用任务关键词</label>
<div className={`flex flex-wrap gap-2 p-3 ${inputBg} ${inputBorder} rounded-lg min-h-[46px]`}>
{['宣传对象分析', '核心人群识别'].map((tag) => (
<span key={tag} className={`px-2 py-1 ${isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-orange-100 text-orange-600'} rounded text-xs flex items-center gap-1`}>
{tag}
<button className={`hover:${isDark ? 'text-cyan-300' : 'text-orange-400'}`}><X className="w-3 h-3" /></button>
</span>
))}
<button className={`px-2 py-1 ${textSecondary} hover:${textPrimary} text-xs flex items-center gap-1`}>
<Plus className="w-3 h-3" /> 添加
</button>
</div>
</div>
</div>
<div className={`flex items-center gap-6 pt-2 border-t ${cardBorder}`}>
<div className="flex items-center gap-3">
<span className={`${textSecondary} text-sm`}>支持单独问答</span>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className={`w-11 h-6 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all ${isDark ? 'peer-checked:bg-blue-600' : 'peer-checked:bg-red-500'}`}></div>
</label>
</div>
<div className="flex items-center gap-3">
<span className={`${textSecondary} text-sm`}>支持单独任务执行</span>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className={`w-11 h-6 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all ${isDark ? 'peer-checked:bg-blue-600' : 'peer-checked:bg-red-500'}`}></div>
</label>
</div>
</div>
</div>
</SectionHeader>
{/* 模块2:职责与边界 */}
<SectionHeader
title="职责与边界"
subtitle="明确智能体的工作范围、边界与禁止事项"
icon={Target}
section="responsibility"
>
<div className="space-y-4 pt-2">
<div className="grid grid-cols-2 gap-4">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>核心职责</label>
<textarea
rows={3}
defaultValue="识别任务中的关键受众群体,对受众进行分层,并提炼特征、关切与阻力,形成完整的受众画像报告。"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>职责边界</label>
<textarea
rows={3}
defaultValue="不直接生成叙事方案;不负责风险最终审核;不替代行为分析;不在缺少依据时擅自扩大受众范围。"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>禁止事项</label>
<textarea
rows={2}
defaultValue="不得在缺少依据时擅自扩大受众范围;不得输出与任务无关的人群判断;不得更改任务目标。"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>典型任务</label>
<textarea
rows={2}
defaultValue="分析某宣传任务的核心受众是谁;输出受众画像;识别影响者与阻力群体;提炼受众关切与信任来源。"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
/>
</div>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>不处理的问题</label>
<textarea
rows={2}
defaultValue="不负责撰写传播文案;不负责渠道投放建议;不生成具体执行方案;不进行风险评估。"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
/>
</div>
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
<Sparkles className="w-3 h-3" /> AI生成职责
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Lightbulb className="w-3 h-3" /> AI补全边界
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<AlertCircle className="w-3 h-3" /> AI发现冲突
</button>
</div>
</div>
</SectionHeader>
{/* 模块3:技能配置 */}
<SectionHeader
title="技能配置"
subtitle="定义智能体当前启用的技能及其触发方式"
icon={Layers}
section="skills"
>
<div className="space-y-3 pt-2">
{skills.map((skill, index) => (
<div key={index} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded ${isDark ? 'bg-purple-500/20' : 'bg-purple-100'} flex items-center justify-center`}>
<Brain className={`w-4 h-4 ${isDark ? 'text-purple-400' : 'text-purple-600'}`} />
</div>
<div>
<p className={`${textPrimary} text-sm font-medium`}>{skill.name}</p>
<p className={`${textMuted} text-xs`}>类型:{skill.type} · 触发:{skill.trigger} · 优先级:{skill.priority}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button className={`p-1.5 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-200'} rounded transition-colors`}>
<Wrench className={`w-4 h-4 ${textSecondary}`} />
</button>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked={skill.enabled} />
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
</label>
</div>
</div>
</div>
))}
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Plus className="w-3 h-3" /> 新增Skill
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
<Sparkles className="w-3 h-3" /> AI推荐Skill
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Zap className="w-3 h-3" /> AI生成技能组
</button>
</div>
</div>
</SectionHeader>
{/* 模块4:数据源访问 */}
<SectionHeader
title="数据源访问"
subtitle="定义智能体可读取的知识与系统数据"
icon={Database}
section="datasource"
>
<div className="space-y-3 pt-2">
{dataSources.map((ds, index) => (
<div key={index} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded flex items-center justify-center ${
ds.status === 'connected' ? (isDark ? 'bg-green-500/20' : 'bg-green-100') : (isDark ? 'bg-slate-500/20' : 'bg-gray-200')
}`}>
<Database className={`w-4 h-4 ${ds.status === 'connected' ? (isDark ? 'text-green-400' : 'text-green-600') : textSecondary}`} />
</div>
<div>
<p className={`${textPrimary} text-sm font-medium`}>{ds.name}</p>
<p className={`${textMuted} text-xs`}>类型:{ds.type} · 权限:{ds.permission} · 调用:{ds.trigger}</p>
</div>
</div>
<span className={`px-2 py-0.5 rounded text-xs ${
ds.status === 'connected'
? isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600'
: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-200 text-gray-500'
}`}>
{ds.status === 'connected' ? '已连接' : '未连接'}
</span>
</div>
<p className={`${textSecondary} text-xs mt-2`}>可访问:{ds.access}</p>
</div>
))}
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Plus className="w-3 h-3" /> 绑定数据源
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
<Sparkles className="w-3 h-3" /> AI推荐数据源
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<AlertCircle className="w-3 h-3" /> AI识别缺失
</button>
</div>
</div>
</SectionHeader>
{/* 模块5:功能调用 */}
<SectionHeader
title="功能调用"
subtitle="定义智能体可调配的处理功能与工具能力"
icon={Box}
section="function"
>
<div className="space-y-3 pt-2">
{functions.map((func, index) => (
<div key={index} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded ${isDark ? 'bg-cyan-500/20' : 'bg-cyan-100'} flex items-center justify-center`}>
<Box className={`w-4 h-4 ${isDark ? 'text-cyan-400' : 'text-cyan-600'}`} />
</div>
<div>
<p className={`${textPrimary} text-sm font-medium`}>{func.name}</p>
<p className={`${textMuted} text-xs`}>类型:{func.type} · 调用:{func.trigger}</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked={func.enabled} />
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
</label>
</div>
<p className={`${textSecondary} text-xs mt-2`}>用途:{func.usage}</p>
</div>
))}
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Plus className="w-3 h-3" /> 新增功能
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
<Sparkles className="w-3 h-3" /> AI推荐功能
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Zap className="w-3 h-3" /> AI检查冗余
</button>
</div>
</div>
</SectionHeader>
{/* 模块6:思考路径 */}
<SectionHeader
title="思考路径"
subtitle="定义智能体处理任务时的默认分析顺序"
icon={Route}
section="thinking"
>
<div className="space-y-3 pt-2">
{thinkingSteps.map((step) => (
<div key={step.step} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-start gap-3">
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 mt-0.5 ${
step.step <= 2
? isDark ? 'bg-green-500 text-white' : 'bg-green-500 text-white'
: isDark ? 'bg-blue-500 text-white' : 'bg-blue-500 text-white'
}`}>
{step.step}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<p className={`${textPrimary} text-sm font-medium`}>{step.name}</p>
{step.required && (
<span className={`px-1.5 py-0.5 ${isDark ? 'bg-red-500/20 text-red-400' : 'bg-red-100 text-red-600'} rounded text-[10px]`}>必选</span>
)}
</div>
<p className={`${textSecondary} text-xs mt-1`}>{step.desc}</p>
<div className="flex items-center gap-4 mt-2 text-xs">
<span className={`${textMuted}`}>Skill: <span className={isDark ? 'text-purple-400' : 'text-purple-600'}>{step.skill}</span></span>
<span className={`${textMuted}`}>功能: <span className={isDark ? 'text-cyan-400' : 'text-cyan-600'}>{step.func}</span></span>
<span className={`${textMuted}`}>数据源: <span className={isDark ? 'text-green-400' : 'text-green-600'}>{step.ds}</span></span>
</div>
</div>
<button className={`p-1.5 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-200'} rounded transition-colors`}>
<Wrench className={`w-4 h-4 ${textSecondary}`} />
</button>
</div>
</div>
))}
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Plus className="w-3 h-3" /> 新增步骤
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
<Sparkles className="w-3 h-3" /> AI生成思考路径
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Route className="w-3 h-3" /> AI优化顺序
</button>
</div>
</div>
</SectionHeader>
{/* 模块7:输出结构 */}
<SectionHeader
title="输出结构"
subtitle="定义对话输出、结构化输出与可复用片段"
icon={FileOutput}
section="output"
>
<div className="space-y-4 pt-2">
<div className={`p-3 ${isDark ? 'bg-blue-500/10 border border-blue-500/30' : 'bg-red-50 border border-red-200'} rounded-lg`}>
<p className={`${isDark ? 'text-blue-400' : 'text-red-600'} text-sm font-medium mb-2`}>A. 对话摘要输出</p>
<p className={`${textSecondary} text-xs`}>核心受众结论 · 结论理由摘要</p>
</div>
<div>
<p className={`${isDark ? 'text-cyan-400' : 'text-orange-600'} text-sm font-medium mb-3`}>B. 结构化输出字段</p>
<div className="space-y-2">
{outputFields.map((field, index) => (
<div key={index} className={`flex items-center gap-3 p-2 ${inputBg} ${inputBorder} rounded-lg`}>
<input
type="text"
defaultValue={field.name}
className={`flex-1 px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${isDark ? 'text-cyan-400' : 'text-orange-600'} text-sm font-mono focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
/>
<select className={`px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${textPrimary} text-xs`}>
<option selected={field.type === '文本'}>文本</option>
<option selected={field.type === '数组'}>数组</option>
<option>对象</option>
</select>
<label className="flex items-center gap-2 cursor-pointer">
<span className={`${textSecondary} text-xs`}>{field.required ? '必填' : '可选'}</span>
<input type="checkbox" className="sr-only peer" defaultChecked={field.required} />
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-blue-600' : 'peer-checked:bg-red-500'}`}></div>
</label>
</div>
))}
</div>
</div>
<div className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
<p className={`${isDark ? 'text-green-400' : 'text-green-600'} text-sm font-medium mb-2`}>C. 报告片段输出</p>
<div className="space-y-2">
<div>
<label className={`${textMuted} text-xs`}>字段名</label>
<input
type="text"
defaultValue="audience_analysis_paragraph"
className={`w-full px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${isDark ? 'text-cyan-400' : 'text-orange-600'} text-sm font-mono focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
/>
</div>
<div>
<label className={`${textMuted} text-xs`}>用途说明</label>
<input
type="text"
defaultValue={'用于写入正式方案中的"受众分析"段落'}
className={`w-full px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
/>
</div>
</div>
</div>
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
<Sparkles className="w-3 h-3" /> AI生成输出字段
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Lightbulb className="w-3 h-3" /> AI补全字段说明
</button>
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
<Check className="w-3 h-3" /> AI校验完整性
</button>
</div>
</div>
</SectionHeader>
</div>
{/* 右侧辅助区 (35%) */}
<div className="col-span-4 flex flex-col gap-4 min-h-0 overflow-y-auto pl-2">
{/* 卡片1:AI辅助生成 */}
<div className={`${isDark ? 'bg-gradient-to-br from-purple-600/20 to-blue-600/20' : 'bg-gradient-to-br from-purple-100 to-blue-100'} backdrop-blur-xl ${isDark ? 'border border-purple-500/30' : 'border border-purple-300'} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
<Sparkles className={`w-4 h-4 ${isDark ? 'text-purple-400' : 'text-purple-600'}`} />
AI辅助生成
</h3>
<div className="space-y-3">
<div>
<label className={`block ${textSecondary} text-xs mb-1.5`}>一句话描述</label>
<textarea
rows={2}
placeholder="例如:这是一个负责识别宣传任务核心受众并输出画像的分析型智能体..."
className={`w-full px-3 py-2 ${inputBg} ${isDark ? 'border-purple-500/30' : 'border-purple-300'} rounded-lg ${textPrimary} text-xs focus:outline-none ${isDark ? 'focus:border-purple-500/50' : 'focus:border-purple-400'} resize-none`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-xs mb-1.5`}>示例任务</label>
<textarea
rows={2}
placeholder="例如:请分析某青年节宣传活动的核心受众、次级受众及其主要关切..."
className={`w-full px-3 py-2 ${inputBg} ${isDark ? 'border-purple-500/30' : 'border-purple-300'} rounded-lg ${textPrimary} text-xs focus:outline-none ${isDark ? 'focus:border-purple-500/50' : 'focus:border-purple-400'} resize-none`}
/>
</div>
<button
onClick={handleGenerateConfig}
className={`w-full px-4 py-2 ${isDark ? 'bg-purple-600 hover:bg-purple-500' : 'bg-purple-500 hover:bg-purple-400'} text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2`}
>
<Zap className="w-4 h-4" />
一键生成配置
</button>
{showGeneratedConfig && (
<div className={`p-3 ${inputBg} ${isDark ? 'border-purple-500/30' : 'border-purple-300'} rounded-lg space-y-2`}>
<p className={`${isDark ? 'text-purple-400' : 'text-purple-600'} text-xs font-medium`}>生成建议:</p>
<div className={`space-y-1 text-xs ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
<p>• 推荐职责:受众识别与画像输出</p>
<p>• 推荐技能:分层识别、特征抽取</p>
<p>• 推荐数据源:Y系统、宣传知识库</p>
<p>• 推荐思考路径:4步法</p>
</div>
<div className={`flex items-center gap-2 pt-2 border-t ${isDark ? 'border-purple-500/30' : 'border-purple-300'}`}>
<button className={`flex-1 px-3 py-1.5 ${isDark ? 'bg-green-600 hover:bg-green-500' : 'bg-green-500 hover:bg-green-400'} text-white rounded text-xs transition-colors`}>
采纳全部
</button>
<button className={`flex-1 px-3 py-1.5 ${btnSecondary} rounded text-xs transition-colors`}>
逐项插入
</button>
<button className={`px-3 py-1.5 ${btnSecondary} rounded text-xs transition-colors`}>
忽略
</button>
</div>
</div>
)}
</div>
</div>
{/* 卡片2:AI优化建议 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
<Lightbulb className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
AI优化建议
</h3>
<div className="space-y-3">
{aiOptimizations.map((opt, index) => (
<div key={index} className={`p-2 ${isDark ? 'bg-amber-500/10 border border-amber-500/30' : 'bg-amber-50 border border-amber-200'} rounded-lg`}>
<p className={`${isDark ? 'text-slate-300' : 'text-gray-700'} text-xs mb-2`}>{opt.issue}</p>
<div className="flex items-center gap-2">
{opt.canFix && (
<button className={`px-2 py-1 ${btnPrimary} text-white rounded text-xs transition-colors`}>
一键修复
</button>
)}
<button className={`px-2 py-1 ${btnSecondary} rounded text-xs transition-colors`}>
忽略
</button>
</div>
</div>
))}
</div>
</div>
{/* 卡片3:对话试用 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 flex flex-col`}>
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
<MessageSquare className={`w-4 h-4 ${isDark ? 'text-cyan-400' : 'text-cyan-600'}`} />
对话试用
</h3>
<div className="flex items-center gap-3 mb-3">
<div className="flex-1">
<label className={`${textMuted} text-xs mb-1`}>模式</label>
<select className={`w-full px-2 py-1.5 ${inputBg} ${inputBorder} rounded ${textPrimary} text-xs`}>
<option>问答</option>
<option>任务</option>
</select>
</div>
<div className="flex items-center gap-1">
<span className={`${textSecondary} text-xs`}>数据源</span>
<label className="relative inline-flex items-center cursor-pointer mx-1">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
</label>
</div>
<div className="flex items-center gap-1">
<span className={`${textSecondary} text-xs`}>功能</span>
<label className="relative inline-flex items-center cursor-pointer mx-1">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
</label>
</div>
</div>
<div className={`flex-1 min-h-[180px] max-h-[180px] overflow-y-auto p-3 ${inputBg} ${inputBorder} rounded-lg mb-3 space-y-3`}>
{chatMessages.map((msg, index) => (
<div key={index} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-2.5 rounded-lg text-xs ${
msg.role === 'user'
? isDark ? 'bg-blue-600 text-white' : 'bg-red-500 text-white'
: isDark ? 'bg-slate-800 text-slate-300' : 'bg-gray-200 text-gray-800'
}`}>
{msg.content}
</div>
</div>
))}
</div>
<div className="flex gap-2">
<input
type="text"
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="请输入问题或任务要求..."
className={`flex-1 px-3 py-2 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-xs focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
/>
<button
onClick={handleSendMessage}
className={`px-3 py-2 ${btnPrimary} text-white rounded-lg transition-colors`}
>
<Send className="w-4 h-4" />
</button>
</div>
<button className={`mt-2 w-full px-3 py-1.5 ${btnSecondary} rounded text-xs transition-colors`}>
插入示例任务
</button>
</div>
{/* 卡片4:本轮执行详情 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
<Clock className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
本轮执行详情
</h3>
<div className="space-y-3 text-xs">
<div>
<p className={`${textMuted} mb-1.5`}>调用数据源</p>
<div className="flex flex-wrap gap-1">
{executionDetails.dataSources.map((ds) => (
<span key={ds} className={`px-2 py-0.5 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600'} rounded`}>{ds}</span>
))}
</div>
</div>
<div>
<p className={`${textMuted} mb-1.5`}>启用技能</p>
<div className="flex flex-wrap gap-1">
{executionDetails.skills.map((skill) => (
<span key={skill} className={`px-2 py-0.5 ${isDark ? 'bg-purple-500/20 text-purple-400' : 'bg-purple-100 text-purple-600'} rounded`}>{skill}</span>
))}
</div>
</div>
<div>
<p className={`${textMuted} mb-1.5`}>调用功能</p>
<div className="flex flex-wrap gap-1">
{executionDetails.functions.map((func) => (
<span key={func} className={`px-2 py-0.5 ${isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-cyan-100 text-cyan-600'} rounded`}>{func}</span>
))}
</div>
</div>
<div>
<p className={`${textMuted} mb-1.5`}>输出字段</p>
<div className="flex flex-wrap gap-1">
{executionDetails.outputs.map((out) => (
<span key={out} className={`px-2 py-0.5 ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-blue-100 text-blue-600'} rounded font-mono`}>{out}</span>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default AgentDetail;
+404
View File
@@ -0,0 +1,404 @@
// ============================================
// AgentList.tsx - 智能体列表页面组件
// ============================================
// 功能描述:展示系统中所有智能体的列表,支持搜索、筛选、分页等功能
// 主要特性:
// - 智能体卡片展示(名称、描述、模型类型、状态等)
// - 支持按类型和状态筛选
// - 支持搜索智能体名称
// - 分页功能
// - 暗黑/亮色主题切换
// - 查看、编辑、测试智能体操作
// ============================================
import React, { useState, useEffect, useRef } from 'react';
// 导入图标组件,用于界面中的各种图标显示
import { Plus, Search, Eye, Edit, Play, Pause, Trash2, Bot, ChevronRight, GitBranch } from 'lucide-react';
// 导入主题上下文,用于获取当前主题(暗黑/亮色)
import { useTheme } from '../contexts/ThemeContext';
// 导入页面类型定义,用于页面导航
import { PageType } from '../components/Sidebar';
// 导入请求服务,用于与后端 API 交互
import { requestCrudService } from '@/service/request-axios-service';
// 导入 TDesign 组件库的加载和分页组件
import { Loading, Pagination } from 'tdesign-react';
/**
* AgentItem - 智能体数据项接口定义
* 描述单个智能体的数据结构
*/
interface AgentItem {
avatarUrl: string; // 智能体头像 URL
createdAt: string; // 创建时间
description: string; // 智能体描述
id: string; // 智能体唯一标识符
modelName: string; // 使用的模型名称
name: string; // 智能体名称
systemPrompt: string; // 系统提示词
userId: string; // 创建者用户 ID
isDraft: boolean; // 是否为草稿状态
}
/**
* AgentListProps - AgentList 组件属性接口
* 定义组件接收的外部属性
*/
interface AgentListProps {
onViewDetail: (id: string) => void; // 查看智能体详情的回调函数
onNavigate: (page: PageType) => void; // 页面导航的回调函数
}
// 定义 API 基础 URL,从环境变量中获取,如果不存在则为空字符串
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
// 创建请求服务实例,用于后续 API 调用
const service = requestCrudService;
/**
* AgentList - 智能体列表组件
* 主组件函数,负责渲染智能体列表页面
* @param onViewDetail - 查看详情的回调函数
* @param onNavigate - 页面导航的回调函数
*/
const AgentList: React.FC<AgentListProps> = ({ onViewDetail, onNavigate }) => {
// 获取当前主题配置
const { theme } = useTheme();
// 判断是否为暗黑模式
const isDark = theme === 'dark';
// 智能体列表数据状态,存储所有智能体信息
const [agents, setAgents] = useState<AgentItem[]>([]);
const [searchQuery, setSearchQuery] = useState('');
// 加载状态,用于显示加载动画
const [loading, setLoading] = useState<boolean>(false);
// 当前页码,默认为第 1 页
const [pageNum, setPageNum] = useState<number>(1);
// 每页显示数量,默认为 10 条
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
// 数据总数,用于分页组件显示
const [total, setTotal] = useState<number>(0);
// 使用 ref 存储分页状态,避免组件重新渲染时丢失
const state = useRef({
pageNum: 1,
pageSize: 10
});
// 回车处理
const handleKeyDown = async (e) => {
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
e.preventDefault();
} else if (e.keyCode === 13) {
e.preventDefault();
await agentListSearch();
} else if (e.key === "/") {
e.preventDefault();
}
};
/**
* 根据智能体类型返回对应的标签颜色样式
* 根据当前主题(暗黑/亮色)返回不同的颜色配置
* @param category - 智能体类型(如 GLM-5、Qwen3.5-27B、叙事生成等)
* @returns 返回对应的 CSS 类名,用于标签背景色和文字颜色
*/
const getCategoryColor = (category: string) => {
// 暗黑模式下的颜色配置
if (isDark) {
switch (category) {
case 'GLM-5':
return 'bg-blue-500/20 text-blue-400'; // 蓝色背景 + 蓝色文字
case 'Qwen3.5-27B':
return 'bg-cyan-500/20 text-cyan-400'; // 青色背景 + 青色文字
case 'Qwen3-32B':
return 'bg-purple-500/20 text-purple-400'; // 紫色背景 + 紫色文字
case '叙事生成':
return 'bg-pink-500/20 text-pink-400'; // 粉色背景 + 粉色文字
case '逻辑链构建':
return 'bg-amber-500/20 text-amber-400'; // 琥珀色背景 + 琥珀色文字
case '风险评估':
return 'bg-red-500/20 text-red-400'; // 红色背景 + 红色文字
case '方案生成':
return 'bg-green-500/20 text-green-400'; // 绿色背景 + 绿色文字
default:
return 'bg-slate-500/20 text-slate-400'; // 默认灰色
}
} else {
// 亮色模式下的颜色配置,统一使用红色系
switch (category) {
case 'GLM-5':
return 'bg-red-100 text-red-600';
case 'Qwen3.5-27B':
return 'bg-red-100 text-red-600';
case 'Qwen3-32B':
return 'bg-red-100 text-red-600';
case '叙事生成':
return 'bg-red-100 text-red-600';
case '逻辑链构建':
return 'bg-red-100 text-red-600';
case '风险评估':
return 'bg-red-100 text-red-600';
case '方案生成':
return 'bg-red-100 text-red-600';
default:
return 'bg-gray-100 text-gray-600'; // 默认灰色
}
}
};
/**
* 根据智能体类型返回对应的图标颜色样式
* 用于智能体卡片左侧图标的背景色和图标颜色
* @param type - 智能体类型
* @returns 返回包含背景色(bg)和图标颜色(icon)的对象
*/
const getTypeIconColor = (type: string) => {
// 暗黑模式下的图标颜色配置
if (isDark) {
switch (type) {
case 'GLM-5':
return { bg: 'bg-blue-500/20', icon: 'text-blue-400' }; // 蓝色系
case 'Qwen3.5-27B':
return { bg: 'bg-cyan-500/20', icon: 'text-cyan-400' }; // 青色系
case 'Qwen3-32B':
return { bg: 'bg-pink-500/20', icon: 'text-pink-400' }; // 粉色系
case '校验类':
return { bg: 'bg-amber-500/20', icon: 'text-amber-400' }; // 琥珀色系
case '成稿类':
return { bg: 'bg-green-500/20', icon: 'text-green-400' }; // 绿色系
default:
return { bg: 'bg-slate-500/20', icon: 'text-slate-400' }; // 默认灰色
}
} else {
// 亮色模式下统一使用红色系
return { bg: 'bg-red-100', icon: 'text-red-500' };
}
};
/**
* 获取智能体列表数据
* 调用后端 API 获取智能体列表,并更新组件状态
* 请求参数包括页码、每页数量和搜索名称
*/
const agentListSearch = async () => {
// 构建请求参数对象
const parmy = {
pageNum: state.current.pageNum, // 当前页码
pageSize: state.current.pageSize, // 每页显示数量
name: searchQuery // 搜索名称(当前为空,可扩展)
}
// 开始加载,显示加载动画
setLoading(true);
// 调用后端 API 获取智能体列表数据
const res = await requestCrudService.post(`${zncmUrl}/agent/expertAgents/list`, parmy);
// 加载完成,隐藏加载动画
setLoading(false);
// 打印响应数据到控制台,用于调试
console.log('智能体列表', res);
// 解析响应数据
const result = res.data;
// 更新数据总数,用于分页
setTotal(result.total);
// 更新智能体列表数据,如果无数据则设置为空数组
setAgents(result.records || []);
}
/**
* 页码变化时的回调函数
* 当用户切换页码时触发,更新页码并重新获取数据
* @param value - 新的页码值
*/
const pageIndexChange = async (value: number) => {
// 更新 ref 中存储的页码值
state.current.pageNum = value;
// 更新页码状态,触发组件重新渲染
setPageNum(value);
// 重新获取智能体列表数据
await agentListSearch();
}
/**
* 每页显示数量变化时的回调函数
* 当用户切换每页显示条数时触发,更新每页数量并重新获取数据
* @param value - 新的每页显示数量
*/
const pageSizeChange = async (value: number) => {
// 更新 ref 中存储的每页显示数量
state.current.pageSize = value;
// 更新每页显示数量状态,触发组件重新渲染
setPageSize(value);
// 重新获取智能体列表数据
await agentListSearch();
}
/**
* 组件挂载时的副作用
* 组件首次渲染时自动调用一次获取智能体列表
* 依赖数组为空,表示只在组件挂载时执行一次
*/
useEffect(() => {
agentListSearch();
}, []);
return (
<div className="space-y-6">
{/* 页面标题区 */}
<div className="flex items-center justify-between">
<div>
<h1 className={`text-2xl font-bold mb-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>智能体配置管理</h1>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>统一管理系统中的智能体配置</p>
</div>
<button className={`flex items-center gap-2 px-4 py-2.5 text-white rounded-lg transition-colors shadow-md ${isDark
? 'bg-blue-600 hover:bg-blue-500'
: 'bg-red-600 hover:bg-red-500'
}`}>
<Plus className="w-4 h-4" />
<span className="text-sm font-medium">新建智能体</span>
</button>
</div>
{/* 搜索和筛选区 */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-red-400'}`} />
<input
type="text"
value={searchQuery}
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索智能体名称..."
className={`w-full pl-10 pr-4 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-white placeholder-slate-500'
: 'bg-white border-red-200 text-gray-900 placeholder-red-300'
}`}
/>
</div>
<button className={`px-4 py-2.5 border rounded-lg text-sm transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-slate-700/50'
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50'
}`}>
全部类型
</button>
<button className={`px-4 py-2.5 border rounded-lg text-sm transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-slate-700/50'
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50'
}`}>
全部状态
</button>
</div>
{/* 分类标签 */}
<div className="flex items-center gap-3">
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>按类型筛选:</span>
{['编排类', '分析类', '叙事类', '校验类', '成稿类'].map((type) => (
<button
key={type}
className={`px-3 py-1.5 border rounded-full text-sm transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-blue-600/20 hover:border-blue-500/50 hover:text-blue-400'
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50 hover:border-red-300 hover:text-red-600'
}`}
>
{type}
</button>
))}
</div>
{/* 智能体列表 */}
<Loading loading={loading}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{agents.map((agent) => {
const typeColors = getTypeIconColor(agent.modelName);
return (
<div
key={agent.id}
className={`rounded-xl p-5 transition-all duration-300 group cursor-pointer border theme-card ${isDark
? 'bg-slate-800/30 backdrop-blur-xl border-blue-800/30 hover:border-blue-500/50'
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
}`}
onClick={() => {
onViewDetail(agent.id);
onNavigate('agent-detail');
}}
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${typeColors.bg}`}>
<Bot className={`w-6 h-6 ${typeColors.icon}`} />
</div>
<div>
<h3 className={`font-semibold text-lg group-hover:text-red-500 transition-colors ${isDark ? 'text-white' : 'text-gray-900'}`}>{agent.name}</h3>
<p className={`text-sm ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{agent.description}</p>
</div>
</div>
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${agent.isDraft
? isDark
? 'bg-green-500/20 text-green-400'
: 'bg-green-100 text-green-600'
: isDark
? 'bg-yellow-500/20 text-yellow-400'
: 'bg-yellow-100 text-yellow-600'
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${agent.isDraft
? isDark ? 'bg-green-400' : 'bg-green-500'
: isDark ? 'bg-yellow-400' : 'bg-yellow-500'
}`} />
{agent.isDraft ? '启用' : '暂停'}
</span>
</div>
<div className="flex items-center gap-4 mb-4">
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${getCategoryColor(agent.modelName)}`}>
{agent.modelName}
</span>
{/* <span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{agent.description}</span> */}
</div>
<div className={`flex items-center justify-between pt-4 border-t ${isDark ? 'border-blue-800/30' : 'border-red-100'
}`}>
<div className="flex items-center gap-6">
<div className="overflow-hidden text-ellipsis line-clamp-3">
{/* <p className={`text-xs mb-1 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>输入字段</p> */}
<p className={`font-medium ${isDark ? 'text-cyan-400' : 'text-red-600'}`}>{agent.systemPrompt} 个</p>
</div>
{/* <div>
<p className={`text-xs mb-1 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>输出字段</p>
<p className={`font-medium ${isDark ? 'text-green-400' : 'text-red-600'}`}>{agent.outputs} 个</p>
</div> */}
</div>
<div className="flex items-center gap-2">
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
}`} title="查看">
<Eye className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-white' : 'text-red-400 group-hover/btn:text-red-600'}`} />
</button>
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
}`} title="编辑">
<Edit className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-white' : 'text-red-400 group-hover/btn:text-red-600'}`} />
</button>
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-green-500/20' : 'hover:bg-green-50'
}`} title="测试">
<Play className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-green-400' : 'text-red-400 group-hover/btn:text-green-600'}`} />
</button>
</div>
</div>
</div>
);
})}
</div>
</Loading>
{/* 分页 */}
<div className={`flex items-center justify-between pt-4 ${isDark ? '' : 'border-t border-red-100'}`}>
<Pagination
total={total}
current={pageNum}
pageSize={pageSize}
onCurrentChange={(page) => pageIndexChange(Number(page))}
onPageSizeChange={(size) => pageSizeChange(Number(size))} // 每页数量变化时的回调
/>
</div>
</div>
);
};
export default AgentList;
+601
View File
@@ -0,0 +1,601 @@
"use client";
import { toolsCrudService } from "@/service/request-tools-service";
import {
Sparkles,
ArrowRight,
Plus,
Bot,
Trash2,
Loader2,
Pencil,
Info,
MessageCircle,
Eye,
EyeOff,
} from "lucide-react";
import { useAtomValue } from "jotai";
import { useEffect, useRef, useState } from "react";
import { Link, useNavigate } from 'react-router-dom';
import { toast } from "sonner";
import { Button, Dialog, Switch } from "tdesign-react";
import { toolsUser, useChatContext } from "@/contexts/ChatContext";
interface ExpertAgent {
id: number,
name: string,
avatar_url: string,
description: string,
system_prompt: string,
model_name: string,
user_id: string,
created_at: string,
is_draft: boolean,
}
interface expert {
id: number;
name: string;
avatar_url: string | null;
description: string | null;
system_prompt: string;
model_name: string | null;
user_id: string;
created_at: string;
is_draft: boolean;
}
/**
* 将纯 SVG 字符串转换为 data URL
*/
export const svgToDataUrl = (svg: string) => {
const encoded = encodeURIComponent(svg)
.replace(/'/g, '%27')
.replace(/"/g, '%22');
return `data:image/svg+xml;charset=utf-8,${encoded}`;
}
const backendUrl = import.meta.env.VITE_AIPI_BACKEND_URL || '';
const service = toolsCrudService
// 专家详情模态框组件
function ExpertDetailDialog({
expert,
open,
onOpenChange,
agentSearch,
}: {
expert: ExpertAgent;
open: boolean;
onOpenChange: (open: boolean) => void;
agentSearch?: () => void;
}) {
const router = useNavigate();
const [isDraft, setIsDraft] = useState<boolean>(expert.is_draft || true);
const [isUpdating, setIsUpdating] = useState(false);
const isSvg = expert.avatar_url
? /<svg[\s\S]*<\/svg>/i.test(expert.avatar_url)
: false;
const avatarSrc =
isSvg && expert.avatar_url
? svgToDataUrl(expert.avatar_url)
: expert.avatar_url;
const gradients = [
{ from: "rose-500", to: "pink-500" },
{ from: "violet-500", to: "purple-500" },
{ from: "amber-500", to: "orange-500" },
{ from: "emerald-500", to: "teal-500" },
{ from: "blue-500", to: "indigo-500" },
{ from: "cyan-500", to: "sky-500" },
];
const gradient = gradients[expert.id % gradients.length];
const handleTogglePublished = async (checked: boolean) => {
// checked 为 true 表示发布(is_draft = false),checked 为 false 表示草稿(is_draft = true)
const newDraftStatus: boolean = !checked;
setIsUpdating(true);
try {
const response = await service.patch(
`${backendUrl}/api/v1/expert-agents/${expert.id}`,
{ is_draft: newDraftStatus }
);
setIsDraft(newDraftStatus);
toast.success(newDraftStatus ? "已设为草稿" : "已发布");
await agentSearch();
} catch (error) {
console.error("Toggle publish error:", error);
toast.error("操作失败");
} finally {
setIsUpdating(false);
}
};
const handleStartChat = () => {
onOpenChange(false);
router(`/agents/new-chat?expert_id=${expert.id}`);
};
return (
<Dialog
visible={open}
header="专家详情"
width="520px"
confirmBtn="保存"
cancelBtn="取消"
onConfirm={() => onOpenChange(false)}
onClose={() => onOpenChange(false)}
>
<div className="flex flex-col items-center space-y-4 py-4">
{/* 头像 */}
<div
className={`w-20 h-20 rounded-2xl bg-gradient-to-br from-${gradient.from} to-${gradient.to} flex items-center justify-center shadow-lg`}
>
{avatarSrc ? (
<img
src={avatarSrc}
alt={expert.name}
className="w-12 h-12 rounded-xl object-contain"
/>
) : (
<Bot className="w-10 h-10 text-white" />
)}
</div>
{/* 名称和描述 */}
<div className="text-center">
<h3 className="text-xl font-semibold text-gray-900">
{expert.name}
</h3>
{expert.description ? (
<p className="mt-2 text-sm text-gray-500">{expert.description}</p>
) : (
<p className="mt-2 text-sm text-gray-400 italic">暂无描述</p>
)}
</div>
{/* 发布开关 */}
<div className="flex items-center justify-between w-full px-4 py-3 bg-gray-50 rounded-xl">
<div className="flex items-center gap-2">
{isDraft ? (
<EyeOff className="w-4 h-4 text-gray-400" />
) : (
<Eye className="w-4 h-4 text-gray-600" />
)}
<span className="text-sm text-gray-700">
{isDraft ? "草稿状态" : "已发布"}
</span>
</div>
<Switch
value={!isDraft}
onChange={handleTogglePublished}
disabled={isUpdating}
/>
</div>
{/* 开始聊天按钮 */}
<Button
onClick={handleStartChat}
className="w-full bg-gradient-to-r border-[bg-gradient-to-r] from-violet-500 to-purple-500 hover:from-violet-600 hover:to-purple-600"
>
<MessageCircle className="w-4 h-4 mr-2" />
开始聊天
</Button>
</div>
</Dialog>
);
}
function ExpertCard({ expert, changeState }: { expert: ExpertAgent, changeState: () => void }) {
const [isDeleting, setIsDeleting] = useState(false);
const [showDetail, setShowDetail] = useState(false);
const isSvg = expert.avatar_url
? /<svg[\s\S]*<\/svg>/i.test(expert.avatar_url)
: false;
// 如果是纯 SVG,转换为 data URL;否则直接使用
const avatarSrc =
isSvg && expert.avatar_url
? svgToDataUrl(expert.avatar_url)
: expert.avatar_url;
const router = useNavigate();
const handleDelete = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!confirm(`确定要删除专家 "${expert.name}" 吗?`)) {
return;
}
setIsDeleting(true);
try {
await service.delete(`${backendUrl}/api/v1/expert-agents/${expert.id}`);
await changeState();
} finally {
setIsDeleting(false);
}
};
// Generate consistent gradient based on expert id
const gradients = [
{ from: "rose-500", to: "pink-500", shadow: "rose-500" },
{ from: "violet-500", to: "purple-500", shadow: "violet-500" },
{ from: "amber-500", to: "orange-500", shadow: "amber-500" },
{ from: "emerald-500", to: "teal-500", shadow: "emerald-500" },
{ from: "blue-500", to: "indigo-500", shadow: "blue-500" },
{ from: "cyan-500", to: "sky-500", shadow: "cyan-500" },
];
const gradient = gradients[expert.id % gradients.length];
return (
<div
className="rounded-lg border text-card-foreground shadow-sm group relative h-full cursor-pointer overflow-hidden border-gray-200/60 book-bg-yellow bg-white transition-all duration-300 hover:border-gray-300 hover:shadow-xl hover:shadow-gray-200/40"
>
<div
className="group relative h-full p-[20px] cursor-pointer overflow-hidden border-gray-200/60 book-bg-yellow bg-white transition-all duration-300 hover:border-gray-300 hover:shadow-xl hover:shadow-gray-200/40"
onClick={() => router(`/agents/new-chat?expert_id=${expert.id}`)}
>
{/* Subtle gradient overlay on hover */}
<div
className={`absolute inset-0 bg-gradient-to-br from-${gradient.from}/5 to-${gradient.to}/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100 pointer-events-none`}
/>
<div className="relative pb-4">
<div className="flex items-start justify-between">
{/* Enhanced icon with animated gradient background */}
<div className="relative">
<div
className={`w-14 h-14 rounded-2xl bg-gradient-to-br from-${gradient.from} to-${gradient.to} flex items-center justify-center shadow-lg shadow-${gradient.shadow}/30 transition-all duration-300 group-hover:scale-110 group-hover:shadow-xl group-hover:shadow-${gradient.shadow}/40`}
>
{avatarSrc ? (
// avatarSrc 已经是正确的 URL 格式(纯 SVG 已转换为 data URL)
<img
src={avatarSrc}
alt={expert.name}
className="w-9 h-9 rounded-xl object-contain"
/>
) : (
<Bot className="w-7 h-7 text-white" />
)}
</div>
{/* Glowing effect on hover */}
<div
className={`absolute inset-0 rounded-2xl bg-gradient-to-br from-${gradient.from} to-${gradient.to} opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-30 -z-10`}
/>
</div>
{/* Enhanced action buttons with larger click areas */}
<div className="flex items-center gap-1">
{expert.user_id && (
<>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowDetail(true);
}}
className="group/btn relative flex h-10 min-w-[40px] items-center justify-center rounded-xl text-gray-400 transition-all duration-300 hover:bg-gray-100 hover:text-gray-600"
title="查看详情"
>
<Info className="w-4 h-4 transition-transform duration-200 group-hover/btn:scale-110" />
</button>
<span
className="group/btn relative flex h-10 min-w-[40px] items-center justify-center rounded-xl text-gray-400 transition-all duration-300 hover:bg-blue-50 hover:text-blue-600"
onClick={(e) => {e.stopPropagation(); router(`/agents/new-expert/${expert.id}`)}}
title="编辑专家"
>
<Pencil className="w-4 h-4 transition-transform duration-200 group-hover/btn:scale-110" />
</span>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDelete(e);
}}
disabled={isDeleting}
className="group/btn relative flex h-10 min-w-[40px] items-center justify-center rounded-xl text-gray-400 transition-all duration-300 hover:bg-red-50 hover:text-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="删除专家"
>
{isDeleting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<>
<Trash2 className="w-4 h-4 transition-transform duration-200 group-hover/btn:scale-110" />
</>
)}
</button>
</>
)}
</div>
</div>
</div>
<div className="relative pb-4 pt-0">
{/* 发布状态标识 */}
<div className="flex items-center gap-2 mb-2">
{/* Enhanced title with better typography */}
<div className="text-lg font-semibold text-gray-900 transition-colors duration-200 group-hover:text-gray-900 line-clamp-1 flex-1">
{expert.name}
</div>
{/* 发布状态徽章 */}
{!expert.is_draft ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">
<Eye className="w-3 h-3" />
已发布
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500 border border-gray-200">
<EyeOff className="w-3 h-3" />
草稿
</span>
)}
</div>
{/* Enhanced description with better readability */}
{expert.description ? (
<div className="mt-2 text-sm text-gray-500 leading-relaxed line-clamp-2">
{expert.description}
</div>
) : (
<div className="mt-2 text-sm text-gray-400 italic line-clamp-2">
暂无描述
</div>
)}
{/* Interactive indicator */}
<div className="mt-4 flex items-center gap-2 text-xs font-medium text-gray-400 transition-all duration-300 group-hover:text-gray-600">
<span>点击开始对话</span>
<ArrowRight className="w-3 h-3 transition-transform duration-300 group-hover:translate-x-1" />
</div>
</div>
{/* Bottom accent bar that animates on hover */}
<div
className={`absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-${gradient.from} to-${gradient.to} transform scale-x-0 transition-transform duration-300 group-hover:scale-x-100 origin-left`}
/>
</div>
{/* 详情模态框 */}
<ExpertDetailDialog
expert={expert}
open={showDetail}
onOpenChange={(open) => {
setShowDetail(open);
changeState()
}}
/>
</div>
);
}
function LoadingSkeleton() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<div key={i} className="h-48 rounded-2xl bg-gray-100 animate-pulse" />
))}
</div>
);
}
export default function ExpertsAgentsPage() {
const {toolsUser, setToolsUser} = useChatContext()
const router = useNavigate();
const [isCreating, setIsCreating] = useState(false);
const [llmModels, setLlmModels] = useState<{ id: string; name: string }[]>(
[]
);
const [activeTab, setActiveTab] = useState<"mine" | "published">("mine");
const queryClient = '';
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [experts, setExperts] = useState<ExpertAgent[]>([]);
const toolsUserData = useRef<toolsUser | null>(null)
const filteredExperts = [];
// 对于"我的专家",需要根据 user_id 过滤(因为后端返回所有专家)
// 获取可用的 LLM 模型列表
useEffect(() => {
toolsLoginSearch();
}, [activeTab]);
const toolsLoginSearch = async () => {
const formData = new FormData();
formData.append('username', 'liuqiang@qq.com');
formData.append('password', '123456');
formData.append('grant_type', 'password');
const res: any = await fetch(`${backendUrl}/auth/jwt/login`, {
method: 'POST',
headers: {
accept: 'application/json',
},
body: formData,
}).then(res => {
return res.json();
});
console.log('登录信息', res);
localStorage.setItem('toolToken', res.access_token);
await searchMeChange();
await expertAgentSearh();
await llmSearch();
}
const llmSearch = async () => {
const res = await service.get(`${backendUrl}/api/v1/expert-agents/llms`);
console.log('模型',res);
const result = JSON.parse(JSON.stringify(res));
setLlmModels(result)
}
const searchMeChange = async () => {
const res = await service.get(`${backendUrl}/users/me`, '');
const result =JSON.parse(JSON.stringify(res));
toolsUserData.current = result;
setToolsUser(result as toolsUser);
}
const expertAgentSearh = async () => {
const param = {
is_draft: activeTab === 'mine'
}
const res: any = await service.post(`${backendUrl}/api/v1/expert-agents/list`, param);
console.log('res', res);
const result = res.filter(item => item.user_id === toolsUserData.current.id);
console.log(result);
setExperts(result);
}
const handleCreateExpert = async () => {
setIsCreating(true);
try {
const request = {
name: "未命名专家",
model_name: llmModels[0]?.name ?? null,
system_prompt: "你是一个专业的助手,能够回答用户的问题。",
};
const res: any = await service.post(`${backendUrl}/api/v1/expert-agents`, request);
// 跳转到编辑页面
// router(`/agents/new-expert/${res.id}`);
} finally {
setIsCreating(false);
}
};
return (
<div className="min-h-screen bg-white">
{/* Header */}
<div
className="px-8 py-12 border-b border-gray-100"
>
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-500 flex items-center justify-center shadow-lg shadow-violet-500/20">
<Sparkles className="w-5 h-5 text-white" />
</div>
<h1 className="text-3xl font-semibold text-gray-900">我的专家</h1>
</div>
<button
onClick={handleCreateExpert}
disabled={isCreating}
className="inline-flex items-center gap-2 px-5 py-2.5 bg-gray-900 dark-theme-btn text-white rounded-xl text-sm font-medium hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isCreating ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Plus className="w-4 h-4" />
)}
创建专家
</button>
</div>
<p className="text-gray-500 mb-6">管理你的专属 AI 专家助手</p>
{/* 标签切换 */}
<div className="flex items-center gap-2">
<button
onClick={() => {
setActiveTab("mine")
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "mine"
? "bg-gray-900 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
我的专家
</button>
<button
onClick={() => {
setActiveTab("published");
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "published"
? "bg-gray-900 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
发布的专家
</button>
</div>
</div>
</div>
{/* Main content */}
<div className="px-8 py-12">
<div className="max-w-7xl mx-auto">
{isLoading ? (
<LoadingSkeleton />
) : isError ? (
<div className="text-center py-16 text-red-500">
<p>加载失败: {"未知错误"}</p>
</div>
) : experts.length === 0 ? (
<div className="text-center py-16">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-100 flex items-center justify-center">
<Bot className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
{activeTab === "mine" ? "暂无我的专家" : "暂无已发布的专家"}
</h3>
<p className="text-gray-500 mb-6">
{activeTab === "mine"
? "创建你的第一个专家助手,开始高效工作"
: "还没有专家发布到广场"}
</p>
{activeTab === "mine" && (
<button
onClick={handleCreateExpert}
disabled={isCreating}
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 text-white rounded-xl font-medium hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isCreating ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Plus className="w-4 h-4" />
)}
创建第一个专家
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{experts.map((expert, index) => (
<div
key={expert.id}
>
<ExpertCard expert={expert} changeState={expertAgentSearh} />
</div>
))}
</div>
)}
</div>
</div>
{/* Quick start section */}
<div
className="px-8 pb-12"
>
<div className="max-w-7xl mx-auto">
<div className="rounded-2xl border border-gray-100 bg-gradient-to-br dark-item-card from-violet-50 to-purple-50 p-8">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
不确定从哪个开始?
</h3>
<p className="text-gray-600">
直接开始对话,AI 会自动匹配最合适的专家助手
</p>
</div>
<div
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-violet-500 to-purple-500 text-white rounded-xl font-medium hover:from-violet-600 hover:to-purple-600 hover:shadow-lg hover:shadow-violet-500/25 transition-all duration-300"
>
开始对话
<ArrowRight className="w-4 h-4" />
</div>
</div>
</div>
</div>
</div>
</div>
);
}
+636
View File
@@ -0,0 +1,636 @@
import React, { useState } from 'react';
import {
Send,
Search,
History,
Bot,
Sparkles,
Database,
Layers,
FileText,
Bookmark,
ChevronRight,
Cpu,
Target,
MessageSquare,
Zap,
CheckCircle,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
interface Agent {
id: string;
name: string;
category: string;
tags: string[];
description: string;
skills: string[];
dataSources: string[];
functions: string[];
outputFields: string[];
}
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
agentName?: string;
skills?: string[];
dataSources?: string[];
functions?: string[];
structuredOutput?: Record<string, any>;
}
const AgentQA: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 模拟智能体列表
const agents: Agent[] = [
{
id: 'audience-agent',
name: 'AudienceAgent',
category: '受众分析',
tags: ['受众分层', '画像抽取', '行为预测'],
description: '专业的受众分析智能体,能够识别目标受众特征并生成画像',
skills: ['受众分层', '核心受众识别', '画像抽取', '行为预测'],
dataSources: ['知识库', 'Y系统', 'A系统'],
functions: ['信息抽取器', '标签聚类器', '结构化输出器'],
outputFields: ['core_audience', 'key_traits', 'barriers', 'opportunities'],
},
{
id: 'brand-agent',
name: 'BrandAgent',
category: '品牌分析',
tags: ['品牌定位', '传播策略', '调性分析'],
description: '专注品牌建设与传播策略的智能体',
skills: ['品牌定位分析', '传播路径规划', '品牌调性设计'],
dataSources: ['知识库', '行业数据库'],
functions: ['品牌诊断器', '策略生成器'],
outputFields: ['brand_position', 'communication_path', 'tone_guide'],
},
{
id: 'content-agent',
name: 'ContentAgent',
category: '内容创作',
tags: ['文案撰写', '创意生成', '多形式内容'],
description: '智能内容创作助手,支持多种形式的内容生成',
skills: ['文案撰写', '创意生成', '多形式内容', 'A/B测试文案'],
dataSources: ['知识库', '优秀案例库'],
functions: ['文案生成器', '风格适配器', '质量评估器'],
outputFields: ['headlines', 'body_copy', 'call_to_action', 'variants'],
},
{
id: 'channel-agent',
name: 'ChannelAgent',
category: '渠道规划',
tags: ['渠道选择', '投放策略', '效果评估'],
description: '专业的渠道分析与规划智能体',
skills: ['渠道选择', '投放策略规划', '效果评估', '预算分配'],
dataSources: ['Y系统', 'A系统', '行业数据'],
functions: ['渠道分析器', '投放优化器', 'ROI预测器'],
outputFields: ['recommended_channels', 'budget_allocation', 'expected_roi'],
},
];
// 状态
const [selectedAgent, setSelectedAgent] = useState<Agent>(agents[0]);
const [searchQuery, setSearchQuery] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
// 过滤智能体
const filteredAgents = agents.filter(agent =>
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agent.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
agent.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
);
// 处理发送消息
const handleSend = () => {
if (!inputValue.trim()) return;
setIsLoading(true);
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date(),
};
setMessages([...messages, userMessage]);
setInputValue('');
// 模拟AI响应
setTimeout(() => {
const response: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `基于${selectedAgent.name}的专业能力,我来为您分析:\n\n根据您的问题,我识别到核心受众特征如下:\n\n**核心受众识别**:\n- 主要年龄段:25-35岁\n- 职业特征:都市白领\n- 消费能力:中高端\n- 关注点:品质生活、个人成长\n\n**关键特质**:\n1. 追求个性化表达\n2. 注重社交认同\n3. 愿意为品质付费\n\n**可能存在的阻力**:\n- 价格敏感度较高\n- 对品牌忠诚度有限\n\n以上分析是否满足您的需求?如需调整维度,请继续补充信息。`,
timestamp: new Date(),
agentName: selectedAgent.name,
skills: selectedAgent.skills,
dataSources: selectedAgent.dataSources,
functions: selectedAgent.functions,
structuredOutput: {
core_audience: '25-35岁都市白领',
key_traits: ['追求个性化', '注重社交认同', '品质导向'],
barriers: ['价格敏感', '品牌忠诚度有限'],
opportunities: ['社交媒体影响大', '口碑传播效果好'],
},
};
setMessages(prev => [...prev, response]);
setIsLoading(false);
}, 1500);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleSaveRecord = () => {
alert('问答记录已保存');
};
const handleCollect = () => {
alert('已添加到收藏');
};
return (
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
{/* 顶部工具栏 */}
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className="flex items-center gap-4">
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
智能体问答
</h1>
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
当前智能体:
</span>
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-red-600'}`}>
{selectedAgent.name}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onNavigate('qa-records')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isDark
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<History className="w-4 h-4" />
历史记录
</button>
</div>
</div>
{/* 主体内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧智能体区 */}
<div className={`w-72 border-r flex-shrink-0 overflow-y-auto ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'}`}>
{/* 智能体搜索 */}
<div className="p-4">
<div className={`relative ${isDark ? 'bg-slate-700/50' : 'bg-white'} rounded-lg`}>
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索智能体..."
className={`w-full pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none ${
isDark
? 'bg-transparent text-white placeholder-slate-500'
: 'bg-transparent text-gray-800 placeholder-gray-400'
}`}
/>
</div>
</div>
{/* 智能体列表 */}
<div className="px-4 pb-4">
<h3 className={`text-xs font-medium uppercase tracking-wider mb-3 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
智能体列表
</h3>
<div className="space-y-2">
{filteredAgents.map((agent) => (
<button
key={agent.id}
onClick={() => setSelectedAgent(agent)}
className={`w-full text-left p-3 rounded-lg transition-all ${
selectedAgent.id === agent.id
? isDark
? 'bg-blue-600/20 border border-blue-500/50'
: 'bg-red-50 border border-red-200'
: isDark
? 'bg-slate-700/30 border border-transparent hover:bg-slate-700/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
selectedAgent.id === agent.id
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-600 text-slate-300'
: 'bg-gray-100 text-gray-600'
}`}>
<Bot className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-medium truncate ${isDark ? 'text-white' : 'text-gray-900'}`}>
{agent.name}
</div>
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{agent.category}
</div>
</div>
<ChevronRight className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
</div>
</button>
))}
</div>
</div>
{/* 当前智能体能力摘要 */}
<div className={`p-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<h3 className={`text-xs font-medium uppercase tracking-wider mb-3 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
能力摘要
</h3>
{/* 核心职责 */}
<div className="mb-4">
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Target className="w-4 h-4" />
<span className="text-xs">核心职责</span>
</div>
<p className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{selectedAgent.description}
</p>
</div>
{/* 已启用技能 */}
<div className="mb-4">
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Sparkles className="w-4 h-4" />
<span className="text-xs">已启用技能</span>
</div>
<div className="flex flex-wrap gap-1">
{selectedAgent.skills.map((skill, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}
>
{skill}
</span>
))}
</div>
</div>
{/* 可访问数据源 */}
<div className="mb-4">
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Database className="w-4 h-4" />
<span className="text-xs">可访问数据源</span>
</div>
<div className="flex flex-wrap gap-1">
{selectedAgent.dataSources.map((source, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
}`}
>
{source}
</span>
))}
</div>
</div>
{/* 可调用功能 */}
<div>
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Layers className="w-4 h-4" />
<span className="text-xs">可调用功能</span>
</div>
<div className="flex flex-wrap gap-1">
{selectedAgent.functions.map((func, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}
>
{func}
</span>
))}
</div>
</div>
</div>
</div>
{/* 中间对话区 */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<Bot className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">开始与{selectedAgent.name}对话</p>
<p className="text-sm mt-2">基于智能体专业能力获取精准回答</p>
</div>
) : (
messages.map((msg) => (
<div key={msg.id} className="space-y-3">
{/* 用户消息 */}
<div className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
{msg.role === 'assistant' && (
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
}`}>
<Bot className="w-4 h-4" />
</div>
)}
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${
msg.role === 'user'
? isDark
? 'bg-blue-600/30 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-200'
: 'bg-gray-100 text-gray-800'
}`}>
<p className="whitespace-pre-wrap">{msg.content}</p>
</div>
{msg.role === 'user' && (
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-200 text-gray-600'
}`}>
<div className="w-4 h-4 rounded-full bg-current" />
</div>
)}
</div>
{/* 智能体执行详情 */}
{msg.role === 'assistant' && (
<div className={`ml-11 p-3 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-gray-50'}`}>
{/* 本轮启用技能 */}
<div className="mb-3">
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
本轮启用技能
</h4>
<div className="flex flex-wrap gap-1">
{msg.skills?.map((skill, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}
>
{skill}
</span>
))}
</div>
</div>
{/* 本轮访问数据源 */}
<div className="mb-3">
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
本轮访问数据源
</h4>
<div className="flex flex-wrap gap-1">
{msg.dataSources?.map((source, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
}`}
>
{source}
</span>
))}
</div>
</div>
{/* 本轮调用功能 */}
<div className="mb-3">
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
本轮调用功能
</h4>
<div className="flex flex-wrap gap-1">
{msg.functions?.map((func, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}
>
{func}
</span>
))}
</div>
</div>
{/* 结构化输出 */}
{msg.structuredOutput && (
<div>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
结构化输出
</h4>
<div className={`p-3 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
{Object.entries(msg.structuredOutput).map(([key, value]) => (
<div key={key} className="flex items-start gap-2 mb-1 last:mb-0">
<span className={`text-xs font-mono ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
{key}:
</span>
<span className={`text-xs ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{Array.isArray(value) ? value.join(', ') : String(value)}
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
))
)}
{isLoading && (
<div className="flex gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
}`}>
<Bot className="w-4 h-4" />
</div>
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<div className="flex gap-1">
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '0ms' }} />
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '150ms' }} />
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
)}
</div>
{/* 底部输入区 */}
<div className={`border-t p-4 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className={`flex items-end gap-3 rounded-xl p-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入问题或任务要求..."
rows={1}
className={`flex-1 bg-transparent resize-none focus:outline-none ${
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
}`}
/>
<div className="flex items-center gap-2">
<button
onClick={handleSaveRecord}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="保存记录"
>
<Bookmark className="w-5 h-5" />
</button>
<button
onClick={handleCollect}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="加入收藏"
>
<Bookmark className="w-5 h-5" />
</button>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isLoading}
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
inputValue.trim() && !isLoading
? isDark
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
: isDark
? 'bg-slate-600 text-slate-400'
: 'bg-gray-300 text-gray-500'
}`}
>
<Send className="w-4 h-4" />
发送
</button>
</div>
</div>
</div>
</div>
{/* 右侧执行详情区 */}
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
<h3 className={`text-sm font-medium mb-4 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
执行详情
</h3>
{/* 本轮启用技能 */}
<div className="mb-6">
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Sparkles className="w-4 h-4" />
<span className="text-xs font-medium">本轮启用技能</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.skills.map((skill, idx) => (
<div key={idx} className="flex items-center gap-2">
<CheckCircle className={`w-3 h-3 ${isDark ? 'text-blue-400' : 'text-green-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{skill}</span>
</div>
))}
</div>
</div>
</div>
{/* 本轮访问数据源 */}
<div className="mb-6">
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Database className="w-4 h-4" />
<span className="text-xs font-medium">本轮访问数据源</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.dataSources.map((source, idx) => (
<div key={idx} className="flex items-center gap-2">
<Database className={`w-3 h-3 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{source}</span>
</div>
))}
</div>
</div>
</div>
{/* 本轮调用功能 */}
<div className="mb-6">
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Layers className="w-4 h-4" />
<span className="text-xs font-medium">本轮调用功能</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.functions.map((func, idx) => (
<div key={idx} className="flex items-center gap-2">
<Cpu className={`w-3 h-3 ${isDark ? 'text-purple-400' : 'text-purple-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{func}</span>
</div>
))}
</div>
</div>
</div>
{/* 结构化输出字段 */}
<div>
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<FileText className="w-4 h-4" />
<span className="text-xs font-medium">输出字段</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.outputFields.map((field, idx) => (
<div key={idx} className="flex items-center gap-2">
<Zap className={`w-3 h-3 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
<span className={`text-xs font-mono ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>{field}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default AgentQA;
+532
View File
@@ -0,0 +1,532 @@
import React, { useState } from 'react';
import {
Bot,
PlayCircle,
ArrowLeft,
Send,
Clock,
CheckCircle,
AlertCircle,
FileText,
Sparkles,
User,
Settings,
Zap,
BookOpen,
Lightbulb,
Target,
Users,
MessageSquare,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
interface AgentTrialProps {
onNavigate: (page: PageType) => void;
}
// 单智能体能力卡片
const agentCapabilities = [
{
id: 'goal',
name: 'GoalAgent',
role: '目标分析专家',
description: '分析业务目标,提取核心KPI和成功标准',
icon: Target,
color: 'blue',
skills: ['目标分解', 'KPI提取', '优先级排序'],
example: '分析这次五四青年节宣传的核心目标是什么?',
},
{
id: 'audience',
name: 'AudienceAgent',
role: '受众分析专家',
description: '定义目标受众画像,分析用户特征和需求',
icon: Users,
color: 'cyan',
skills: ['用户画像', '行为分析', '需求挖掘'],
example: '谁是我们的核心受众?他们的特征是什么?',
},
{
id: 'behavior',
name: 'BehaviorAgent',
role: '行为设计专家',
description: '设计用户行为路径和转化漏斗',
icon: Sparkles,
color: 'green',
skills: ['行为设计', '转化路径', '激励策略'],
example: '我们希望用户产生什么样的行为?',
},
{
id: 'driver',
name: 'DriverAgent',
role: '驱动因素专家',
description: '分析影响用户决策的核心驱动因素',
icon: Lightbulb,
color: 'yellow',
skills: ['驱动分析', '动机挖掘', '阻力识别'],
example: '什么因素会驱动用户做出我们期望的行为?',
},
{
id: 'narrative',
name: 'NarrativeAgent',
role: '叙事策略专家',
description: '设计品牌叙事框架和内容策略',
icon: MessageSquare,
color: 'purple',
skills: ['故事设计', '内容策略', '情感共鸣'],
example: '我们应该如何讲述品牌故事?',
},
{
id: 'risk',
name: 'RiskAgent',
role: '风险评估专家',
description: '识别项目风险,制定应对策略',
icon: AlertCircle,
color: 'red',
skills: ['风险识别', '影响评估', '应对策略'],
example: '这个方案有哪些潜在风险?',
},
];
const AgentTrial: React.FC<AgentTrialProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 主题颜色类
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-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 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 btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
const [inputText, setInputText] = useState('');
const [isRunning, setIsRunning] = useState(false);
const [conversation, setConversation] = useState<Array<{ role: 'user' | 'agent'; content: string; time: string }>>([]);
const currentAgent = agentCapabilities.find((a) => a.id === selectedAgent);
const handleSelectAgent = (agentId: string) => {
setSelectedAgent(agentId);
const agent = agentCapabilities.find((a) => a.id === agentId);
if (agent) {
setInputText(agent.example);
}
};
const handleSend = async () => {
if (!inputText.trim() || !currentAgent) return;
const now = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
// 添加用户消息
setConversation((prev) => [...prev, { role: 'user', content: inputText, time: now }]);
const userInput = inputText;
setInputText('');
setIsRunning(true);
// 模拟智能体回复
setTimeout(() => {
const responseTime = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
let response = '';
switch (currentAgent.id) {
case 'goal':
response = `【目标分析完成】
基于您的描述,我分析出以下核心目标:
1. **主目标**:提升品牌在Z世代用户中的认知度
2. **次目标**:建立品牌年轻化形象,增强情感连接
3. **量化指标**:
- 曝光量:500万+
- 互动率:5%+
- UGC数量:1000+
建议优先聚焦主目标,确保资源集中。`;
break;
case 'audience':
response = `【受众分析完成】
核心受众画像:
**人口特征**
- 年龄:18-25岁
- 身份:学生、基层青年工作者
- 地域:一二线城市为主
**行为特征**
- 社交媒体重度用户
- 短视频爱好者
- 注重真实性和态度
**核心需求**
- 社交认同与自我表达
- 归属感与群体认同
- 性价比与实用性`;
break;
case 'behavior':
response = `【行为设计完成】
目标行为路径:
浏览 → 点赞 → 评论 → 转发 → 创作
**关键转化节点**
1. 引起注意:内容需有话题性
2. 激发互动:设置讨论话题
3. 促进创作:提供UGC模板和激励
**激励机制建议**
- 虚拟勋章/积分
- 优质内容曝光
- 创作者社群归属`;
break;
case 'driver':
response = `【驱动因素分析完成】
核心驱动因素:
1. **社交认同需求**
- 年轻人渴望被关注和认可
- UGC是自我表达的重要方式
2. **群体归属感**
- 青年节与年轻人天然关联
- 参与活动获得身份认同
3. **表达欲驱动**
- 有态度的品牌内容激发创作
- 提供创作框架降低门槛
**主要阻力**
- 注意力的稀缺
- 创作成本感知`;
break;
case 'narrative':
response = `【叙事策略完成】
核心叙事框架:
**主题**:真实、有态度的青年故事
**叙事结构**
1. 讲述真实的人物故事
2. 引发情感共鸣
3. 传递品牌价值观
4. 激发用户参与
**内容类型**
- 人物专访型
- 事件记录型
- 观点表达型
**关键要素**
- 真实感 > 精致感
- 态度感 > 说教感
- 参与感 > 观看感`;
break;
case 'risk':
response = `【风险评估完成】
识别的风险项:
**高风险**
1. 内容质量不稳定
- 风险:UGC质量参差不齐
- 对策:建立内容审核机制
**中风险**
2. 预算控制难度大
- 风险:投放效果不可控
- 对策:分阶段预算分配
3. 青年群体注意力分散
- 风险:话题热度难维持
- 对策:多波次内容规划
**建议**:重点关注内容质量风险`;
break;
default:
response = '正在分析中...';
}
setConversation((prev) => [...prev, { role: 'agent', content: response, time: responseTime }]);
setIsRunning(false);
}, 2000);
};
return (
<div className="space-y-6">
{/* 顶部操作栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => onNavigate('agent-list')}
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-red-200'}`} />
<div>
<h1 className={`${textPrimary} font-bold text-lg`}>单智能体试用</h1>
<p className={`${textSecondary} text-sm`}>直接调用单个智能体进行专项分析</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`px-3 py-1.5 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700'} rounded-full text-sm font-medium flex items-center gap-1.5`}>
<Bot className="w-4 h-4" />
能力调用模式
</span>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-6">
{/* 左侧:智能体选择区 */}
<div className="col-span-4">
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
<Bot className={`w-4 h-4 ${isDark ? 'text-purple-400' : 'text-purple-600'}`} />
选择智能体
</h3>
<div className="space-y-3">
{agentCapabilities.map((agent) => {
const Icon = agent.icon;
const isSelected = selectedAgent === agent.id;
return (
<button
key={agent.id}
onClick={() => handleSelectAgent(agent.id)}
className={`w-full p-4 rounded-xl border transition-all text-left ${
isSelected
? `${isDark ? 'bg-blue-500/20 border-blue-500/50' : 'bg-red-50 border-red-300'}`
: `${inputBg} ${cardBorder} hover:${isDark ? 'border-blue-500/30' : 'border-red-400'}`
}`}
>
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center flex-shrink-0`}>
<Icon className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
</div>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium ${isSelected ? (isDark ? 'text-blue-400' : 'text-red-600') : textPrimary}`}>
{agent.name}
</p>
<p className={`${textSecondary} text-xs mt-0.5`}>{agent.role}</p>
<p className={`${textMuted} text-xs mt-1 line-clamp-2`}>{agent.description}</p>
</div>
</div>
{isSelected && (
<div className={`mt-3 pt-3 border-t ${isDark ? 'border-blue-500/30' : 'border-red-300'}`}>
<p className={`text-xs ${textSecondary} mb-2`}>核心技能</p>
<div className="flex flex-wrap gap-1">
{agent.skills.map((skill, i) => (
<span
key={i}
className={`px-2 py-0.5 ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600'} rounded text-xs`}
>
{skill}
</span>
))}
</div>
</div>
)}
</button>
);
})}
</div>
</div>
{/* 使用说明 */}
<div className={`mt-4 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold text-sm mb-3 flex items-center gap-2`}>
<BookOpen className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-blue-600'}`} />
使用说明
</h3>
<div className={`space-y-2 text-xs ${textSecondary}`}>
<div className="flex items-start gap-2">
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>1.</span>
<p>选择一个智能体进入专项分析模式</p>
</div>
<div className="flex items-start gap-2">
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>2.</span>
<p>输入您的问题或任务要求</p>
</div>
<div className="flex items-start gap-2">
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>3.</span>
<p>智能体将基于其专业能力给出分析结果</p>
</div>
<div className="flex items-start gap-2">
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>4.</span>
<p>结果可用于后续场景任务或直接参考</p>
</div>
</div>
</div>
</div>
{/* 右侧:对话区 */}
<div className="col-span-8">
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl flex flex-col h-[calc(100vh-220px)]`}>
{selectedAgent ? (
<>
{/* 顶部智能体信息 */}
<div className={`p-4 border-b ${cardBorder}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{currentAgent && (
<>
<div className={`w-10 h-10 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center`}>
<currentAgent.icon className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
</div>
<div>
<p className={`${textPrimary} font-medium`}>{currentAgent.name}</p>
<p className={`${textSecondary} text-xs`}>{currentAgent.role}</p>
</div>
</>
)}
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700'} rounded text-xs flex items-center gap-1`}>
<Zap className="w-3 h-3" />
独立运行中
</span>
</div>
</div>
</div>
{/* 对话区域 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{conversation.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className={`w-16 h-16 rounded-full ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center mb-4`}>
<Bot className={`w-8 h-8 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
</div>
<p className={`${textPrimary} font-medium mb-2`}>开始与 {currentAgent?.name} 对话</p>
<p className={`${textSecondary} text-sm max-w-md`}>
{currentAgent?.description}
<br />
请在下方输入您的问题或任务要求
</p>
</div>
) : (
conversation.map((msg, i) => (
<div
key={i}
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
msg.role === 'user'
? (isDark ? 'bg-blue-500/20' : 'bg-red-100')
: (isDark ? 'bg-blue-500/20' : 'bg-red-100')
}`}
>
{msg.role === 'user' ? (
<User className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
) : (
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
)}
</div>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
msg.role === 'user'
? (isDark ? 'bg-blue-600 text-white' : 'bg-red-600 text-white')
: `${inputBg} ${inputBorder}`
}`}
>
<div
className={`text-xs mb-1 ${
msg.role === 'user' ? (isDark ? 'text-blue-200' : 'text-red-200') : textMuted
}`}
>
{msg.role === 'user' ? '您' : currentAgent?.name} · {msg.time}
</div>
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
</div>
</div>
))
)}
{isRunning && (
<div className="flex gap-3">
<div className={`w-8 h-8 rounded-full ${isDark ? 'bg-green-500/20' : 'bg-green-100'} flex items-center justify-center`}>
<Bot className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
</div>
<div className={`${inputBg} ${inputBorder} rounded-2xl px-4 py-3`}>
<div className={`flex items-center gap-2 ${textSecondary} text-sm`}>
<div className={`w-2 h-2 ${isDark ? 'bg-green-400' : 'bg-green-500'} rounded-full animate-pulse`} />
{currentAgent?.name} 正在分析中...
</div>
</div>
</div>
)}
</div>
{/* 输入区域 */}
<div className={`p-4 border-t ${cardBorder}`}>
<div className="flex gap-3">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
placeholder={`输入您要咨询 ${currentAgent?.name} 的问题...`}
rows={3}
className={`flex-1 px-4 py-3 ${inputBg} ${inputBorder} rounded-xl ${textPrimary} text-sm ${textMuted} resize-none focus:outline-none focus:${isDark ? 'border-blue-500/50' : 'border-red-400'}`}
/>
<div className="flex flex-col gap-2">
<button
onClick={handleSend}
disabled={!inputText.trim() || isRunning}
className={`px-6 py-3 ${btnPrimary} ${isDark ? 'disabled:bg-slate-700 disabled:text-slate-500' : 'disabled:bg-gray-300 disabled:text-gray-500'} text-white rounded-xl text-sm font-medium transition-colors flex items-center gap-2`}
>
<Send className="w-4 h-4" />
发送
</button>
<button
onClick={() => {
setConversation([]);
setInputText('');
}}
className={`px-4 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
>
清空
</button>
</div>
</div>
<p className={`${textMuted} text-xs mt-2`}>
按 Enter 发送,Shift+Enter 换行
</p>
</div>
</>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center p-8">
<div className={`w-20 h-20 rounded-full ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center mb-6`}>
<PlayCircle className={`w-10 h-10 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
</div>
<h3 className={`${textPrimary} font-semibold text-lg mb-2`}>选择智能体开始试用</h3>
<p className={`${textSecondary} text-sm max-w-md`}>
左侧选择一个智能体,即可进入单智能体对话模式。
<br />
该模式适合进行专项分析、快速验证或测试单个智能体能力。
</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default AgentTrial;
@@ -0,0 +1,900 @@
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
Scale,
Search,
Plus,
Edit,
Trash2,
Download,
Filter,
Folder,
FolderOpen,
ChevronRight,
TrendingUp,
TrendingDown,
Minus,
Target,
CheckCircle,
AlertCircle,
Clock,
Star,
BarChart3,
PieChart,
Activity,
Settings,
Copy,
Link2,
FileText,
User,
Calendar,
} from 'lucide-react';
import { PageType } from '../components/Sidebar';
interface EvaluationIndex {
id: string;
name: string;
category: string;
type: 'quantitative' | 'qualitative' | 'composite';
weight: number;
description: string;
calculationMethod?: string;
dataSource?: string;
updateFrequency: 'realtime' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly';
status: 'active' | 'draft' | 'deprecated';
createdAt: string;
updatedAt: string;
author: string;
tags: string[];
baseline?: number;
target?: number;
currentValue?: number;
trend: 'up' | 'down' | 'stable';
relatedIndices: string[];
}
interface EvaluationSystem {
id: string;
name: string;
description: string;
category: string;
indices: string[];
status: 'active' | 'draft' | 'archived';
createdAt: string;
updatedAt: string;
author: string;
}
interface EvaluationIndexResourceManagementProps {
onNavigate: (page: PageType) => void;
}
const mockIndices: EvaluationIndex[] = [
{
id: 'idx-001',
name: '网络影响力指数',
category: '影响力评估',
type: 'composite',
weight: 0.25,
description: '综合评估目标在网络空间的影响力水平',
calculationMethod: '加权平均法',
dataSource: '社交媒体数据、新闻报道量',
updateFrequency: 'daily',
status: 'active',
createdAt: '2023-06-15',
updatedAt: '2024-03-10',
author: '评估组A',
tags: ['影响力', '网络', '综合'],
baseline: 50,
target: 80,
currentValue: 72,
trend: 'up',
relatedIndices: ['idx-002', 'idx-003'],
},
{
id: 'idx-002',
name: '传播范围指数',
category: '传播效果',
type: 'quantitative',
weight: 0.20,
description: '衡量信息在网络中的传播覆盖范围',
calculationMethod: '触达人数 × 地域覆盖率',
dataSource: '平台API数据',
updateFrequency: 'realtime',
status: 'active',
createdAt: '2023-07-20',
updatedAt: '2024-03-12',
author: '评估组B',
tags: ['传播', '覆盖', '定量'],
baseline: 10000,
target: 50000,
currentValue: 42500,
trend: 'up',
relatedIndices: ['idx-001'],
},
{
id: 'idx-003',
name: '舆论倾向指数',
category: '舆情分析',
type: 'qualitative',
weight: 0.30,
description: '评估网络舆论对目标的整体态度倾向',
calculationMethod: '情感分析模型',
dataSource: '社交媒体评论、新闻评论区',
updateFrequency: 'hourly',
status: 'active',
createdAt: '2023-08-10',
updatedAt: '2024-03-11',
author: '评估组A',
tags: ['舆情', '情感', '定性'],
baseline: 0,
target: 75,
currentValue: 68,
trend: 'stable',
relatedIndices: ['idx-001', 'idx-004'],
},
{
id: 'idx-004',
name: '响应时效指数',
category: '效能评估',
type: 'quantitative',
weight: 0.15,
description: '评估对网络事件的响应速度',
calculationMethod: '平均响应时间 (小时)',
dataSource: '事件追踪系统',
updateFrequency: 'weekly',
status: 'active',
createdAt: '2023-09-05',
updatedAt: '2024-03-08',
author: '评估组C',
tags: ['时效', '效能', '定量'],
baseline: 24,
target: 4,
currentValue: 6,
trend: 'down',
relatedIndices: ['idx-005'],
},
{
id: 'idx-005',
name: '协调联动指数',
category: '协作效能',
type: 'composite',
weight: 0.10,
description: '评估多部门协作的顺畅程度',
calculationMethod: '任务完成率 × 沟通效率',
dataSource: '协同办公系统',
updateFrequency: 'monthly',
status: 'draft',
createdAt: '2024-01-15',
updatedAt: '2024-03-05',
author: '评估组D',
tags: ['协作', '联动', '综合'],
trend: 'stable',
relatedIndices: ['idx-004'],
},
{
id: 'idx-006',
name: '风险预警准确率',
category: '预警效能',
type: 'quantitative',
weight: 0.25,
description: '风险预警的准确程度',
calculationMethod: '准确预警数 / 总预警数',
dataSource: '预警系统日志',
updateFrequency: 'monthly',
status: 'active',
createdAt: '2023-10-20',
updatedAt: '2024-02-28',
author: '评估组A',
tags: ['预警', '准确率', '定量'],
baseline: 60,
target: 90,
currentValue: 82,
trend: 'up',
relatedIndices: [],
},
{
id: 'idx-007',
name: '策略有效性指数',
category: '策略评估',
type: 'composite',
weight: 0.30,
description: '评估所采取措施的有效性',
calculationMethod: '多维度加权评估',
dataSource: '策略执行数据、效果追踪',
updateFrequency: 'quarterly',
status: 'active',
createdAt: '2023-11-10',
updatedAt: '2024-03-01',
author: '评估组B',
tags: ['策略', '效果', '综合'],
baseline: 50,
target: 85,
currentValue: 78,
trend: 'up',
relatedIndices: ['idx-001', 'idx-003'],
},
{
id: 'idx-008',
name: '信息完整度',
category: '数据质量',
type: 'quantitative',
weight: 0.10,
description: '目标画像信息的完整程度',
calculationMethod: '已填写字段数 / 总字段数',
dataSource: '画像数据库',
updateFrequency: 'weekly',
status: 'deprecated',
createdAt: '2023-05-01',
updatedAt: '2024-01-15',
author: '评估组C',
tags: ['数据', '完整度', '定量'],
trend: 'stable',
relatedIndices: [],
},
];
const mockSystems: EvaluationSystem[] = [
{
id: 'sys-001',
name: '网络态势综合评估体系',
description: '用于评估网络空间整体态势的综合指标体系',
category: '态势评估',
indices: ['idx-001', 'idx-002', 'idx-003'],
status: 'active',
createdAt: '2023-06-01',
updatedAt: '2024-03-10',
author: '评估部',
},
{
id: 'sys-002',
name: '响应效能评估体系',
description: '评估应急响应和处置效能的指标体系',
category: '效能评估',
indices: ['idx-004', 'idx-005'],
status: 'active',
createdAt: '2023-09-01',
updatedAt: '2024-03-08',
author: '效能组',
},
];
const typeLabels: Record<EvaluationIndex['type'], string> = {
quantitative: '定量',
qualitative: '定性',
composite: '综合',
};
const typeColors: Record<EvaluationIndex['type'], string> = {
quantitative: 'bg-blue-500/20 text-blue-400',
qualitative: 'bg-purple-500/20 text-purple-400',
composite: 'bg-orange-500/20 text-orange-400',
};
const statusColors: Record<EvaluationIndex['status'], { bg: string; text: string; label: string }> = {
active: { bg: 'bg-green-500/20', text: 'text-green-400', label: '使用中' },
draft: { bg: 'bg-yellow-500/20', text: 'text-yellow-400', label: '草稿' },
deprecated: { bg: 'bg-gray-500/20', text: 'text-gray-400', label: '已废弃' },
};
const frequencyLabels: Record<EvaluationIndex['updateFrequency'], string> = {
realtime: '实时',
hourly: '每小时',
daily: '每日',
weekly: '每周',
monthly: '每月',
quarterly: '每季度',
};
const trendIcons: Record<EvaluationIndex['trend'], React.ReactNode> = {
up: <TrendingUp className="w-4 h-4 text-green-400" />,
down: <TrendingDown className="w-4 h-4 text-red-400" />,
stable: <Minus className="w-4 h-4 text-gray-400" />,
};
const categories = ['全部', '影响力评估', '传播效果', '舆情分析', '效能评估', '协作效能', '预警效能', '策略评估', '数据质量'];
const EvaluationIndexResourceManagement: React.FC<EvaluationIndexResourceManagementProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [viewMode, setViewMode] = useState<'index' | 'system'>('index');
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('全部');
const [selectedType, setSelectedType] = useState<string>('all');
const [selectedStatus, setSelectedStatus] = useState<string>('all');
const [selectedIndex, setSelectedIndex] = useState<EvaluationIndex | null>(null);
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set(['影响力评估', '传播效果', '舆情分析']));
const filteredIndices = mockIndices.filter(index => {
const matchesSearch = index.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
index.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
index.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCategory = selectedCategory === '全部' || index.category === selectedCategory;
const matchesType = selectedType === 'all' || index.type === selectedType;
const matchesStatus = selectedStatus === 'all' || index.status === selectedStatus;
return matchesSearch && matchesCategory && matchesType && matchesStatus;
});
const stats = {
total: mockIndices.length,
active: mockIndices.filter(i => i.status === 'active').length,
draft: mockIndices.filter(i => i.status === 'draft').length,
deprecated: mockIndices.filter(i => i.status === 'deprecated').length,
};
const groupedIndices = filteredIndices.reduce((acc, index) => {
if (!acc[index.category]) {
acc[index.category] = [];
}
acc[index.category].push(index);
return acc;
}, {} as Record<string, EvaluationIndex[]>);
const toggleCategory = (category: string) => {
setExpandedCategories(prev => {
const next = new Set(prev);
if (next.has(category)) {
next.delete(category);
} else {
next.add(category);
}
return next;
});
};
const calculateProgress = (index: EvaluationIndex) => {
if (!index.baseline || !index.target || !index.currentValue) return null;
const progress = ((index.currentValue - index.baseline) / (index.target - index.baseline)) * 100;
return Math.min(Math.max(progress, 0), 100);
};
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-amber-500/20' : 'bg-amber-100'}`}>
<Scale className={`w-6 h-6 ${isDark ? 'text-amber-400' : 'text-amber-600'}`} />
</div>
<div>
<h1 className={`text-2xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
评估指标体系资源管理
</h1>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
管理和维护评估指标库与指标体系
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
isDark
? 'bg-slate-700 hover:bg-slate-600 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<Download className="w-4 h-4" />
导出
</button>
<button
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
isDark
? 'bg-amber-600 hover:bg-amber-500 text-white'
: 'bg-amber-500 hover:bg-amber-600 text-white'
}`}
>
<Plus className="w-4 h-4" />
添加指标
</button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-amber-800/30' : 'border-amber-100'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>指标总数</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.total}</p>
</div>
<div className={`p-3 rounded-lg ${isDark ? 'bg-amber-500/20' : 'bg-amber-50'}`}>
<Scale className={`w-6 h-6 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-green-800/30' : 'border-green-100'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>使用中</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.active}</p>
</div>
<div className={`p-3 rounded-lg bg-green-500/20`}>
<CheckCircle className="w-6 h-6 text-green-400" />
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-yellow-800/30' : 'border-yellow-100'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>草稿</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.draft}</p>
</div>
<div className={`p-3 rounded-lg bg-yellow-500/20`}>
<Edit className="w-6 h-6 text-yellow-400" />
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>已废弃</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.deprecated}</p>
</div>
<div className={`p-3 rounded-lg bg-gray-500/20`}>
<AlertCircle className="w-6 h-6 text-gray-400" />
</div>
</div>
</div>
</div>
{/* 视图切换和筛选栏 */}
<div className="space-y-4">
{/* 视图切换 */}
<div className={`flex items-center rounded-lg p-1 w-fit ${isDark ? 'bg-slate-800/50' : 'bg-gray-100'}`}>
<button
onClick={() => setViewMode('index')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
viewMode === 'index'
? isDark ? 'bg-amber-500/20 text-amber-400' : 'bg-white text-amber-600 shadow-sm'
: isDark ? 'text-slate-400' : 'text-gray-500'
}`}
>
<Scale className="w-4 h-4 inline mr-2" />
指标管理
</button>
<button
onClick={() => setViewMode('system')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
viewMode === 'system'
? isDark ? 'bg-amber-500/20 text-amber-400' : 'bg-white text-amber-600 shadow-sm'
: isDark ? 'text-slate-400' : 'text-gray-500'
}`}
>
<BarChart3 className="w-4 h-4 inline mr-2" />
体系管理
</button>
</div>
{/* 筛选栏 */}
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-amber-800/30' : 'border-amber-100'}`}>
<div className="flex items-center gap-4 flex-wrap">
{/* 搜索框 */}
<div className="flex-1 min-w-[200px] relative">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<input
type="text"
placeholder="搜索指标名称、描述或标签..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white placeholder-slate-500 focus:border-amber-500'
: 'bg-gray-50 border-gray-200 text-gray-900 placeholder-gray-400 focus:border-amber-400'
}`}
/>
</div>
{/* 分类筛选 */}
<div className="flex items-center gap-2">
<Filter className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className={`px-3 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white'
: 'bg-gray-50 border-gray-200 text-gray-900'
}`}
>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
{/* 类型筛选 */}
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
className={`px-3 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white'
: 'bg-gray-50 border-gray-200 text-gray-900'
}`}
>
<option value="all">全部类型</option>
<option value="quantitative">定量</option>
<option value="qualitative">定性</option>
<option value="composite">综合</option>
</select>
{/* 状态筛选 */}
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className={`px-3 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white'
: 'bg-gray-50 border-gray-200 text-gray-900'
}`}
>
<option value="all">全部状态</option>
<option value="active">使用中</option>
<option value="draft">草稿</option>
<option value="deprecated">已废弃</option>
</select>
</div>
</div>
</div>
{/* 指标列表 */}
{viewMode === 'index' ? (
<div className={`rounded-xl overflow-hidden border ${isDark ? 'bg-slate-800/50 border-amber-800/30' : 'bg-white border-amber-100'}`}>
{Object.entries(groupedIndices).map(([category, indices]) => (
<div key={category} className="border-b border-gray-200 dark:border-slate-700 last:border-b-0">
{/* 分类标题 */}
<button
onClick={() => toggleCategory(category)}
className={`w-full px-4 py-3 flex items-center justify-between ${
isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'
} transition-colors`}
>
<div className="flex items-center gap-3">
{expandedCategories.has(category) ? (
<FolderOpen className={`w-5 h-5 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
) : (
<Folder className={`w-5 h-5 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
)}
<span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
{category}
</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
}`}>
{indices.length}个指标
</span>
</div>
<ChevronRight className={`w-4 h-4 transition-transform ${expandedCategories.has(category) ? 'rotate-90' : ''} ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
</button>
{/* 指标列表 */}
{expandedCategories.has(category) && (
<div className={`${isDark ? 'bg-slate-900/30' : 'bg-gray-50/50'}`}>
{indices.map((index, idx) => (
<div
key={index.id}
className={`px-4 py-4 cursor-pointer transition-colors ${
isDark ? 'hover:bg-slate-700/50' : 'hover:bg-white'
} ${idx !== indices.length - 1 ? 'border-b border-gray-200 dark:border-slate-700/50' : ''}`}
onClick={() => setSelectedIndex(index)}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<div className={`p-2 rounded-lg ${typeColors[index.type]}`}>
{index.type === 'quantitative' ? <BarChart3 className="w-4 h-4" /> :
index.type === 'qualitative' ? <PieChart className="w-4 h-4" /> :
<Activity className="w-4 h-4" />}
</div>
<div>
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
{index.name}
</h3>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{index.description}
</p>
</div>
</div>
{/* 进度条 */}
{index.currentValue !== undefined && index.baseline !== undefined && index.target !== undefined && (
<div className="mt-3">
<div className="flex items-center justify-between mb-1">
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
当前值: {index.currentValue}
</span>
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
目标: {index.target}
</span>
</div>
<div className="h-2 bg-gray-200 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${
calculateProgress(index)! >= 100 ? 'bg-green-500' :
calculateProgress(index)! >= 50 ? 'bg-amber-500' : 'bg-red-500'
}`}
style={{ width: `${calculateProgress(index)}%` }}
/>
</div>
</div>
)}
</div>
<div className="flex items-center gap-3 ml-4">
<div className="text-right">
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs ${typeColors[index.type]}`}>
{typeLabels[index.type]}
</span>
<span className={`px-2 py-1 rounded text-xs ${statusColors[index.status].bg} ${statusColors[index.status].text}`}>
{statusColors[index.status].label}
</span>
</div>
<div className="flex items-center gap-2 mt-1 justify-end">
{index.trend && trendIcons[index.trend]}
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{index.updateFrequency === 'realtime' ? '实时' :
index.updateFrequency === 'daily' ? '每日' :
index.updateFrequency === 'weekly' ? '每周' :
index.updateFrequency === 'monthly' ? '每月' : '每季度'}
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-gray-200 dark:border-slate-700/50">
<div className="flex items-center gap-1 text-sm">
<Star className={`w-4 h-4 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
<span className={isDark ? 'text-slate-400' : 'text-gray-500'}>权重: {index.weight * 100}%</span>
</div>
<div className="flex flex-wrap gap-2">
{index.tags.slice(0, 3).map(tag => (
<span
key={tag}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
}`}
>
{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
) : (
/* 体系管理视图 */
<div className="grid grid-cols-2 gap-4">
{mockSystems.map(system => (
<div
key={system.id}
className={`p-5 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-amber-800/30' : 'border-amber-100'}`}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-lg ${isDark ? 'bg-amber-500/20' : 'bg-amber-50'}`}>
<BarChart3 className={`w-6 h-6 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
</div>
<div>
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
{system.name}
</h3>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{system.category}
</p>
</div>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
system.status === 'active' ? 'bg-green-500/20 text-green-400' :
system.status === 'draft' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-gray-500/20 text-gray-400'
}`}>
{system.status === 'active' ? '使用中' : system.status === 'draft' ? '草稿' : '已归档'}
</span>
</div>
<p className={`text-sm mb-4 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{system.description}
</p>
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-slate-700">
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
包含 {system.indices.length} 个指标
</span>
<div className="flex items-center gap-2">
<button className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<Edit className="w-4 h-4" />
</button>
<button className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<Copy className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
{/* 指标详情弹窗 */}
{selectedIndex && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={() => setSelectedIndex(null)}>
<div
className={`w-full max-w-3xl rounded-2xl ${isDark ? 'bg-slate-800' : 'bg-white'} shadow-xl max-h-[90vh] overflow-y-auto`}
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 p-6 border-b ${isDark ? 'border-slate-700 bg-slate-800' : 'border-gray-200 bg-white'}">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-lg ${typeColors[selectedIndex.type]}`}>
{selectedIndex.type === 'quantitative' ? <BarChart3 className="w-5 h-5" /> :
selectedIndex.type === 'qualitative' ? <PieChart className="w-5 h-5" /> :
<Activity className="w-5 h-5" />}
</div>
<div>
<div className="flex items-center gap-2">
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{selectedIndex.name}
</h2>
<span className={`px-2 py-0.5 rounded text-xs ${typeColors[selectedIndex.type]}`}>
{typeLabels[selectedIndex.type]}
</span>
</div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{selectedIndex.category}
</p>
</div>
</div>
<button
onClick={() => setSelectedIndex(null)}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>指标描述</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.description}</p>
</div>
{selectedIndex.calculationMethod && (
<div className={`p-4 rounded-lg ${isDark ? 'bg-slate-900/50' : 'bg-gray-50'}`}>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>计算方法</p>
<p className={isDark ? 'text-white' : 'text-gray-700'}>{selectedIndex.calculationMethod}</p>
</div>
)}
{selectedIndex.dataSource && (
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>数据来源</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.dataSource}</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>权重</p>
<p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
{selectedIndex.weight * 100}%
</p>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>更新频率</p>
<p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
{frequencyLabels[selectedIndex.updateFrequency]}
</p>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>状态</p>
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[selectedIndex.status].bg} ${statusColors[selectedIndex.status].text}`}>
{statusColors[selectedIndex.status].label}
</span>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>趋势</p>
<div className="flex items-center gap-2">
{trendIcons[selectedIndex.trend]}
<span className={isDark ? 'text-white' : 'text-gray-900'}>
{selectedIndex.trend === 'up' ? '上升' : selectedIndex.trend === 'down' ? '下降' : '稳定'}
</span>
</div>
</div>
{selectedIndex.baseline !== undefined && (
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>基线值</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.baseline}</p>
</div>
)}
{selectedIndex.target !== undefined && (
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>目标值</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.target}</p>
</div>
)}
{selectedIndex.currentValue !== undefined && (
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>当前值</p>
<p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{selectedIndex.currentValue}</p>
</div>
)}
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>作者</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.author}</p>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>更新时间</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.updatedAt}</p>
</div>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>标签</p>
<div className="flex flex-wrap gap-2">
{selectedIndex.tags.map(tag => (
<span
key={tag}
className={`px-3 py-1 rounded-full text-sm ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
}`}
>
{tag}
</span>
))}
</div>
</div>
{selectedIndex.relatedIndices.length > 0 && (
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>关联指标</p>
<div className="flex flex-wrap gap-2">
{selectedIndex.relatedIndices.map(relatedId => {
const related = mockIndices.find(i => i.id === relatedId);
return related ? (
<span
key={relatedId}
className={`px-3 py-1 rounded-full text-sm cursor-pointer ${
isDark ? 'bg-amber-500/20 text-amber-400' : 'bg-amber-50 text-amber-600'
}`}
>
{related.name}
</span>
) : null;
})}
</div>
</div>
)}
</div>
<div className={`p-6 border-t ${isDark ? 'border-slate-700' : 'border-gray-200'} flex justify-end gap-3`}>
<button
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
isDark ? 'bg-slate-700 hover:bg-slate-600 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<Download className="w-4 h-4 inline mr-2" />
导出
</button>
<button
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
isDark ? 'bg-amber-600 hover:bg-amber-500 text-white' : 'bg-amber-500 hover:bg-amber-600 text-white'
}`}
>
<Edit className="w-4 h-4 inline mr-2" />
编辑
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default EvaluationIndexResourceManagement;
+529
View File
@@ -0,0 +1,529 @@
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
import {
FileText,
Download,
Printer,
RefreshCw,
ArrowLeft,
ChevronRight,
Target,
Users,
TrendingUp,
MessageSquare,
Megaphone,
Shield,
BarChart3,
Link2,
Clock,
AlertTriangle,
CheckCircle,
XCircle,
History,
FileUp,
Send,
Eye,
} from 'lucide-react';
interface FinalPlanProps {
onNavigate: (page: PageType) => void;
}
const FinalPlan: React.FC<FinalPlanProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 主题颜色类
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-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 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 btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
const [activeSection, setActiveSection] = useState('background');
const [reviewStatus, setReviewStatus] = useState<'draft' | 'pending' | 'approved' | 'rejected'>('pending');
const [showVersionHistory, setShowVersionHistory] = useState(false);
// 版本历史数据
const versionHistory = [
{ version: 'V3', date: '2026-03-19 16:00', author: '张策略', changes: '完成驱动分析与叙事策略优化' },
{ version: 'V2', date: '2026-03-19 14:30', author: '李智能', changes: '修正受众细分,补充渠道安排' },
{ version: 'V1', date: '2026-03-19 10:00', author: '张策略', changes: '初始版本生成' },
];
const sections = [
{ id: 'background', label: '一、任务背景', icon: FileText },
{ id: 'goal', label: '二、宣传目标', icon: Target },
{ id: 'audience', label: '三、目标受众', icon: Users },
{ id: 'behavior', label: '四、预期行为', icon: TrendingUp },
{ id: 'driver', label: '五、驱动因素', icon: MessageSquare },
{ id: 'narrative', label: '六、叙事策略', icon: Megaphone },
{ id: 'channel', label: '七、渠道安排', icon: Link2 },
{ id: 'risk', label: '八、风险控制', icon: Shield },
{ id: 'evaluation', label: '九、效果评估', icon: BarChart3 },
];
return (
<div className="space-y-6">
{/* 顶部操作栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => onNavigate('strategy-result')}
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>
<h1 className={`${textPrimary} font-bold text-lg`}>五四青年节宣传活动策略方案</h1>
<p className={`${textSecondary} text-sm`}>最终版方案 · V3 · 2026-03-19</p>
</div>
</div>
<div className="flex items-center gap-4">
{/* 审核状态 */}
<div className="flex items-center gap-2">
<span className={`${textSecondary} text-sm`}>审核状态:</span>
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium ${
reviewStatus === 'approved'
? 'bg-green-500/20 text-green-400'
: reviewStatus === 'pending'
? 'bg-yellow-500/20 text-yellow-400'
: reviewStatus === 'rejected'
? 'bg-red-500/20 text-red-400'
: `${inputBg} ${textSecondary}`
}`}>
{reviewStatus === 'approved' ? (
<><CheckCircle className="w-4 h-4" /> 已通过</>
) : reviewStatus === 'pending' ? (
<><Clock className="w-4 h-4" /> 审核中</>
) : reviewStatus === 'rejected' ? (
<><XCircle className="w-4 h-4" /> 未通过</>
) : (
<><FileText className="w-4 h-4" /> 草稿</>
)}
</div>
</div>
{/* 版本历史按钮 */}
<button
onClick={() => setShowVersionHistory(!showVersionHistory)}
className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
>
<History className="w-4 h-4" />
版本历史
</button>
<div className={`h-8 w-px ${isDark ? 'bg-blue-800/30' : 'bg-gray-200'}`} />
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Printer className="w-4 h-4" />
打印
</button>
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Download className="w-4 h-4" />
导出Word
</button>
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Download className="w-4 h-4" />
导出PDF
</button>
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
<FileUp className="w-4 h-4" />
导出汇报提纲
</button>
{/* 审核操作 */}
{reviewStatus === 'pending' ? (
<div className="flex items-center gap-2">
<button
onClick={() => setReviewStatus('approved')}
className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}
>
<CheckCircle className="w-4 h-4" />
通过
</button>
<button
onClick={() => setReviewStatus('rejected')}
className="flex items-center gap-2 px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 border border-red-500/30 text-red-400 rounded-lg text-sm transition-colors"
>
<XCircle className="w-4 h-4" />
驳回
</button>
</div>
) : reviewStatus === 'draft' ? (
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
<Send className="w-4 h-4" />
提交审核
</button>
) : (
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
<RefreshCw className="w-4 h-4" />
发起修订
</button>
)}
</div>
</div>
{/* 版本历史面板 */}
{showVersionHistory && (
<div className={`mt-4 pt-4 border-t ${cardBorder}`}>
<div className="grid grid-cols-3 gap-4">
{versionHistory.map((v, i) => (
<div
key={v.version}
className={`p-4 rounded-lg border ${
i === 0
? 'bg-blue-500/10 border-blue-500/30'
: `${inputBg} ${cardBorder}`
}`}
>
<div className="flex items-center justify-between mb-2">
<span className={`text-sm font-bold ${
i === 0 ? 'text-blue-400' : textSecondary
}`}>
{v.version}
</span>
{i === 0 && (
<span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs">
当前
</span>
)}
</div>
<p className={`${textSecondary} text-xs mb-1`}>{v.date}</p>
<p className={`${textPrimary} text-sm`}>{v.changes}</p>
<p className={`${textMuted} text-xs mt-1`}>作者:{v.author}</p>
</div>
))}
</div>
</div>
)}
</div>
<div className="grid grid-cols-4 gap-6">
{/* 左侧导航目录 (20%) */}
<div className="col-span-1">
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 sticky top-6`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>目录导航</h3>
<nav className="space-y-1">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left text-sm transition-colors ${
activeSection === section.id
? 'bg-blue-600/20 text-blue-400 border border-blue-500/30'
: `${textSecondary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-100'} hover:${textPrimary}`
}`}
>
<span className="text-xs opacity-50">{section.label.split('、')[0]}</span>
<span>{section.label.split('、')[1]}</span>
</button>
))}
</nav>
{/* 版本信息 */}
<div className={`mt-6 pt-4 border-t ${cardBorder}`}>
<div className={`flex items-center gap-2 ${textSecondary} text-xs mb-2`}>
<Clock className="w-3 h-3" />
版本信息
</div>
<div className="space-y-1 text-xs">
<p className={textSecondary}>当前版本:<span className={isDark ? 'text-cyan-400' : 'text-red-500'}>V3</span></p>
<p className={textSecondary}>生成时间:2026-03-19 16:00</p>
<p className={textSecondary}>关联任务:TASK-2026-0319-001</p>
</div>
</div>
{/* 风险提示 */}
<div className={`mt-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg`}>
<div className={`flex items-center gap-2 text-amber-400 text-xs font-medium mb-1`}>
<AlertTriangle className="w-3 h-3" />
风险提示
</div>
<p className={`${textSecondary} text-xs`}>需关注预算控制和内容质量</p>
</div>
</div>
</div>
{/* 中间正文区 (60%) */}
<div className="col-span-2">
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-8`}>
{/* 标题 */}
<div className={`text-center mb-8 pb-6 border-b ${cardBorder}`}>
<h1 className={`text-2xl font-bold ${textPrimary} mb-2`}>五四青年节宣传活动策略方案</h1>
<p className={textSecondary}>最终版 · V3</p>
</div>
{/* 逻辑总括 - 高亮显示 */}
<div className="mb-8 p-6 bg-gradient-to-r from-blue-600/20 to-cyan-600/20 border border-blue-500/30 rounded-xl">
<p className={`${textSecondary} leading-relaxed`}>
<strong className="text-cyan-400">逻辑总括:</strong>
本次宣传拟面向<span className="text-cyan-400 font-medium">【18-25岁基层青年】</span>,
围绕其<span className="text-yellow-400 font-medium">【社交认同与自我表达】</span>驱动,
传播<span className="text-purple-400 font-medium">【真实、有态度的青年故事】</span>叙事,
促使其产生<span className="text-green-400 font-medium">【主动参与互动并生成UGC内容】</span>行为,
从而实现<span className="text-blue-400 font-medium">【提升品牌在Z世代用户中的认知度】</span>目标。
</p>
</div>
{/* 正文内容 */}
<div className="space-y-8">
{/* 一、任务背景 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<FileText className="w-5 h-5 text-blue-400" />
一、任务背景
</h2>
<div className={`${textSecondary} leading-relaxed space-y-3 pl-7`}>
<p>五四青年节将至,公司希望借此契机开展一次面向年轻群体的品牌宣传活动,提升品牌在Z世代用户中的认知度和好感度。</p>
<p>本次活动需要制定一套完整的宣传策略,包括目标受众分析、行为设计、叙事框架、渠道安排等内容。</p>
<div className={`p-4 ${inputBg} ${cardBorder} rounded-lg`}>
<p className={`${textMuted} text-sm mb-2`}>特殊约束</p>
<p>• 预算有限,需注重性价比</p>
<p>• 需在5月1日前完成所有准备工作</p>
<p>• 传播效果需可量化评估</p>
</div>
</div>
</section>
{/* 二、宣传目标 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<Target className="w-5 h-5 text-blue-400" />
二、宣传目标
</h2>
<div className="pl-7 space-y-4">
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<p className="text-blue-400 text-sm font-medium mb-2">主目标</p>
<p className={textPrimary}>提升品牌在Z世代用户中的认知度</p>
<p className={`${textSecondary} text-sm mt-2`}>具体指标:活动期间品牌相关话题曝光量达500万+</p>
</div>
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-green-400 text-sm font-medium mb-2">次目标</p>
<p className={textPrimary}>建立品牌年轻化形象,增强用户情感连接</p>
</div>
</div>
</section>
{/* 三、目标受众 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<Users className="w-5 h-5 text-cyan-400" />
三、目标受众
</h2>
<div className="pl-7 space-y-4">
<div className="p-4 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
<p className="text-cyan-400 text-sm font-medium mb-2">核心受众</p>
<p className={`${textPrimary} font-medium`}>18-25岁基层青年</p>
<div className="mt-3 grid grid-cols-2 gap-4">
<div>
<p className={`${textMuted} text-xs mb-1`}>人口特征</p>
<p className={`${textSecondary} text-sm`}>学生、基层工作者为主</p>
</div>
<div>
<p className={`${textMuted} text-xs mb-1`}>媒体习惯</p>
<p className={`${textSecondary} text-sm`}>短视频、社交平台重度用户</p>
</div>
</div>
</div>
<div className={`p-4 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-200'} rounded-lg`}>
<p className={`${textSecondary} text-sm font-medium mb-2`}>次级受众</p>
<p className={textSecondary}>25-30岁职场新人</p>
</div>
</div>
</section>
{/* 四、预期行为及关键驱动 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<TrendingUp className="w-5 h-5 text-green-400" />
四、预期行为及关键驱动
</h2>
<div className="pl-7 space-y-4">
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-green-400 text-sm font-medium mb-2">目标行为</p>
<p className={textPrimary}>主动参与话题讨论、生成UGC内容</p>
<div className="mt-3 flex items-center gap-2">
<span className={`${textSecondary} text-sm`}>转化路径:</span>
{['浏览', '点赞', '评论', '转发', '创作'].map((step, i) => (
<React.Fragment key={step}>
<span className={`px-2 py-1 rounded text-xs ${
i < 2 ? 'bg-green-500/20 text-green-400' : `${inputBg} ${textSecondary}`
}`}>
{step}
</span>
{i < 4 && <span className={isDark ? 'text-slate-600' : 'text-gray-400'}>→</span>}
</React.Fragment>
))}
</div>
</div>
<div className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<p className="text-yellow-400 text-sm font-medium mb-2">核心驱动</p>
<p className={textPrimary}>社交认同与自我表达</p>
<div className="mt-2 flex gap-2">
{['归属感需求', '表达欲驱动', '群体认同激励'].map((item) => (
<span key={item} className="px-2 py-1 bg-yellow-500/20 text-yellow-400 rounded text-xs">
{item}
</span>
))}
</div>
</div>
</div>
</section>
{/* 五、叙事策略 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<Megaphone className="w-5 h-5 text-purple-400" />
五、叙事策略
</h2>
<div className="pl-7 space-y-4">
<div className="p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg">
<p className="text-purple-400 text-sm font-medium mb-2">主叙事模式</p>
<p className={textPrimary}>真实、有态度的青年故事</p>
</div>
<div className={`p-4 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-200'} rounded-lg`}>
<p className={`${textSecondary} text-sm font-medium mb-2`}>叙事策略</p>
<p className={textSecondary}>人物+事件结合,以真实故事传递品牌价值观。讲述年轻人自己的故事,引发情感共鸣。</p>
</div>
</div>
</section>
{/* 六、渠道与执行安排 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<Link2 className="w-5 h-5 text-pink-400" />
六、渠道与执行安排
</h2>
<div className="pl-7">
<div className="grid grid-cols-3 gap-4">
{[
{ platform: '抖音', role: '主战场,短视频传播', weight: '40%' },
{ platform: '微博', role: '话题引爆,社交传播', weight: '30%' },
{ platform: '小红书', role: '种草转化,UGC沉淀', weight: '30%' },
].map((channel) => (
<div key={channel.platform} className="p-4 bg-pink-500/10 border border-pink-500/30 rounded-lg">
<p className="text-pink-400 font-medium">{channel.platform}</p>
<p className={`${textSecondary} text-sm mt-1`}>{channel.role}</p>
<p className={`${textMuted} text-xs mt-2`}>预算占比:{channel.weight}</p>
</div>
))}
</div>
</div>
</section>
{/* 七、风险控制 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<Shield className="w-5 h-5 text-red-400" />
七、风险控制
</h2>
<div className="pl-7 space-y-3">
{[
{ risk: '预算有限,需控制成本', mitigation: '提前布局预热,优化投放策略' },
{ risk: '青年群体注意力分散', mitigation: '内容差异化,强调情感共鸣' },
{ risk: 'UGC质量难以保证', mitigation: '设置激励机制,建立内容审核' },
].map((item, i) => (
<div key={i} className={`flex gap-4 p-3 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-200'} rounded-lg`}>
<div className="flex-1">
<p className="text-red-400 text-xs font-medium">风险{i + 1}</p>
<p className={`${textPrimary} text-sm`}>{item.risk}</p>
</div>
<div className="flex-1">
<p className="text-green-400 text-xs font-medium">应对措施</p>
<p className={`${textSecondary} text-sm`}>{item.mitigation}</p>
</div>
</div>
))}
</div>
</section>
{/* 八、效果评估 */}
<section>
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
<BarChart3 className="w-5 h-5 text-amber-400" />
八、效果评估
</h2>
<div className="pl-7">
<div className="grid grid-cols-4 gap-4">
{[
{ metric: '曝光量', target: '500万+', current: '0' },
{ metric: '互动率', target: '5%', current: '0%' },
{ metric: 'UGC数量', target: '1000+', current: '0' },
{ metric: '话题热度', target: 'TOP 20', current: '-' },
].map((item) => (
<div key={item.metric} className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg text-center">
<p className="text-amber-400 text-xs">{item.metric}</p>
<p className={`${textPrimary} font-bold text-lg mt-1`}>{item.target}</p>
<p className={`${textMuted} text-xs mt-1`}>目标值</p>
</div>
))}
</div>
</div>
</section>
</div>
</div>
</div>
{/* 右侧辅助区 (20%) */}
<div className="col-span-1 space-y-6">
{/* 关联讨论记录 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>关联讨论记录</h3>
<button className={`w-full flex items-center justify-between p-3 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors`}>
<div className="flex items-center gap-2 text-blue-400 text-sm">
<FileText className="w-4 h-4" />
TASK-2026-0319-001
</div>
<ChevronRight className={`w-4 h-4 ${textSecondary}`} />
</button>
</div>
{/* 快速操作 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>快速操作</h3>
<div className="space-y-2">
<button className={`w-full flex items-center gap-3 p-3 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors text-left`}>
<Download className="w-4 h-4 text-green-400" />
<div>
<p className={`${textPrimary} text-sm`}>导出完整方案</p>
<p className={`${textMuted} text-xs`}>Word格式</p>
</div>
</button>
<button className={`w-full flex items-center gap-3 p-3 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors text-left`}>
<RefreshCw className="w-4 h-4 text-blue-400" />
<div>
<p className={`${textPrimary} text-sm`}>发起新版本修订</p>
<p className={`${textMuted} text-xs`}>基于当前版本</p>
</div>
</button>
</div>
</div>
{/* 分享 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>分享方案</h3>
<div className="flex gap-2">
<button className={`flex-1 p-2 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors`}>
<p className={`${textSecondary} text-xs`}>复制链接</p>
</button>
<button className={`flex-1 p-2 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors`}>
<p className={`${textSecondary} text-xs`}>发送邮件</p>
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default FinalPlan;
+984
View File
@@ -0,0 +1,984 @@
import React, { useState } from 'react';
import {
Send,
Upload,
RotateCcw,
Bookmark,
History,
Sparkles,
BookOpen,
Brain,
GitCompare,
ChevronDown,
ChevronRight,
MessageSquare,
FileText,
CheckCircle,
AlertCircle,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
type QAMode = 'single-round' | 'multi-round' | 'deep-search';
type CompareMode = 'compare-off' | 'compare-on';
type ViewMode = 'single' | 'compare';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
sources?: SourceItem[];
}
interface SourceItem {
title: string;
type: string;
snippet: string;
}
interface CompareResult {
leftMode: QAMode;
rightMode: QAMode;
leftContent: string;
rightContent: string;
sources: {
left: SourceItem[];
right: SourceItem[];
};
differences?: {
consensus: string;
differences: string;
leftAdvantage: string;
rightAdvantage: string;
};
}
const GeneralQA: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 状态管理
const [viewMode, setViewMode] = useState<ViewMode>('single');
const [qaMode, setQaMode] = useState<QAMode>('single-round');
const [compareLeftMode, setCompareLeftMode] = useState<QAMode>('single-round');
const [compareRightMode, setCompareRightMode] = useState<QAMode>('deep-search');
// 知识范围
const [knowledgeScope, setKnowledgeScope] = useState<'all' | 'specific-kb' | 'specific-domain'>('all');
// 回答偏好
const [answerStyle, setAnswerStyle] = useState<'concise' | 'detailed'>('detailed');
const [showSources, setShowSources] = useState(true);
const [showProcess, setShowProcess] = useState(false);
const [autoSummary, setAutoSummary] = useState(false);
// 对比设置
const [compareLayout, setCompareLayout] = useState<'horizontal' | 'vertical'>('horizontal');
const [autoSummarizeDiff, setAutoSummarizeDiff] = useState(true);
const [recommendBetter, setRecommendBetter] = useState(true);
// 消息和结果
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [compareResult, setCompareResult] = useState<CompareResult | null>(null);
// 检索过程展示
const [searchProcess, setSearchProcess] = useState({
currentMode: '单轮RAG',
rounds: 1,
hits: 5,
queryRewrite: '',
});
// 示例消息
const sampleMessages: Message[] = [
{
id: '1',
role: 'user',
content: 'RAG技术的工作原理是什么?',
timestamp: new Date(),
},
{
id: '2',
role: 'assistant',
content: 'RAG(检索增强生成)技术的工作原理主要包括以下几个步骤:\n\n1. **检索阶段**:系统首先将用户问题转换为查询向量,在知识库中进行相似度搜索,返回最相关的文档片段。\n\n2. **增强阶段**:将检索到的相关文档与原始问题组合,形成增强后的提示词上下文。\n\n3. **生成阶段**:将增强后的提示词发送给大语言模型,生成最终回答。\n\n这种方式可以有效解决LLM知识过时、幻觉等问题,提升回答的准确性和可信度。',
timestamp: new Date(),
sources: [
{ title: 'RAG技术白皮书', type: 'PDF', snippet: 'RAG是一种结合检索和生成的技术架构...' },
{ title: 'LLM应用实践', type: '文档', snippet: '检索增强生成可以有效提升回答质量...' },
],
},
];
const handleSend = () => {
if (!inputValue.trim()) return;
setIsLoading(true);
// 模拟发送消息
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date(),
};
setMessages([...messages, userMessage]);
setInputValue('');
// 模拟AI响应
setTimeout(() => {
setMessages(prev => [...prev, ...sampleMessages]);
setIsLoading(false);
setSearchProcess({
currentMode: qaMode === 'single-round' ? '单轮RAG' : qaMode === 'multi-round' ? '多轮RAG' : '深度检索',
rounds: qaMode === 'multi-round' ? 3 : 1,
hits: 5,
queryRewrite: qaMode === 'deep-search' ? '原问题→分解子问题→独立检索→综合回答' : '',
});
}, 1500);
};
const handleCompare = () => {
if (!inputValue.trim()) return;
setIsLoading(true);
// 模拟对比结果
setTimeout(() => {
setCompareResult({
leftMode: compareLeftMode,
rightMode: compareRightMode,
leftContent: '单轮RAG回答:直接检索并生成答案,响应速度快,但可能遗漏跨文档的关联信息。适用于简单事实查询。',
rightContent: '深度检索回答:通过问题分解、多轮检索和综合分析,提供更全面的答案。包含详细的推理过程和更多的引用来源。',
sources: {
left: [
{ title: 'RAG基础教程', type: '文档', snippet: '单轮RAG是最基础的检索增强方式...' },
],
right: [
{ title: '高级RAG技术', type: '论文', snippet: '深度检索通过query改写和迭代优化...' },
{ title: 'RAG最佳实践', type: '指南', snippet: '多阶段检索可以显著提升召回率...' },
],
},
differences: {
consensus: '两种方式都使用向量检索技术,都能引用原始文档',
differences: '深度检索采用多轮查询改写,覆盖更全面',
leftAdvantage: '响应更快,资源消耗更低',
rightAdvantage: '答案更完整,引用更充分',
},
});
setIsLoading(false);
}, 2000);
};
const handleSaveRecord = () => {
alert('问答记录已保存');
};
const handleCollect = () => {
alert('已添加到收藏');
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (viewMode === 'compare') {
handleCompare();
} else {
handleSend();
}
}
};
// 获取模式标签
const getModeLabel = (mode: QAMode) => {
switch (mode) {
case 'single-round': return '单轮RAG';
case 'multi-round': return '多轮RAG';
case 'deep-search': return '深度检索';
}
};
return (
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
{/* 顶部模式栏 */}
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className="flex items-center gap-4">
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
通用问答
</h1>
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
支持单轮RAG、多轮RAG、深度检索及结果对比
</span>
</div>
<div className="flex items-center gap-2">
{/* 模式切换按钮 */}
<div className={`flex rounded-lg p-1 ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
<button
onClick={() => setViewMode('single')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
viewMode === 'single'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'text-slate-400 hover:text-white'
: 'text-gray-600 hover:text-red-600'
}`}
>
单一模式
</button>
<button
onClick={() => setViewMode('compare')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
viewMode === 'compare'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'text-slate-400 hover:text-white'
: 'text-gray-600 hover:text-red-600'
}`}
>
对比模式
</button>
</div>
<button
onClick={() => onNavigate('qa-records')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isDark
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<History className="w-4 h-4" />
历史记录
</button>
</div>
</div>
{/* 主体内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧参数区 */}
<div className={`w-72 border-r flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
{viewMode === 'single' ? (
<>
{/* 问答模式选择 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
问答模式
</h3>
<div className="space-y-2">
{[
{ value: 'single-round', label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
{ value: 'multi-round', label: '多轮RAG', icon: Brain, desc: '深入理解' },
{ value: 'deep-search', label: '深度检索', icon: BookOpen, desc: '全面分析' },
].map((mode) => (
<button
key={mode.value}
onClick={() => setQaMode(mode.value as QAMode)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all text-left ${
qaMode === mode.value
? isDark
? 'bg-blue-600/20 border border-blue-500/50 text-blue-400'
: 'bg-red-50 border border-red-200 text-red-600'
: isDark
? 'bg-slate-700/30 border border-transparent text-slate-400 hover:bg-slate-700/50'
: 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
<mode.icon className="w-5 h-5" />
<div className="flex-1">
<div className="text-sm font-medium">{mode.label}</div>
<div className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{mode.desc}</div>
</div>
</button>
))}
</div>
</div>
</>
) : (
<>
{/* 对比模式设置 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对比模式设置
</h3>
{/* 左侧模式 */}
<div className="mb-4">
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
模式A
</label>
<select
value={compareLeftMode}
onChange={(e) => setCompareLeftMode(e.target.value as QAMode)}
className={`w-full px-3 py-2 rounded-lg text-sm ${
isDark
? 'bg-slate-700/50 border border-slate-600 text-white'
: 'bg-white border border-gray-200 text-gray-700'
}`}
>
<option value="single-round">单轮RAG</option>
<option value="multi-round">多轮RAG</option>
<option value="deep-search">深度检索</option>
</select>
</div>
<div className={`text-center my-2 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<span className="text-lg">VS</span>
</div>
{/* 右侧模式 */}
<div className="mb-4">
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
模式B
</label>
<select
value={compareRightMode}
onChange={(e) => setCompareRightMode(e.target.value as QAMode)}
className={`w-full px-3 py-2 rounded-lg text-sm ${
isDark
? 'bg-slate-700/50 border border-slate-600 text-white'
: 'bg-white border border-gray-200 text-gray-700'
}`}
>
<option value="single-round">单轮RAG</option>
<option value="multi-round">多轮RAG</option>
<option value="deep-search">深度检索</option>
</select>
</div>
</div>
{/* 对比布局 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对比布局
</h3>
<div className="flex gap-2">
<button
onClick={() => setCompareLayout('horizontal')}
className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${
compareLayout === 'horizontal'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-300'
: 'bg-gray-100 text-gray-600'
}`}
>
左右对比
</button>
<button
onClick={() => setCompareLayout('vertical')}
className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${
compareLayout === 'vertical'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-300'
: 'bg-gray-100 text-gray-600'
}`}
>
上下对比
</button>
</div>
</div>
</>
)}
{/* 知识范围 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
知识范围
</h3>
<div className="space-y-2">
{[
{ value: 'all', label: '全部知识源' },
{ value: 'specific-kb', label: '指定知识库' },
{ value: 'specific-domain', label: '指定资源域' },
].map((option) => (
<label
key={option.value}
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
knowledgeScope === option.value
? isDark
? 'bg-blue-600/20 text-blue-400'
: 'bg-red-50 text-red-600'
: isDark
? 'text-slate-400 hover:bg-slate-700/30'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<input
type="radio"
name="knowledge-scope"
checked={knowledgeScope === option.value}
onChange={() => setKnowledgeScope(option.value as any)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
knowledgeScope === option.value
? isDark
? 'border-blue-500 bg-blue-500'
: 'border-red-500 bg-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{knowledgeScope === option.value && (
<div className="w-1.5 h-1.5 rounded-full bg-white" />
)}
</div>
<span className="text-sm">{option.label}</span>
</label>
))}
</div>
</div>
{/* 回答偏好 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
回答偏好
</h3>
<div className="space-y-3">
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={answerStyle === 'detailed'}
onChange={() => setAnswerStyle(answerStyle === 'detailed' ? 'concise' : 'detailed')}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
answerStyle === 'detailed'
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{answerStyle === 'detailed' && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">详细回答</span>
</label>
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={showSources}
onChange={() => setShowSources(!showSources)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
showSources
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{showSources && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">展示引用来源</span>
</label>
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={showProcess}
onChange={() => setShowProcess(!showProcess)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
showProcess
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{showProcess && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">展示检索过程</span>
</label>
</div>
</div>
{/* 对比设置 - 仅对比模式 */}
{viewMode === 'compare' && (
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对比设置
</h3>
<div className="space-y-3">
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={autoSummarizeDiff}
onChange={() => setAutoSummarizeDiff(!autoSummarizeDiff)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
autoSummarizeDiff
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{autoSummarizeDiff && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">自动总结差异</span>
</label>
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={recommendBetter}
onChange={() => setRecommendBetter(!recommendBetter)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
recommendBetter
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{recommendBetter && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">推荐更优答案</span>
</label>
</div>
</div>
)}
</div>
{/* 中间结果区 */}
<div className="flex-1 flex flex-col overflow-hidden">
{viewMode === 'single' ? (
<>
{/* 聊天消息区 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<MessageSquare className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">开始你的问答</p>
<p className="text-sm mt-2">输入问题,获取基于知识库的智能回答</p>
</div>
) : (
messages.map((msg) => (
<div key={msg.id} className="space-y-3">
{/* 用户消息 */}
<div className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
{msg.role === 'assistant' && (
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
}`}>
<Brain className="w-4 h-4" />
</div>
)}
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${
msg.role === 'user'
? isDark
? 'bg-blue-600/30 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-200'
: 'bg-gray-100 text-gray-800'
}`}>
<p className="whitespace-pre-wrap">{msg.content}</p>
</div>
{msg.role === 'user' && (
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-200 text-gray-600'
}`}>
<div className="w-4 h-4 rounded-full bg-current" />
</div>
)}
</div>
{/* 来源信息 */}
{msg.role === 'assistant' && msg.sources && showSources && (
<div className={`ml-11 p-3 rounded-lg ${isDark ? 'bg-slate-800/50' : 'bg-gray-50'}`}>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
引用来源
</h4>
<div className="space-y-2">
{msg.sources.map((source, idx) => (
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="flex items-center gap-2 mb-1">
<FileText className={`w-3 h-3 ${isDark ? 'text-blue-400' : 'text-red-400'}`} />
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{source.title}
</span>
<span className={`px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-slate-600 text-slate-400' : 'bg-gray-200 text-gray-500'
}`}>
{source.type}
</span>
</div>
<p className={isDark ? 'text-slate-400' : 'text-gray-500'}>{source.snippet}</p>
</div>
))}
</div>
</div>
)}
</div>
))
)}
{isLoading && (
<div className="flex gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
}`}>
<Brain className="w-4 h-4" />
</div>
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<div className="flex gap-1">
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '0ms' }} />
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '150ms' }} />
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
)}
</div>
</>
) : (
<>
{/* 对比结果区 */}
<div className="flex-1 overflow-y-auto p-4">
{compareResult ? (
compareLayout === 'horizontal' ? (
<div className="grid grid-cols-2 gap-4 h-full">
{/* 左侧结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}>
{getModeLabel(compareResult.leftMode)}
</span>
</div>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.leftContent}
</p>
{/* 左侧来源 */}
<div className={`mt-4 pt-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
引用来源
</h4>
<div className="space-y-2">
{compareResult.sources.left.map((source, idx) => (
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{source.title}
</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* 右侧结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}>
{getModeLabel(compareResult.rightMode)}
</span>
</div>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.rightContent}
</p>
{/* 右侧来源 */}
<div className={`mt-4 pt-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
引用来源
</h4>
<div className="space-y-2">
{compareResult.sources.right.map((source, idx) => (
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{source.title}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
) : (
<div className="space-y-4">
{/* 上方结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}>
{getModeLabel(compareResult.leftMode)}
</span>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.leftContent}
</p>
</div>
</div>
{/* 下方结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}>
{getModeLabel(compareResult.rightMode)}
</span>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.rightContent}
</p>
</div>
</div>
</div>
)
) : (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<GitCompare className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">对比模式</p>
<p className="text-sm mt-2">输入问题,对比不同模式的回答效果</p>
</div>
)}
</div>
</>
)}
{/* 底部输入区 */}
<div className={`border-t p-4 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className={`flex items-end gap-3 rounded-xl p-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={viewMode === 'compare' ? '输入问题进行对比分析...' : '输入你的问题...'}
rows={1}
className={`flex-1 bg-transparent resize-none focus:outline-none ${
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
}`}
/>
<div className="flex items-center gap-2">
<button className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}>
<Upload className="w-5 h-5" />
</button>
<button
onClick={() => {
setMessages([]);
setCompareResult(null);
}}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="重新提问"
>
<RotateCcw className="w-5 h-5" />
</button>
<button
onClick={handleSaveRecord}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="保存记录"
>
<Bookmark className="w-5 h-5" />
</button>
<button
onClick={handleCollect}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="加入收藏"
>
<Bookmark className="w-5 h-5" />
</button>
<button
onClick={viewMode === 'compare' ? handleCompare : handleSend}
disabled={!inputValue.trim() || isLoading}
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
inputValue.trim() && !isLoading
? isDark
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
: isDark
? 'bg-slate-600 text-slate-400'
: 'bg-gray-300 text-gray-500'
}`}
>
<Send className="w-4 h-4" />
发送
</button>
</div>
</div>
</div>
</div>
{/* 右侧辅助区 */}
{viewMode === 'single' && showProcess && (
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
{/* 检索过程 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
检索过程
</h3>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>当前模式</span>
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
{searchProcess.currentMode}
</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>检索轮次</span>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{searchProcess.rounds}
</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>命中数</span>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{searchProcess.hits}
</span>
</div>
{searchProcess.queryRewrite && (
<div>
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>Query改写</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{searchProcess.queryRewrite}
</p>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* 对比模式的差异总结 */}
{viewMode === 'compare' && compareResult?.differences && (
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
差异总结
</h3>
<div className={`p-4 rounded-xl space-y-4 ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div>
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-green-400' : 'text-green-600'}`}>
<CheckCircle className="w-3 h-3" />
一致观点
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.consensus}
</p>
</div>
<div>
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`}>
<AlertCircle className="w-3 h-3" />
差异观点
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.differences}
</p>
</div>
<div>
<span className={`text-xs ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
{getModeLabel(compareResult.leftMode)}优势
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.leftAdvantage}
</p>
</div>
<div>
<span className={`text-xs ${isDark ? 'text-purple-400' : 'text-purple-600'}`}>
{getModeLabel(compareResult.rightMode)}优势
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.rightAdvantage}
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default GeneralQA;
@@ -0,0 +1,54 @@
import React, { useEffect, useState } from "react";
import { useTheme } from "../contexts/ThemeContext";
import { PageType } from "../components/Sidebar";
const knowledgeURL = import.meta.env.VITE_AIPI_KNOWLEDGE_URL || "";
import "./css/KnowledgeBaseResourceManagement.css";
import { requestCrudService } from "@/service/request-axios-service";
interface KnowledgeBaseResourceManagementProps {
onNavigate: (page: PageType) => void;
}
const KnowledgeBaseResourceManagement: React.FC<
KnowledgeBaseResourceManagementProps
> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === "dark";
const service = requestCrudService;
const [iframeUrl, setIframeUrl] = useState("");
useEffect(() => {
initIframeUrl();
}, []);
const initIframeUrl = async () => {
// const url = `${knowledgeURL}/#/knowledge/knowledge-search?isIframe=true`;
const jsonStr = localStorage.getItem("userInfo");
console.log('jsonStr', jsonStr)
if (jsonStr) {
const userInfo = JSON.parse(jsonStr);
console.log('userInfo', userInfo);
const url = `https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl`;
const token = userInfo.accessToken;
service.post(url, { token, skin: "red" }).then((res:any) => {
console.log({ iframeUrl: res.iframeUrl});
setIframeUrl(
res.iframeUrl
);
});
}
// setIframeUrl(
// "http://localhost:4210/#/knowledge/knowledge-search?isIframe=true"
// );
};
return (
<div className="iframe-container">
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
</div>
);
};
export default KnowledgeBaseResourceManagement;
+485
View File
@@ -0,0 +1,485 @@
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
BarChart,
FileText,
Table,
Search,
Send,
Sparkles,
Clock,
ChevronRight,
Bot,
Loader2,
Copy,
CheckCheck,
Download,
RefreshCw,
FileBarChart,
Database,
Brain,
PieChart,
LineChart,
FileSpreadsheet,
FileSearch,
BookOpen,
} from 'lucide-react';
interface LightAppCenterProps {
onNavigate: (page: string) => void;
initialApp?: 'analysis' | 'report' | 'data' | 'research';
}
type AppType = 'analysis' | 'report' | 'data' | 'research';
interface AppCard {
id: AppType;
title: string;
description: string;
icon: React.ElementType;
color: string;
}
const apps: AppCard[] = [
{
id: 'analysis',
title: '快速分析',
description: '对文本、数据进行快速分析处理,提取关键信息和洞察',
icon: BarChart,
color: 'blue',
},
{
id: 'report',
title: '报告生成',
description: '基于输入内容生成结构化分析报告,支持多种格式',
icon: FileText,
color: 'green',
},
{
id: 'data',
title: '数据处理',
description: '数据清洗、转换、统计和可视化处理',
icon: Table,
color: 'purple',
},
{
id: 'research',
title: '研究助手',
description: '辅助学术研究和资料整理,支持文献分析和摘要',
icon: Search,
color: 'orange',
},
];
const LightAppCenter: React.FC<LightAppCenterProps> = ({ onNavigate, initialApp }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [selectedApp, setSelectedApp] = useState<AppType | null>(initialApp || null);
const [input, setInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [result, setResult] = useState('');
const [copied, setCopied] = useState(false);
const [activeTab, setActiveTab] = useState<'input' | 'history'>('input');
const handleAppSelect = (app: AppCard) => {
setSelectedApp(app.id);
setInput('');
setResult('');
};
const handleSubmit = async () => {
if (!input.trim() || !selectedApp) return;
setIsProcessing(true);
setResult('');
// 模拟处理过程
await new Promise(resolve => setTimeout(resolve, 1500));
// 根据不同应用生成模拟结果
let mockResult = '';
switch (selectedApp) {
case 'analysis':
mockResult = `【分析结果】
1. 关键主题
• 主题A:核心概念和定义
• 主题B:应用场景和实践
• 主题C:发展趋势和展望
2. 情感分析
• 整体倾向:中性偏正面
• 关键情感词:创新、发展、应用
3. 关键实体
• 机构:示例机构
• 人物:相关专家
• 地点:主要应用区域
4. 重要数据
• 增长率:15-20%
• 覆盖范围:多个领域
• 时间周期:持续进行`;
break;
case 'report':
mockResult = `# 分析报告
## 概述
基于提供的内容,生成以下分析报告。
## 主要发现
### 1. 核心内容
详细描述主要发现和观察结果...
### 2. 深入分析
对关键点进行深入分析和解读...
### 3. 结论建议
基于分析结果提出建议和行动方案...
## 附录
- 数据来源
- 分析方法
- 局限性说明`;
break;
case 'data':
mockResult = `【数据处理结果】
| 指标 | 数值 | 变化 |
|------|------|------|
| 样本量 | 1,234 | +12% |
| 准确率 | 95.6% | +3.2% |
| 覆盖率 | 89.3% | +8.7% |
统计摘要:
• 总计处理 1,234 条记录
• 有效数据 1,156 条(93.7%)
• 异常数据 78 条(6.3%)
图表数据已准备好可视化`;
break;
case 'research':
mockResult = `【研究摘要】
## 文献概览
找到相关研究文献 12 篇,主要涉及以下方向:
### 核心观点
1. 研究方向A - 理论基础
2. 研究方向B - 方法论
3. 研究方向C - 实证应用
### 关键结论
• 结论1:...
• 结论2:...
• 结论3:...
### 研究空白
当前研究存在以下空白点,可作为进一步探索的方向...
### 建议阅读
1. 《核心文献一》- 理论基础
2. 《核心文献二》- 方法创新
3. 《核心文献三》- 实践应用`;
break;
}
setResult(mockResult);
setIsProcessing(false);
};
const handleCopy = () => {
navigator.clipboard.writeText(result);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const getColorClasses = (color: string) => {
const colors: Record<string, { bg: string; text: string; border: string; light: string }> = {
blue: {
bg: isDark ? 'bg-blue-500/20' : 'bg-blue-50',
text: isDark ? 'text-blue-400' : 'text-blue-600',
border: isDark ? 'border-blue-500/30' : 'border-blue-200',
light: isDark ? 'hover:bg-blue-500/30' : 'hover:bg-blue-100',
},
green: {
bg: isDark ? 'bg-green-500/20' : 'bg-green-50',
text: isDark ? 'text-green-400' : 'text-green-600',
border: isDark ? 'border-green-500/30' : 'border-green-200',
light: isDark ? 'hover:bg-green-500/30' : 'hover:bg-green-100',
},
purple: {
bg: isDark ? 'bg-purple-500/20' : 'bg-purple-50',
text: isDark ? 'text-purple-400' : 'text-purple-600',
border: isDark ? 'border-purple-500/30' : 'border-purple-200',
light: isDark ? 'hover:bg-purple-500/30' : 'hover:bg-purple-100',
},
orange: {
bg: isDark ? 'bg-orange-500/20' : 'bg-orange-50',
text: isDark ? 'text-orange-400' : 'text-orange-600',
border: isDark ? 'border-orange-500/30' : 'border-orange-200',
light: isDark ? 'hover:bg-orange-500/30' : 'hover:bg-orange-100',
},
};
return colors[color];
};
const selectedAppData = apps.find(a => a.id === selectedApp);
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className={`text-2xl font-bold theme-text ${isDark ? 'text-white' : 'text-gray-900'}`}>
轻应用中心
</h1>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
快速调用工具完成临时分析需求
</p>
</div>
{selectedApp && (
<button
onClick={() => setSelectedApp(null)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
返回应用列表
</button>
)}
</div>
{/* 应用选择卡片 */}
{!selectedApp && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{apps.map((app) => {
const Icon = app.icon;
const colors = getColorClasses(app.color);
return (
<button
key={app.id}
onClick={() => handleAppSelect(app)}
className={`p-6 rounded-xl border transition-all duration-200 text-left group ${
isDark
? `bg-slate-800/50 ${colors.border} hover:${colors.bg}`
: `bg-white ${colors.border} hover:${colors.bg}`
}`}
>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-lg ${colors.bg}`}>
<Icon className={`w-6 h-6 ${colors.text}`} />
</div>
<div className="flex-1">
<h3 className={`font-semibold group-hover:${colors.text} ${isDark ? 'text-white' : 'text-gray-900'}`}>
{app.title}
</h3>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{app.description}
</p>
</div>
<ChevronRight className={`w-5 h-5 ${isDark ? 'text-slate-500' : 'text-gray-400'} group-hover:${colors.text}`} />
</div>
</button>
);
})}
</div>
)}
{/* 应用工作区 */}
{selectedApp && selectedAppData && (
<div className={`rounded-xl border overflow-hidden ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
{/* 应用标题栏 */}
<div className={`px-6 py-4 border-b flex items-center gap-3 ${
isDark ? 'border-slate-700 bg-slate-800' : 'border-gray-200 bg-gray-50'
}`}>
<div className={`p-2 rounded-lg ${getColorClasses(selectedAppData.color).bg}`}>
{React.createElement(selectedAppData.icon, {
className: `w-5 h-5 ${getColorClasses(selectedAppData.color).text}`
})}
</div>
<div>
<h2 className={`font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{selectedAppData.title}
</h2>
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{selectedAppData.description}
</p>
</div>
</div>
{/* 标签页 */}
<div className={`px-6 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
<div className="flex gap-4">
<button
onClick={() => setActiveTab('input')}
className={`py-3 px-1 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'input'
? isDark
? 'border-blue-500 text-blue-400'
: 'border-red-500 text-red-600'
: isDark
? 'border-transparent text-slate-400 hover:text-slate-300'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
输入分析
</button>
<button
onClick={() => setActiveTab('history')}
className={`py-3 px-1 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'history'
? isDark
? 'border-blue-500 text-blue-400'
: 'border-red-500 text-red-600'
: isDark
? 'border-transparent text-slate-400 hover:text-slate-300'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
历史记录
</button>
</div>
</div>
<div className="p-6">
{activeTab === 'input' ? (
<div className="space-y-4">
{/* 输入区域 */}
<div>
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
输入内容
</label>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={`请输入需要${selectedAppData.title}的内容...`}
rows={6}
className={`w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 resize-none ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500 focus:ring-blue-500/20'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20'
}`}
/>
</div>
{/* 操作按钮 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<button className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<RefreshCw className="w-4 h-4" />
</button>
<button className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<FileBarChart className="w-4 h-4" />
</button>
<button className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<Database className="w-4 h-4" />
</button>
</div>
<button
onClick={handleSubmit}
disabled={!input.trim() || isProcessing}
className={`px-6 py-2.5 rounded-lg font-medium flex items-center gap-2 transition-all duration-200 ${
!input.trim() || isProcessing
? isDark
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
: isDark
? 'bg-blue-600 text-white hover:bg-blue-500'
: 'bg-red-500 text-white hover:bg-red-600'
}`}
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
处理中...
</>
) : (
<>
<Send className="w-4 h-4" />
开始处理
</>
)}
</button>
</div>
{/* 结果区域 */}
{result && (
<div className={`mt-6 p-4 rounded-lg border ${
isDark ? 'bg-slate-900 border-slate-700' : 'bg-gray-50 border-gray-200'
}`}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Sparkles className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<span className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
处理结果
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleCopy}
className={`p-1.5 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
>
{copied ? (
<CheckCheck className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
<button className={`p-1.5 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}>
<Download className="w-4 h-4" />
</button>
</div>
</div>
<pre className={`whitespace-pre-wrap text-sm font-mono leading-relaxed ${
isDark ? 'text-slate-300' : 'text-gray-600'
}`}>
{result}
</pre>
</div>
)}
</div>
) : (
<div className="text-center py-12">
<Clock className={`w-12 h-12 mx-auto mb-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
暂无历史记录
</p>
</div>
)}
</div>
</div>
)}
{/* 提示信息 */}
{!selectedApp && (
<div className={`p-4 rounded-lg border ${
isDark ? 'bg-blue-500/10 border-blue-500/30' : 'bg-blue-50 border-blue-200'
}`}>
<div className="flex items-start gap-3">
<Sparkles className={`w-5 h-5 mt-0.5 ${isDark ? 'text-blue-400' : 'text-blue-500'}`} />
<div>
<p className={`text-sm font-medium ${isDark ? 'text-blue-300' : 'text-blue-800'}`}>
关于轻应用中心
</p>
<p className={`mt-1 text-sm ${isDark ? 'text-blue-400/80' : 'text-blue-700'}`}>
轻应用中心提供了一系列快速分析工具,适合处理临时性的分析需求。相比完整的任务级应用,轻应用更加轻量便捷,可以快速调用多种工具完成分析工作。
</p>
</div>
</div>
</div>
)}
</div>
);
};
export default LightAppCenter;
+545
View File
@@ -0,0 +1,545 @@
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
Search,
Clock,
ChevronRight,
Download,
Send,
Trash2,
FileText,
User,
MessageSquare,
Network,
Target,
BookOpen,
History,
Filter,
Calendar,
AlertTriangle,
CheckCircle2,
Eye,
} from 'lucide-react';
import { LightAppType } from './LightAppSquare';
import { requestCrudService } from '@/service/request-axios-service';
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Pagination, Select } from 'tdesign-react';
import dayjs from 'dayjs';
// 运行记录类型
interface RunRecord {
id: string;
appId: LightAppType;
appName: string;
appIcon: React.ElementType;
iconColor: string;
input: string;
runTime: string;
resultSummary: string;
status: number;
riskLevel?: 'low' | 'medium' | 'high';
sentiment?: string;
inputObject?: string;
}
interface stateData {
startTime: string;
endTime: string;
pageNum: number;
pageSize: number;
status: string | number;
}
// 应用信息映射
const appInfoMap: Record<LightAppType, { name: string; icon: React.ElementType; iconColor: string }> = {
'info-ty': { name: '信息TY', icon: FileText, iconColor: 'text-blue-500' },
'info-pg': { name: '信息PG', icon: User, iconColor: 'text-cyan-500' },
'info-spread': { name: '信息传播分析', icon: Network, iconColor: 'text-teal-500' },
'account-pg': { name: '账号PG', icon: User, iconColor: 'text-purple-500' },
'sentiment': { name: '三情分析', icon: MessageSquare, iconColor: 'text-orange-500' },
'event': { name: '事件分析', icon: Target, iconColor: 'text-red-500' },
'topic': { name: '专题分析', icon: BookOpen, iconColor: 'text-green-500' },
};
interface LightAppRecordsProps {
onNavigate: (page: string, appId?: string) => void;
}
const appIconList = [
{
appIcon: MessageSquare,
iconColor: 'text-orange-500',
},
{
appIcon: Target,
iconColor: 'text-red-500',
},
{
appIcon: User,
iconColor: 'text-purple-500',
},
{
appIcon: Network,
iconColor: 'text-teal-500',
},
{
appIcon: BookOpen,
iconColor: 'text-green-500',
},
{
appIcon: FileText,
iconColor: 'text-blue-500',
},
]
const service = requestCrudService;
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const LightAppRecords: React.FC<LightAppRecordsProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [searchQuery, setSearchQuery] = useState('');
const [appFilter, setAppFilter] = useState<LightAppType | 'all'>('all');
const [timeFilter, setTimeFilter] = useState<string>('all');
const [records, setRecords] = useState<RunRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [pageNum, setPageNum] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
const [total, setTotal] = useState<number>(0);
const [presets] = useState<DateRangePickerProps['presets']>({
最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
今天: [dayjs().toDate(), dayjs().toDate()],
});
const [range, setRange] = useState<DateRangeValue>([]);
const state = useRef<stateData>({
startTime: '',
endTime: '',
pageNum: 1,
pageSize: 10,
status: ''
})
const [statusValue, setStatusValue] = useState<number | string>('');
// 执行状态,0=执行中, 1=成功, 2=失败, 3=部分成功
const [statusList] = useState([
{ value: '', label: '全部' },
{ value: 0, label: '执行中' },
{ value: 1, label: '成功' },
{ value: 2, label: '失败' },
{ value: 3, label: '部分成功' },
])
const statusChange = async (val) => {
state.current.status = val;
setStatusValue(val);
await historyPageSearch();
}
// 时间日期选择器查询
const setRangeChange = async (val) => {
setRange(val);
console.log('时间日期', val);
state.current.startTime = '';
state.current.endTime = '';
if (val.length > 0) {
state.current.startTime = val[0];
state.current.endTime = val[1];
}
await historyPageSearch();
}
// 过滤记录
// const filteredRecords = useMemo(() => {
// return records.filter(record => {
// // 搜索过滤
// const matchesSearch = searchQuery === '' ||
// record.input.toLowerCase().includes(searchQuery.toLowerCase()) ||
// record.appName.toLowerCase().includes(searchQuery.toLowerCase()) ||
// record.resultSummary.toLowerCase().includes(searchQuery.toLowerCase());
// // 应用类型过滤
// const matchesApp = appFilter === 'all' || record.appId === appFilter;
// // 时间过滤
// let matchesTime = true;
// if (timeFilter !== 'all') {
// const recordDate = new Date(record.runTime);
// const now = new Date();
// if (timeFilter === '7d') {
// const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
// matchesTime = recordDate >= weekAgo;
// } else if (timeFilter === '30d') {
// const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
// matchesTime = recordDate >= monthAgo;
// }
// }
// return matchesSearch && matchesApp && matchesTime;
// });
// }, [records, searchQuery, appFilter, timeFilter]);
// 查看详情
const handleView = (record: RunRecord) => {
console.log('查看详情:', record);
onNavigate('light-app-run', record.appId);
};
// 导出
const handleExport = (record: RunRecord) => {
const data = JSON.stringify(record, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${record.appName}_${record.runTime.replace(/[: ]/g, '-')}.json`;
a.click();
};
// 转正式任务
const handleConvert = (record: RunRecord) => {
onNavigate('task-create');
};
// 获取状态颜色
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return { bg: 'bg-green-500/10', text: 'text-green-500', border: 'border-green-500/30' };
case 2:
return { bg: 'bg-red-500/10', text: 'text-red-500', border: 'border-red-500/30' };
case 3:
return { bg: 'bg-yellow-500/10', text: 'text-yellow-500', border: 'border-yellow-500/30' };
default:
return { bg: 'bg-gray-500/10', text: 'text-gray-500', border: 'border-gray-500/30' };
}
};
// 获取状态标签
const getStatusLabel = (status: number) => {
switch (status) {
case 0: return '执行中';
case 1: return '成功';
case 2: return '失败';
case 3: return '部分';
default: return status;
}
};
// 回车处理
const handleKeyDown = async (e) => {
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
e.preventDefault();
} else if (e.keyCode === 13) {
e.preventDefault();
await historyPageSearch();
} else if (e.key === "/") {
e.preventDefault();
}
};
const historyPageSearch = async () => {
const parmy = {
appName: searchQuery,
keyword: '',
pageNum: state.current.pageNum,
pageSize: state.current.pageSize,
startTime: state.current.startTime,
endTime: state.current.endTime,
status: state.current.status
}
setLoading(true);
const res = await service.post(`${zncmUrl}/app/history/page`, parmy);
console.log('res', res);
setLoading(false);
const result = res.data;
result.records.forEach((item: RunRecord, index: number) => {
const realIndex = index % appIconList.length;
item.appIcon = appIconList[realIndex].appIcon;
item.iconColor = appIconList[realIndex].iconColor;
})
setTotal(result.total);
setRecords(result.records || '');
}
const pageIndexChange = async (value: number) => {
state.current.pageNum = value;
setPageNum(value);
await historyPageSearch();
}
const pageSizeChange = async (value: number) => {
state.current.pageSize = value;
setPageSize(value);
await historyPageSearch();
}
useEffect(() => {
fetch('https://ch1.b.uat.4.cn/zncmweb/templates/index.json') // 注意:不需要 public 前缀
.then(response => {
if (!response.ok) {
throw new Error('网络请求失败');
}
return response.json();
})
.then(jsonData => {
console.log('jsonData', jsonData);
})
.catch(err => {
console.log(err);
});
historyPageSearch();
}, []);
return (
<div className="space-y-6">
{/* 顶部工具栏 */}
<div className={`rounded-xl border p-4 ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* 面包屑和标题 */}
<div className="flex items-center gap-2 flex-1">
<History className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>轻应用中心 /</span>
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>运行记录</span>
</div>
{/* 搜索框 */}
<div className="flex-1 max-w-md">
<div className="relative">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
<input
type="text"
value={searchQuery}
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索记录..."
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-colors focus:outline-none focus:ring-2 ${isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500 focus:ring-blue-500/20'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20'
}`}
/>
</div>
</div>
{/* 筛选 */}
<div className="flex items-center gap-3">
{/* <select
value={appFilter}
onChange={(e) => setAppFilter(e.target.value as LightAppType | 'all')}
className={`px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${isDark
? 'bg-slate-900 border-slate-600 text-slate-200 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-700 focus:border-red-500'
}`}
>
<option value="all">全部应用</option>
<option value="info-ty">信息TY</option>
<option value="info-pg">信息PG</option>
<option value="info-spread">信息传播分析</option>
<option value="account-pg">账号PG</option>
<option value="sentiment">三情分析</option>
<option value="event">事件分析</option>
<option value="topic">专题分析</option>
</select> */}
<Select
value={statusValue}
onChange={statusChange}
style={{ width: '135px' }}
clearable
options={statusList} />
<DateRangePicker value={range} presets={presets} onChange={(val) => setRangeChange(val)} enableTimePicker={true} clearable />
</div>
</div>
</div>
{/* 记录列表 */}
<div className={`rounded-xl border overflow-hidden ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
{/* 表头 */}
<div className={`grid grid-cols-12 gap-4 px-4 py-3 border-b ${isDark ? 'border-slate-700 bg-slate-800/30' : 'border-gray-200 bg-gray-50'
}`}>
<div className={`col-span-3 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
应用名称
</div>
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
输入对象
</div>
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
运行时间
</div>
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
结果摘要
</div>
<div className={`col-span-1 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
状态
</div>
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
操作
</div>
</div>
{/* 记录行 */}
{records.length > 0 ? (
records.map((record) => {
const Icon = record.appIcon;
const statusColor = getStatusColor(record.status);
return (
<div
key={record.id}
className={`grid grid-cols-12 gap-4 px-4 py-4 border-b items-center ${isDark ? 'border-slate-700/50 hover:bg-slate-700/30' : 'border-gray-100 hover:bg-gray-50'
} transition-colors`}
>
{/* 应用名称 */}
<div className="col-span-3 flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<Icon className={`w-4 h-4 ${record.iconColor}`} />
</div>
<div>
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
{record.appName}
</span>
{/* {record.sentiment && (
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${record.sentiment === '偏正' ? 'bg-green-500/10 text-green-500' :
record.sentiment === '偏负' ? 'bg-red-500/10 text-red-500' :
'bg-gray-500/10 text-gray-500'
}`}>
{record.sentiment}
</span>
)} */}
{/* {record.riskLevel && (
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${record.riskLevel === 'high' ? 'bg-red-500/10 text-red-500' :
record.riskLevel === 'medium' ? 'bg-yellow-500/10 text-yellow-500' :
'bg-green-500/10 text-green-500'
}`}>
{record.riskLevel === 'high' ? '高风险' :
record.riskLevel === 'medium' ? '中风险' : '低风险'}
</span>
)} */}
</div>
</div>
{/* 输入对象 */}
<div className="col-span-2">
<span className={`text-sm truncate block ${isDark ? 'text-slate-400' : 'text-gray-600'}`} title={record.input}>
{record.inputObject || '-'}
</span>
</div>
{/* 运行时间 */}
<div className="col-span-2">
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-600'}`}>
{record.runTime}
</span>
</div>
{/* 结果摘要 */}
<div className="col-span-2">
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{record.resultSummary || '-'}
</span>
</div>
{/* 状态 */}
<div className="col-span-1">
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColor.bg} ${statusColor.text}`}>
{getStatusLabel(record.status)}
</span>
</div>
{/* 操作 */}
<div className="col-span-2 flex items-center gap-1">
<button
onClick={() => handleView(record)}
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="查看"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleExport(record)}
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="导出"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={() => handleConvert(record)}
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="转正式任务"
>
<Send className="w-4 h-4" />
</button>
<button
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="删除"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})
) : (
<div className="text-center py-12">
<History className={`w-12 h-12 mx-auto mb-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
暂无运行记录
</p>
</div>
)}
</div>
{/* 统计信息 */}
{/* <div className={`rounded-xl border p-4 ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<CheckCircle2 className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
成功: <span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{filteredRecords.filter(r => r.status === 'success').length}</span>
</span>
</div>
<div className="flex items-center gap-2">
<AlertTriangle className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
部分: <span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{filteredRecords.filter(r => r.status === 'partial').length}</span>
</span>
</div>
<div className="flex items-center gap-2">
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
共 <span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{filteredRecords.length}</span> 条记录
</span>
</div>
</div>
<button
onClick={() => onNavigate('light-app-square')}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${isDark
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30'
: 'bg-red-50 text-red-600 hover:bg-red-100'
}`}
>
<FileText className="w-4 h-4" />
新建分析
</button>
</div>
</div> */}
{/* 分页 */}
<div className="p-4 text-center">
<Pagination
total={total}
current={pageNum}
pageSize={pageSize}
onCurrentChange={(page) => pageIndexChange(Number(page))}
onPageSizeChange={(size) => pageSizeChange(Number(size))}
/>
</div>
</div>
);
};
export default LightAppRecords;
+996
View File
@@ -0,0 +1,996 @@
import React, { useState, useEffect } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
ArrowLeft,
Save,
Download,
Send,
RotateCcw,
PlayCircle,
CheckCircle2,
Circle,
Loader2,
ChevronDown,
ChevronRight,
FileText,
Link2,
Upload,
Clock,
AlertTriangle,
Sparkles,
Users,
Copy,
CheckCheck,
RefreshCw,
Database,
BarChart3,
PieChart,
LineChart,
Table,
User,
MessageSquare,
Network,
Target,
BookOpen,
Calendar,
Search,
} from 'lucide-react';
import { LightAppType } from './LightAppSquare';
// 应用配置接口
interface AppConfig {
id: LightAppType;
name: string;
description: string;
icon: React.ElementType;
iconColor: string;
bgColor: string;
inputs: {
name: string;
label: string;
placeholder: string;
type: 'text' | 'textarea' | 'link' | 'upload';
required: boolean;
}[];
params: {
name: string;
label: string;
options: { value: string; label: string }[];
default: string;
}[];
dimensions: {
name: string;
label: string;
default: boolean;
}[];
steps: string[];
previewFields: { key: string; label: string }[];
}
interface LightAppRunProps {
onNavigate: (page: string, appId?: string) => void;
appId?: LightAppType;
}
// 应用配置数据
const appConfigs: Record<LightAppType, AppConfig> = {
'info-ty': {
id: 'info-ty',
name: '信息TY',
description: '围绕单条或多条信息进行TY向分析',
icon: FileText,
iconColor: 'text-blue-500',
bgColor: 'bg-blue-500/10',
inputs: [
{ name: 'mainInput', label: '信息文本', placeholder: '请输入信息内容...', type: 'textarea', required: true },
{ name: 'link', label: '信息链接', placeholder: '可选,粘贴信息链接', type: 'link', required: false },
{ name: 'materials', label: '上传材料', placeholder: '支持 txt, pdf, docx', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '7d', label: '近7天' },
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'dataRange', label: '数据范围', options: [
{ value: 'all', label: '全部' },
{ value: '国内', label: '国内' },
{ value: '境外', label: '境外' },
], default: 'all' },
{ name: 'depth', label: '输出深度', options: [
{ value: 'brief', label: '简略' },
{ value: 'standard', label: '标准' },
{ value: 'detailed', label: '详细' },
], default: 'standard' },
],
dimensions: [
{ name: 'basic', label: '基础分析', default: true },
{ name: 'risk', label: '风险提示', default: true },
{ name: 'summary', label: '摘要结论', default: true },
],
steps: ['已识别输入对象', '已获取数据', '已完成核心分析', '已生成结果'],
previewFields: [
{ key: 'title', label: '信息标题' },
{ key: 'time', label: '发布时间' },
{ key: 'source', label: '信息来源' },
],
},
'info-pg': {
id: 'info-pg',
name: '信息PG',
description: '围绕单条或多条信息进行PG向分析',
icon: User,
iconColor: 'text-cyan-500',
bgColor: 'bg-cyan-500/10',
inputs: [
{ name: 'mainInput', label: '信息文本', placeholder: '请输入信息内容...', type: 'textarea', required: true },
{ name: 'materials', label: '上传材料', placeholder: '支持 txt, pdf, docx', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: '180d', label: '近半年' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'count', label: '抓取条数', options: [
{ value: '50', label: '50条' },
{ value: '100', label: '100条' },
{ value: '200', label: '200条' },
{ value: '500', label: '500条' },
], default: '100' },
],
dimensions: [
{ name: 'profile', label: '画像分析', default: true },
{ name: 'tendency', label: '内容倾向', default: true },
{ name: 'behavior', label: '行为特征', default: true },
],
steps: ['已识别输入对象', '已获取数据', '已完成画像分析', '已生成结果'],
previewFields: [
{ key: 'title', label: '信息标题' },
{ key: 'author', label: '发布者' },
{ key: 'type', label: '内容类型' },
],
},
'info-spread': {
id: 'info-spread',
name: '信息传播分析',
description: '分析信息传播路径、传播范围与传播表现',
icon: Network,
iconColor: 'text-teal-500',
bgColor: 'bg-teal-500/10',
inputs: [
{ name: 'link', label: '信息链接', placeholder: '请粘贴信息链接', type: 'link', required: true },
{ name: 'text', label: '信息文本', placeholder: '可选,补充信息文本', type: 'textarea', required: false },
],
params: [
{ name: 'scope', label: '传播范围', options: [
{ value: 'all', label: '全部' },
{ value: 'domestic', label: '国内' },
{ value: 'overseas', label: '境外' },
], default: 'all' },
{ name: 'timeRange', label: '时间范围', options: [
{ value: '7d', label: '近7天' },
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
], default: '7d' },
],
dimensions: [
{ name: 'path', label: '传播路径', default: true },
{ name: 'nodes', label: '传播节点', default: true },
{ name: 'performance', label: '传播表现', default: true },
{ name: 'impact', label: '影响摘要', default: true },
],
steps: ['已识别信息', '已获取传播数据', '已完成传播路径分析', '已生成结果'],
previewFields: [
{ key: 'title', label: '信息标题' },
{ key: 'originalPlatform', label: '原始平台' },
{ key: 'totalSpread', label: '总传播量' },
],
},
'account-pg': {
id: 'account-pg',
name: '账号PG',
description: '围绕账号内容与行为特征进行PG分析',
icon: Users,
iconColor: 'text-purple-500',
bgColor: 'bg-purple-500/10',
inputs: [
{ name: 'accountId', label: '账号ID/链接', placeholder: '请输入账号ID或粘贴账号链接', type: 'text', required: true },
{ name: 'materials', label: '上传内容包', placeholder: '支持 json, csv, zip', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: '180d', label: '近半年' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'count', label: '抓取条数', options: [
{ value: '50', label: '50条' },
{ value: '100', label: '100条' },
{ value: '200', label: '200条' },
{ value: '500', label: '500条' },
], default: '100' },
],
dimensions: [
{ name: 'profile', label: '账号画像', default: true },
{ name: 'tendency', label: '内容倾向', default: true },
{ name: 'behavior', label: '行为特征', default: true },
{ name: 'impact', label: '影响判断', default: true },
],
steps: ['已定位账号', '已抓取内容', '已完成内容倾向分析', '已完成账号画像分析', '已生成结果'],
previewFields: [
{ key: 'accountName', label: '账号名称' },
{ key: 'accountType', label: '账号类型' },
{ key: 'followers', label: '粉丝数' },
],
},
'sentiment': {
id: 'sentiment',
name: '三情分析',
description: '对事件进行舆情、社情、民情综合分析',
icon: MessageSquare,
iconColor: 'text-orange-500',
bgColor: 'bg-orange-500/10',
inputs: [
{ name: 'eventName', label: '事件名称', placeholder: '请输入事件名称', type: 'text', required: true },
{ name: 'eventLink', label: '事件链接', placeholder: '可选,粘贴事件相关链接', type: 'link', required: false },
{ name: 'materials', label: '上传材料', placeholder: '支持上传相关素材', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '7d', label: '近7天' },
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
], default: '7d' },
{ name: 'dataRange', label: '数据范围', options: [
{ value: 'all', label: '全部' },
{ value: '国内', label: '国内' },
{ value: '境外', label: '境外' },
], default: 'all' },
],
dimensions: [
{ name: 'publicSentiment', label: '舆情分析', default: true },
{ name: 'socialSentiment', label: '社情分析', default: true },
{ name: 'publicOpinion', label: '民情分析', default: true },
{ name: 'riskAlert', label: '风险提示', default: true },
],
steps: ['已识别事件', '已聚合相关内容', '已完成舆情分析', '已完成社情分析', '已完成民情分析', '已生成结果'],
previewFields: [
{ key: 'eventName', label: '事件名称' },
{ key: 'eventTime', label: '事件时间' },
{ key: 'relatedCount', label: '相关内容数' },
],
},
'event': {
id: 'event',
name: '事件分析',
description: '围绕某个事件进行整体分析',
icon: Target,
iconColor: 'text-red-500',
bgColor: 'bg-red-500/10',
inputs: [
{ name: 'eventName', label: '事件名称', placeholder: '请输入事件名称', type: 'text', required: true },
{ name: 'eventDesc', label: '事件说明', placeholder: '请补充事件相关说明', type: 'textarea', required: false },
{ name: 'materials', label: '上传材料', placeholder: '支持上传事件相关材料', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: '180d', label: '近半年' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'depth', label: '输出深度', options: [
{ value: 'brief', label: '简略' },
{ value: 'standard', label: '标准' },
{ value: 'detailed', label: '详细' },
], default: 'standard' },
],
dimensions: [
{ name: 'context', label: '事件脉络', default: true },
{ name: 'keyPoints', label: '关键节点', default: true },
{ name: 'risk', label: '风险判断', default: true },
{ name: 'suggestions', label: '初步建议', default: true },
],
steps: ['已识别事件', '已整理事件材料', '已完成脉络分析', '已完成风险判断', '已生成结果'],
previewFields: [
{ key: 'eventName', label: '事件名称' },
{ key: 'eventStage', label: '所处阶段' },
{ key: 'keyPlayers', label: '关键参与者' },
],
},
'topic': {
id: 'topic',
name: '专题分析',
description: '对某专题做集中分析',
icon: BookOpen,
iconColor: 'text-green-500',
bgColor: 'bg-green-500/10',
inputs: [
{ name: 'topicName', label: '专题名称', placeholder: '请输入专题名称', type: 'text', required: true },
{ name: 'materials', label: '上传材料', placeholder: '支持上传专题相关文本集合', type: 'upload', required: false },
],
params: [
{ name: 'depth', label: '分析深度', options: [
{ value: 'surface', label: '表面' },
{ value: 'standard', label: '标准' },
{ value: 'deep', label: '深度' },
], default: 'standard' },
{ name: 'sourceRange', label: '来源范围', options: [
{ value: 'all', label: '全部' },
{ value: '国内', label: '国内' },
{ value: '境外', label: '境外' },
], default: 'all' },
],
dimensions: [
{ name: 'structure', label: '专题结构', default: true },
{ name: 'coreIssues', label: '核心问题', default: true },
{ name: 'viewpoints', label: '观点聚合', default: true },
{ name: 'conclusions', label: '结论建议', default: true },
],
steps: ['已识别专题', '已获取相关材料', '已完成结构分析', '已聚合核心观点', '已生成结果'],
previewFields: [
{ key: 'topicName', label: '专题名称' },
{ key: 'relatedDocs', label: '相关文档' },
{ key: 'coverage', label: '覆盖范围' },
],
},
};
type ResultTab = 'overview' | 'details' | 'raw';
const LightAppRun: React.FC<LightAppRunProps> = ({ onNavigate, appId }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [currentAppId, setCurrentAppId] = useState<LightAppType>(appId || 'sentiment');
const [resultTab, setResultTab] = useState<ResultTab>('overview');
const [isRunning, setIsRunning] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [copied, setCopied] = useState(false);
const [formData, setFormData] = useState<Record<string, string>>({});
const [params, setParams] = useState<Record<string, string>>({});
const [dimensions, setDimensions] = useState<Record<string, boolean>>({});
const [hasResult, setHasResult] = useState(false);
const [result, setResult] = useState<any>(null);
const config = appConfigs[currentAppId];
const Icon = config.icon;
// 初始化表单数据
useEffect(() => {
console.log('当前应用:', currentAppId);
if (config) {
const initialForm: Record<string, string> = {};
config.inputs.forEach(input => {
initialForm[input.name] = '';
});
setFormData(initialForm);
const initialParams: Record<string, string> = {};
config.params.forEach(param => {
initialParams[param.name] = param.default;
});
setParams(initialParams);
const initialDims: Record<string, boolean> = {};
config.dimensions.forEach(dim => {
initialDims[dim.name] = dim.default;
});
setDimensions(initialDims);
}
}, [currentAppId]);
// 执行分析
const handleRun = async () => {
setIsRunning(true);
setCurrentStep(0);
setHasResult(false);
// 模拟执行步骤
for (let i = 0; i < config.steps.length; i++) {
await new Promise(resolve => setTimeout(resolve, 800));
setCurrentStep(i + 1);
}
// 生成结果
await new Promise(resolve => setTimeout(resolve, 500));
setResult(generateMockResult());
setHasResult(true);
setIsRunning(false);
};
// 生成模拟结果
const generateMockResult = () => {
switch (currentAppId) {
case 'sentiment':
return {
overview: {
overallSentiment: '中偏负',
mainConcerns: ['活动形式', '品牌调性', '执行成本'],
riskAlert: '讨论热度上升,负向占比增加',
sentimentTrend: '下行',
},
details: {
publicSentiment: '舆情方面,主要关注点是活动本身的创新性和参与体验。部分用户对活动细节提出质疑,整体讨论呈现多元化特征。',
socialSentiment: '社情方面,该事件引发了关于活动公平性的讨论,社交媒体上相关话题讨论量显著上升。',
publicOpinion: '民情方面,公众对该活动的看法分化明显,支持和反对声音并存,需关注后续舆情走向。',
riskAssessment: '当前风险等级:中。建议持续关注舆情变化,准备应对预案。',
},
raw: [
{ type: '正面评论', content: '活动创意很好,期待参与', count: 234 },
{ type: '中性评论', content: '等待活动正式开始', count: 567 },
{ type: '负面评论', content: '对活动细节表示质疑', count: 189 },
],
};
case 'event':
return {
overview: {
eventStage: '发酵期',
keyNodes: '活动公布期,关注度持续上升',
riskLevel: '中',
suggestion: '建议加强信息透明度,及时回应公众关切',
},
details: {
context: '该事件起因于...,经过...,目前处于...阶段。',
keyPoints: ['关键节点一:...', '关键节点二:...', '关键节点三:...'],
riskJudgment: '风险可控,但需关注...',
suggestions: '1. ... 2. ... 3. ...',
},
raw: [
{ time: '2026-03-18', title: '事件起因', content: '...' },
{ time: '2026-03-19', title: '初步发酵', content: '...' },
{ time: '2026-03-20', title: '关注度上升', content: '...' },
],
};
case 'account-pg':
return {
overview: {
accountProfile: '该账号为...类型账号',
contentTendency: '内容倾向明显偏...',
influenceJudgment: '具有一定影响力,需关注...',
},
details: {
profileAnalysis: '账号画像分析...',
contentAnalysis: '内容倾向分析...',
behaviorAnalysis: '行为特征分析...',
influenceAnalysis: '影响判断分析...',
},
raw: [
{ date: '2026-03-15', content: '...' },
{ date: '2026-03-16', content: '...' },
{ date: '2026-03-17', content: '...' },
],
};
default:
return {
overview: { summary: '分析结果摘要...' },
details: { analysis: '详细分析内容...' },
raw: [],
};
}
};
// 重置
const handleReset = () => {
const initialForm: Record<string, string> = {};
config.inputs.forEach(input => {
initialForm[input.name] = '';
});
setFormData(initialForm);
const initialParams: Record<string, string> = {};
config.params.forEach(param => {
initialParams[param.name] = param.default;
});
setParams(initialParams);
const initialDims: Record<string, boolean> = {};
config.dimensions.forEach(dim => {
initialDims[dim.name] = dim.default;
});
setDimensions(initialDims);
setHasResult(false);
setResult(null);
setCurrentStep(0);
};
// 复制结果
const handleCopy = () => {
navigator.clipboard.writeText(JSON.stringify(result, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
// 保存快照
const handleSaveSnapshot = () => {
// 模拟保存
alert('快照已保存到运行记录');
onNavigate('light-app-records');
};
// 导出
const handleExport = () => {
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${config.name}_结果_${new Date().toISOString().split('T')[0]}.json`;
a.click();
};
return (
<div className="space-y-4">
{/* 顶部操作栏 */}
<div className={`rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => onNavigate('light-app-square')}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'
}`}
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${config.bgColor}`}>
<Icon className={`w-5 h-5 ${config.iconColor}`} />
</div>
<div>
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{config.name}
</h1>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{config.description}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleSaveSnapshot}
disabled={!hasResult}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
hasResult
? isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
: isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
}`}
>
<Save className="w-4 h-4" />
保存快照
</button>
<button
onClick={handleExport}
disabled={!hasResult}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
hasResult
? isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
: isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
}`}
>
<Download className="w-4 h-4" />
导出结果
</button>
<button
onClick={() => onNavigate('task-create')}
disabled={!hasResult}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
hasResult
? isDark
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30'
: 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200'
: isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed border border-transparent'
: 'bg-gray-50 text-gray-400 cursor-not-allowed border border-transparent'
}`}
>
<Send className="w-4 h-4" />
转正式任务
</button>
</div>
</div>
</div>
{/* 主体内容区 - 三栏布局 */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4">
{/* 左侧:输入与配置 */}
<div className={`lg:col-span-4 rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="space-y-6">
{/* 输入对象 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
输入对象
</h3>
<div className="space-y-3">
{config.inputs.map(input => (
<div key={input.name}>
<label className={`block text-xs mb-1.5 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{input.label}
{input.required && <span className="text-red-500 ml-1">*</span>}
</label>
{input.type === 'textarea' ? (
<textarea
value={formData[input.name] || ''}
onChange={(e) => setFormData({ ...formData, [input.name]: e.target.value })}
placeholder={input.placeholder}
rows={4}
className={`w-full px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500'
}`}
/>
) : input.type === 'upload' ? (
<div className={`border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors ${
isDark
? 'border-slate-600 hover:border-blue-500/50'
: 'border-gray-300 hover:border-red-500/50'
}`}>
<Upload className={`w-6 h-6 mx-auto mb-2 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{input.placeholder}
</p>
</div>
) : (
<input
type="text"
value={formData[input.name] || ''}
onChange={(e) => setFormData({ ...formData, [input.name]: e.target.value })}
placeholder={input.placeholder}
className={`w-full px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500'
}`}
/>
)}
</div>
))}
</div>
</div>
{/* 参数配置 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
参数配置
</h3>
<div className="space-y-3">
{config.params.map(param => (
<div key={param.name}>
<label className={`block text-xs mb-1.5 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{param.label}
</label>
<select
value={params[param.name] || param.default}
onChange={(e) => setParams({ ...params, [param.name]: e.target.value })}
className={`w-full px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-900 focus:border-red-500'
}`}
>
{param.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
))}
</div>
</div>
{/* 分析维度 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
分析维度
</h3>
<div className="space-y-2">
{config.dimensions.map(dim => (
<label
key={dim.name}
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-colors ${
isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'
}`}
>
<input
type="checkbox"
checked={dimensions[dim.name] ?? dim.default}
onChange={(e) => setDimensions({ ...dimensions, [dim.name]: e.target.checked })}
className="w-4 h-4 rounded border-gray-300"
/>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{dim.label}
</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* 中间:执行过程/对象预览 */}
<div className={`lg:col-span-4 rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="space-y-6">
{/* 执行过程 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
执行过程
</h3>
<div className="space-y-2">
{config.steps.map((step, index) => (
<div key={index} className="flex items-center gap-3">
{isRunning && index === currentStep ? (
<Loader2 className={`w-4 h-4 animate-spin ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
) : index < currentStep || hasResult ? (
<CheckCircle2 className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
) : (
<Circle className={`w-4 h-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
)}
<span className={`text-sm ${
index < currentStep || hasResult
? isDark ? 'text-slate-300' : 'text-gray-700'
: isDark ? 'text-slate-500' : 'text-gray-400'
}`}>
{index + 1}. {step}
</span>
</div>
))}
</div>
</div>
{/* 对象预览 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对象预览
</h3>
<div className={`rounded-lg p-4 ${
isDark ? 'bg-slate-900/50' : 'bg-gray-50'
}`}>
{formData.mainInput || formData.eventName || formData.accountId || formData.link || formData.topicName ? (
<div className="space-y-2">
{config.previewFields.map(field => (
<div key={field.key} className="flex items-start gap-2">
<span className={`text-xs font-medium min-w-[60px] ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{field.label}:
</span>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{formData.mainInput?.slice(0, 30) || formData.eventName || formData.accountId || formData.link || formData.topicName}
</span>
</div>
))}
</div>
) : (
<p className={`text-sm text-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
请在左侧输入对象后预览
</p>
)}
</div>
</div>
</div>
</div>
{/* 右侧:结果区 */}
<div className={`lg:col-span-4 rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between mb-4">
<h3 className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
分析结果
</h3>
{hasResult && (
<div className="flex items-center gap-1">
<button
onClick={handleCopy}
className={`p-1.5 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'
}`}
>
{copied ? <CheckCheck className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</button>
</div>
)}
</div>
{/* 结果标签页 */}
{hasResult ? (
<>
<div className="flex gap-1 mb-4">
{(['overview', 'details', 'raw'] as ResultTab[]).map(tab => (
<button
key={tab}
onClick={() => setResultTab(tab)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
resultTab === tab
? isDark
? 'bg-blue-500/20 text-blue-400'
: 'bg-red-50 text-red-600'
: isDark
? 'text-slate-400 hover:text-slate-300'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab === 'overview' ? '概览' : tab === 'details' ? '详细结果' : '原始数据'}
</button>
))}
</div>
{/* 结果内容 */}
<div className={`rounded-lg p-4 ${
isDark ? 'bg-slate-900/50' : 'bg-gray-50'
}`}>
{resultTab === 'overview' && (
<div className="space-y-3">
{Object.entries(result?.overview || {}).map(([key, value]) => (
<div key={key}>
<span className={`text-xs font-medium ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{key === 'overallSentiment' ? '总体情绪' :
key === 'mainConcerns' ? '主要关注点' :
key === 'riskAlert' ? '风险提示' :
key === 'sentimentTrend' ? '情绪趋势' :
key === 'eventStage' ? '事件阶段' :
key === 'keyNodes' ? '关键节点' :
key === 'riskLevel' ? '风险等级' :
key === 'suggestion' ? '建议' :
key === 'accountProfile' ? '账号画像' :
key === 'contentTendency' ? '内容倾向' :
key === 'influenceJudgment' ? '影响判断' :
key === 'summary' ? '结果摘要' : key}:
</span>
<p className={`text-sm mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{Array.isArray(value) ? value.join('、') : String(value)}
</p>
</div>
))}
</div>
)}
{resultTab === 'details' && (
<div className="space-y-3">
{Object.entries(result?.details || {}).map(([key, value]) => (
<div key={key}>
<span className={`text-xs font-medium ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{key === 'publicSentiment' ? '舆情分析' :
key === 'socialSentiment' ? '社情分析' :
key === 'publicOpinion' ? '民情分析' :
key === 'riskAssessment' ? '风险评估' :
key === 'context' ? '事件脉络' :
key === 'keyPoints' ? '关键节点' :
key === 'riskJudgment' ? '风险判断' :
key === 'suggestions' ? '建议' :
key === 'profileAnalysis' ? '画像分析' :
key === 'contentAnalysis' ? '内容分析' :
key === 'behaviorAnalysis' ? '行为分析' :
key === 'influenceAnalysis' ? '影响分析' :
key === 'analysis' ? '分析' : key}:
</span>
<p className={`text-sm mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{Array.isArray(value) ? value.join('\n') : String(value)}
</p>
</div>
))}
</div>
)}
{resultTab === 'raw' && (
<div className="space-y-2">
{(result?.raw || []).length > 0 ? (
result.raw.map((item: any, index: number) => (
<div key={index} className={`p-2 rounded ${isDark ? 'bg-slate-800/50' : 'bg-white'}`}>
<div className="flex items-center gap-2 mb-1">
{item.type && (
<span className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-blue-50 text-blue-600'
}`}>
{item.type}
</span>
)}
{item.time && (
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{item.time}
</span>
)}
{item.date && (
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{item.date}
</span>
)}
</div>
<p className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{item.content || item.title || JSON.stringify(item)}
</p>
</div>
))
) : (
<p className={`text-sm text-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
无原始数据
</p>
)}
</div>
)}
</div>
</>
) : (
<div className="text-center py-12">
<Sparkles className={`w-12 h-12 mx-auto mb-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{isRunning ? '分析中...' : '点击下方按钮开始分析'}
</p>
</div>
)}
</div>
</div>
{/* 底部操作区 */}
<div className={`rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between">
<button
onClick={handleReset}
disabled={isRunning}
className={`px-6 py-2.5 rounded-lg font-medium flex items-center gap-2 transition-colors ${
isRunning
? isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
: isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<RotateCcw className="w-4 h-4" />
重置
</button>
<button
onClick={handleRun}
disabled={isRunning || !formData.mainInput && !formData.eventName && !formData.accountId && !formData.link && !formData.topicName}
className={`px-8 py-2.5 rounded-lg font-medium flex items-center gap-2 transition-all ${
isRunning
? isDark
? 'bg-blue-500/20 text-blue-400 cursor-wait'
: 'bg-red-50 text-red-400 cursor-wait'
: formData.mainInput || formData.eventName || formData.accountId || formData.link || formData.topicName
? isDark
? 'bg-blue-600 text-white hover:bg-blue-500 shadow-lg shadow-blue-500/25'
: 'bg-red-500 text-white hover:bg-red-600 shadow-lg shadow-red-500/25'
: isDark
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{isRunning ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
分析中...
</>
) : (
<>
<PlayCircle className="w-4 h-4" />
开始分析
</>
)}
</button>
</div>
</div>
</div>
);
};
export default LightAppRun;
+681
View File
@@ -0,0 +1,681 @@
import React, { useState, useMemo, useEffect } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
Search,
ChevronRight,
PlayCircle,
Clock,
Star,
FileText,
User,
Zap,
BookOpen,
TrendingUp,
MessageSquare,
Network,
Link2,
Filter,
Layout,
Sparkles,
BarChart3,
Users,
Target,
PieChart,
Send,
Plus,
} from 'lucide-react';
import { Button, Dialog, Radio, Upload, Input, MessagePlugin, Row, Col, Textarea } from 'tdesign-react';
import { get } from '../lib/api';
import type { UploadFile, UploadProps } from 'tdesign-react';
import { requestCrudService } from '@/service/request-axios-service';
interface LightAppSquareProps {
onNavigate: (page: string, appId?: string) => void;
}
// 定义轻应用类型
export type LightAppType =
| 'info-ty' // 信息TY
| 'info-pg' // 信息PG
| 'info-spread' // 信息传播分析
| 'account-pg' // 账号PG
| 'sentiment' // 三情分析
| 'event' // 事件分析
| 'topic'; // 专题分析
// 应用分类
type AppCategory = 'info' | 'account' | 'event' | 'topic';
interface LightApp {
id: LightAppType;
name: string;
description: string;
category: AppCategory;
categoryName: string;
input: string;
output: string;
icon: React.ElementType;
color: string;
bgColor: string;
borderColor: string;
iconColor: string;
dimensions?: string[]; // 可选的额外分析维度
}
interface analysisDataItem {
appName: string;
appDesc: string;
appIcon: string;
jumpUrl: string;
appType: number
}
interface analysisListItem {
categoryName: string;
category: string;
appName: string;
appDesc: string;
appIcon: string;
jumpUrl: string;
appType: number
}
interface analysisListData {
internalList: analysisListItem[];
externalList: analysisListItem[];
}
// 应用数据
const lightApps: LightApp[] = [
// 信息分析类
{
id: 'info-ty',
name: '信息TY',
description: '围绕单条或多条信息进行TY向分析',
category: 'info',
categoryName: '信息分析类',
input: '信息文本/链接/素材',
output: '信息特征、重点摘要、分析判断',
icon: FileText,
color: 'blue',
bgColor: 'bg-blue-500/10',
borderColor: 'border-blue-500/30',
iconColor: 'text-blue-500',
dimensions: ['基础分析', '风险提示', '摘要结论'],
},
{
id: 'info-pg',
name: '信息PG',
description: '围绕单条或多条信息进行PG向分析',
category: 'info',
categoryName: '信息分析类',
input: '信息文本/链接/素材',
output: '画像特征、相关判断',
icon: User,
color: 'cyan',
bgColor: 'bg-cyan-500/10',
borderColor: 'border-cyan-500/30',
iconColor: 'text-cyan-500',
dimensions: ['账号画像', '内容倾向', '行为特征'],
},
{
id: 'info-spread',
name: '信息传播分析',
description: '分析信息传播路径、传播范围与传播表现',
category: 'info',
categoryName: '信息分析类',
input: '信息链接/信息集合',
output: '传播过程、传播节点、传播影响',
icon: Network,
color: 'teal',
bgColor: 'bg-teal-500/10',
borderColor: 'border-teal-500/30',
iconColor: 'text-teal-500',
dimensions: ['传播路径', '传播节点', '传播表现', '影响摘要'],
},
// 账号分析类
{
id: 'account-pg',
name: '账号PG',
description: '围绕账号内容与行为特征进行PG分析',
category: 'account',
categoryName: '账号分析类',
input: '账号ID/账号链接/内容集合',
output: '账号画像、内容倾向、行为特征',
icon: Users,
color: 'purple',
bgColor: 'bg-purple-500/10',
borderColor: 'border-purple-500/30',
iconColor: 'text-purple-500',
dimensions: ['账号画像', '内容倾向', '行为特征', '影响判断'],
},
// 事件分析类
{
id: 'sentiment',
name: '三情分析',
description: '对事件进行舆情、社情、民情综合分析',
category: 'event',
categoryName: '事件分析类',
input: '事件关键词/事件链接/素材',
output: '情绪概览、关注焦点、风险提示',
icon: MessageSquare,
color: 'orange',
bgColor: 'bg-orange-500/10',
borderColor: 'border-orange-500/30',
iconColor: 'text-orange-500',
dimensions: ['舆情分析', '社情分析', '民情分析', '风险提示'],
},
{
id: 'event',
name: '事件分析',
description: '围绕某个事件进行整体分析',
category: 'event',
categoryName: '事件分析类',
input: '事件名称/事件材料/事件链接',
output: '事件脉络、关键节点、风险判断',
icon: Target,
color: 'red',
bgColor: 'bg-red-500/10',
borderColor: 'border-red-500/30',
iconColor: 'text-red-500',
dimensions: ['事件脉络', '关键节点', '风险判断', '初步建议'],
},
// 专题分析类
{
id: 'topic',
name: '专题分析',
description: '对某专题做集中分析',
category: 'topic',
categoryName: '专题分析类',
input: '专题名称/专题材料/文本集合',
output: '专题结构、核心问题、观点聚合、结论建议',
icon: BookOpen,
color: 'green',
bgColor: 'bg-green-500/10',
borderColor: 'border-green-500/30',
iconColor: 'text-green-500',
dimensions: ['专题结构', '核心问题', '观点聚合', '结论建议'],
},
];
const categoryOrder: AppCategory[] = ['info', 'account', 'event', 'topic'];
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const LightAppSquare: React.FC<LightAppSquareProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<AppCategory | 'all'>('all');
const [recentApps, setRecentApps] = useState<LightAppType[]>(['sentiment', 'event', 'account-pg']);
const [visible, setVisible] = useState(false);
const [analysisData, setAnalysisData] = useState<analysisDataItem>({
appName: '',
appDesc: '',
appIcon: '',
jumpUrl: '',
appType: 2,
});
const [analysisList, setAnalysisList] = useState<analysisListData>(
{
internalList: [], // 内部应用
externalList: [], // 外部应用
})
const [fileFail, setFileFail] = useState<UploadFile[]>([
// 如果需要默认回显图片,可以在这里初始化
// { name: '默认图片.png', url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'fail' }
]);
const showImageFileName = true;
const service = requestCrudService;
// 解构出 Group 和 Button,方便使用
const { Group: RadioGroup, Button: RadioButton } = Radio;
// 用于格式化接口返回的数据,使其符合 TDesign 的要求
const formatResponse = (res: any) => {
// 假设接口返回 { code: 0, data: { url: '...' } }
// 如果接口报错,可以在这里处理,让组件显示失败状态
if (res.code !== 0) {
return {
error: res.message || '上传失败',
url: '',
name: ''
};
}
return {
url: res.data.url,
name: res.data.name,
};
};
const handleChange: UploadProps['onChange'] = (files) => {
setFileFail(files);
console.log('当前文件列表:', files);
};
// 过滤应用
const filteredApps = useMemo(() => {
return lightApps.filter(app => {
const matchesSearch = searchQuery === '' ||
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.input.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = categoryFilter === 'all' || app.category === categoryFilter;
return matchesSearch && matchesCategory;
});
}, [searchQuery, categoryFilter]);
// 按分类分组
const groupedApps = useMemo(() => {
const groups: Record<AppCategory, LightApp[]> = {
info: [],
account: [],
event: [],
topic: [],
};
filteredApps.forEach(app => {
groups[app.category].push(app);
});
return groups;
}, [filteredApps]);
// 处理应用启动
const handleLaunchApp = (app: LightApp) => {
console.log('启动应用:', app);
// 更新最近使用
setRecentApps(prev => {
const filtered = prev.filter(id => id !== app.id);
return [app.id, ...filtered].slice(0, 5);
});
onNavigate('light-app-run', app.id);
};
// 获取分类颜色
const getCategoryColor = (category: AppCategory) => {
const colors: Record<AppCategory, { text: string; bg: string }> = {
info: { text: isDark ? 'text-blue-400' : 'text-blue-600', bg: isDark ? 'bg-blue-500/20' : 'bg-blue-50' },
account: { text: isDark ? 'text-purple-400' : 'text-purple-600', bg: isDark ? 'bg-purple-500/20' : 'bg-purple-50' },
event: { text: isDark ? 'text-orange-400' : 'text-orange-600', bg: isDark ? 'bg-orange-500/20' : 'bg-orange-50' },
topic: { text: isDark ? 'text-green-400' : 'text-green-600', bg: isDark ? 'bg-green-500/20' : 'bg-green-50' },
};
return colors[category];
};
// 获取应用列表
const analysisListSearch = async () => {
const parmy = {
appName: '',
pageNum: 1,
pageSize: 10,
}
const res = await service.post(`${zncmUrl}/app/analysis/list`, parmy)
console.log(res);
const result = res.data;
const listData: analysisListData = {
internalList: [],
externalList: [],
}
result.records.map((item: analysisListItem) => {
if (item.appType === 1) {
item.category = 'info';
item.categoryName = '内部应用';
listData.internalList.push(item)
} else if (item.appType === 2) {
item.category = 'account';
item.categoryName = '外部应用';
listData.internalList.push(item)
}
})
}
// 添加应用
const analysieAddListChange = async () => {
const res = await service.post(`${zncmUrl}/app/analysis/add`, analysisData);
console.log(res);
MessagePlugin.success('添加成功');
}
const showDialog = () => {
setAnalysisData({
appName: '',
appDesc: '',
appIcon: '',
jumpUrl: '',
appType: 2,
})
setVisible(true);
};
const handleOk = async () => {
await analysieAddListChange();
setVisible(false);
};
const handleClose = () => {
setVisible(false);
};
const getBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const imgUpload: UploadProps['requestMethod'] = async (file: any) => {
console.log('file', file);
const base64 = await getBase64(file.raw)
handleDataChange('appIcon', base64);
return {
status: 'success',
// response 对象里的数据会传给 formatResponse
response: {
url: '',
name: '',
},
}
}
// 通用的处理函数
const handleDataChange = (key: keyof typeof analysisData, value: string | number) => {
setAnalysisData((prev) => ({
...prev,
[key]: value,
}));
};
// 发送请求
useEffect(() => {
analysisListSearch();
}, []);
return (
<div className="space-y-6">
{/* 顶部工具栏 */}
<div className={`rounded-xl border p-4 ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* 面包屑和标题 */}
<div className="flex items-center gap-2 flex-1">
<Layout className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>轻应用中心 /</span>
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>应用广场</span>
</div>
{/* 搜索框 */}
<div className="flex-1 max-w-md">
<div className="relative">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索应用名称或分析对象..."
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-colors focus:outline-none focus:ring-2 ${isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500 focus:ring-blue-500/20'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20'
}`}
/>
</div>
</div>
{/* 筛选和操作 */}
<div className="flex items-center gap-3">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value as AppCategory | 'all')}
className={`px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${isDark
? 'bg-slate-900 border-slate-600 text-slate-200 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-700 focus:border-red-500'
}`}
>
<option value="all">全部分类</option>
<option value="info">信息分析类</option>
<option value="account">账号分析类</option>
<option value="event">事件分析类</option>
<option value="topic">专题分析类</option>
</select>
<button
onClick={() => onNavigate('light-app-records')}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<Clock className="w-4 h-4" />
运行记录
</button>
<Button theme="primary" onClick={showDialog}>
<span className="flex items-center">
<Plus className="w-4 h-4 mr-[10px]" />
添加
</span>
</Button>
</div>
</div>
</div>
{/* 最近使用 */}
{recentApps.length > 0 && searchQuery === '' && categoryFilter === 'all' && (
<div className={`rounded-xl border p-4 ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center gap-2 mb-4">
<Clock className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<h3 className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>最近使用</h3>
</div>
<div className="flex flex-wrap gap-2">
{recentApps.map(appId => {
const app = lightApps.find(a => a.id === appId);
if (!app) return null;
const Icon = app.icon;
return (
<button
key={app.id}
onClick={() => handleLaunchApp(app)}
className={`px-4 py-2 rounded-lg border text-sm font-medium flex items-center gap-2 transition-all hover:scale-105 ${isDark
? `bg-slate-700/50 border-slate-600 text-slate-300 hover:bg-slate-700`
: `bg-gray-50 border-gray-200 text-gray-700 hover:bg-gray-100`
}`}
>
<Icon className={`w-4 h-4 ${app.iconColor}`} />
{app.name}
</button>
);
})}
</div>
</div>
)}
{/* 应用分组区 */}
<div className="space-y-6">
{categoryOrder.map(category => {
const apps = groupedApps[category];
if (apps.length === 0) return null;
const categoryInfo = lightApps.find(a => a.category === category);
const catColor = getCategoryColor(category);
return (
<div key={category} className="space-y-4">
{/* 分类标题 */}
<div className="flex items-center gap-2">
<h2 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{categoryInfo?.categoryName}
</h2>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${catColor.bg} ${catColor.text}`}>
{apps.length} 个应用
</span>
</div>
{/* 应用卡片网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{apps.map(app => {
const Icon = app.icon;
return (
<div
key={app.id}
className={`rounded-xl border p-5 transition-all duration-200 hover:shadow-lg group ${isDark
? 'bg-slate-800/50 border-slate-700 hover:border-slate-600'
: 'bg-white border-gray-200 hover:border-gray-300'
}`}
>
{/* 应用头部 */}
<div className="flex items-start gap-4 mb-4">
<div className={`p-3 rounded-lg ${app.bgColor}`}>
<Icon className={`w-6 h-6 ${app.iconColor}`} />
</div>
<div className="flex-1 min-w-0">
<h3 className={`font-semibold group-hover:${app.iconColor} ${isDark ? 'text-white' : 'text-gray-900'}`}>
{app.name}
</h3>
<p className={`mt-1 text-sm line-clamp-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{app.description}
</p>
</div>
</div>
{/* 输入输出 */}
<div className={`text-xs space-y-1 mb-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<div className="flex items-start gap-2">
<span className="font-medium">输入:</span>
<span>{app.input}</span>
</div>
<div className="flex items-start gap-2">
<span className="font-medium">输出:</span>
<span>{app.output}</span>
</div>
</div>
{/* 立即使用按钮 */}
<button
onClick={() => handleLaunchApp(app)}
className={`w-full py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 transition-all ${isDark
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30'
: 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200'
}`}
>
<PlayCircle className="w-4 h-4" />
立即使用
</button>
</div>
);
})}
</div>
</div>
);
})}
</div>
{/* 空状态 */}
{filteredApps.length === 0 && (
<div className={`text-center py-12 rounded-xl border ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<Search className={`w-12 h-12 mx-auto mb-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
未找到匹配的应用,请尝试其他搜索词或筛选条件
</p>
</div>
)}
{/* 底部提示 */}
<div className={`p-4 rounded-xl border ${isDark ? 'bg-blue-500/10 border-blue-500/30' : 'bg-blue-50 border-blue-200'
}`}>
<div className="flex items-start gap-3">
<Sparkles className={`w-5 h-5 mt-0.5 ${isDark ? 'text-blue-400' : 'text-blue-500'}`} />
<div>
<p className={`text-sm font-medium ${isDark ? 'text-blue-300' : 'text-blue-800'}`}>
关于轻应用中心
</p>
<p className={`mt-1 text-sm ${isDark ? 'text-blue-400/80' : 'text-blue-700'}`}>
轻应用中心提供了一系列快速分析工具,适合处理临时性的分析需求。相比完整的任务级应用,轻应用更加轻量便捷,可以快速调用多种工具完成分析工作。运行结果可保存快照、导出或转为正式任务。
</p>
</div>
</div>
</div>
<Dialog
header="添加应用"
visible={visible}
onClose={handleClose} // 监听关闭事件
onConfirm={handleOk} // 点击确定按钮
onCancel={handleClose} // 点击取消按钮
closeOnOverlayClick={true} // 点击遮罩层是否关闭
>
<div className='mb-[10px]'>
<Upload
// 对应 v-model
files={fileFail}
onChange={handleChange}
// 对应 theme="image"
theme="image"
requestMethod={imgUpload}
// 对应 accept
accept="image/*"
tips="系统logo"
// 对应 :show-image-file-name
showImageFileName={showImageFileName}
// 对应 :format-response
formatResponse={formatResponse}
// 可选:限制上传数量(演示单张)
multiple={false}
// 可选:上传失败时的提示(如果接口直接返回 4xx/5xx)
onFail={({ e, file }) => {
MessagePlugin.error(`文件 ${file.name} 上传失败`);
}} />
</div>
<div className='mb-[10px] overflow-hidden'>
<Row>
<Col span={2}>
<div>名称:</div>
</Col>
<Col span={9}>
<Input placeholder="请输入应用名称" value={analysisData.appName} onChange={(val) => handleDataChange('appName', val)} clearable />
</Col>
</Row>
</div>
<div className='mb-[10px] overflow-hidden'>
<Row>
<Col span={2}>
<div>详情:</div>
</Col>
<Col span={9}>
<Textarea placeholder="请输入应用详情" value={analysisData.appDesc} onChange={(val) => handleDataChange('appDesc', val)} autosize />
</Col>
</Row>
</div>
<div className='mb-[10px] overflow-hidden'>
<Row>
<Col span={2}>
<div>类型:</div>
</Col>
<Col span={9}>
<RadioGroup
variant="primary-filled" // 对应 variant="primary-filled"
value={analysisData.appType} // 绑定状态值
onChange={(value) => handleDataChange('appType', Number(value))}
>
{/* 3. 使用 RadioButton */}
<RadioButton value={1}>内部应用</RadioButton>
<RadioButton value={2}>外部应用</RadioButton>
<RadioButton value={3}>第三方集成</RadioButton>
</RadioGroup>
</Col>
</Row>
</div>
<div className='mb-[10px] overflow-hidden'>
<Row>
<Col span={2}>
<div>链接:</div>
</Col>
<Col span={9}>
<Input placeholder="请输入链接" value={analysisData.jumpUrl} onChange={(val) => handleDataChange('jumpUrl', val)} clearable />
</Col>
</Row>
</div>
</Dialog>
</div>
);
};
export default LightAppSquare;
+979
View File
@@ -0,0 +1,979 @@
import React, { useState, useMemo, useEffect, useRef } from "react";
import { useTheme } from "../contexts/ThemeContext";
import {
Search,
ChevronRight,
PlayCircle,
Clock,
Star,
FileText,
User,
Zap,
BookOpen,
TrendingUp,
MessageSquare,
Network,
Link2,
Filter,
Layout,
Sparkles,
BarChart3,
Users,
Target,
PieChart,
Send,
Plus,
} from "lucide-react";
import {
Button,
Dialog,
Radio,
Upload,
Input,
MessagePlugin,
Row,
Col,
Textarea,
Loading,
Empty,
Pagination,
} from "tdesign-react";
import { get } from "../lib/api";
import type { UploadFile, UploadProps } from "tdesign-react";
import { requestCrudService } from "@/service/request-axios-service";
import { AppCard } from "@/components/square/AppCard";
interface LightAppSquareProps {
onNavigate: (page: string, appId?: string) => void;
}
// 定义轻应用类型
export type LightAppType =
| "info-ty" // 信息TY
| "info-pg" // 信息PG
| "info-spread" // 信息传播分析
| "account-pg" // 账号PG
| "sentiment" // 三情分析
| "event" // 事件分析
| "topic"; // 专题分析
// 应用分类
type AppCategory = "info" | "account" | "event" | "topic";
interface LightApp {
id: LightAppType;
name: string;
description: string;
category: AppCategory;
categoryName: string;
input: string;
output: string;
icon: React.ElementType;
color: string;
bgColor: string;
borderColor: string;
iconColor: string;
dimensions?: string[]; // 可选的额外分析维度
}
interface analysisDataItem {
appName: string;
appDesc: string;
appIcon: string;
jumpUrl: string;
appType: number;
}
interface analysisListItem {
id: number;
categoryName: string;
category: string;
appName: string;
appDesc: string;
appIcon: string;
jumpUrl: string;
appType: number;
}
interface analysisListData {
key: "info" | "account" | "event" | "topic";
list: analysisListItem[];
}
// 应用数据
const lightApps: LightApp[] = [
// 信息分析类
{
id: "info-ty",
name: "信息TY",
description: "围绕单条或多条信息进行TY向分析",
category: "info",
categoryName: "信息分析类",
input: "信息文本/链接/素材",
output: "信息特征、重点摘要、分析判断",
icon: FileText,
color: "blue",
bgColor: "bg-blue-500/10",
borderColor: "border-blue-500/30",
iconColor: "text-blue-500",
dimensions: ["基础分析", "风险提示", "摘要结论"],
},
{
id: "info-pg",
name: "信息PG",
description: "围绕单条或多条信息进行PG向分析",
category: "info",
categoryName: "信息分析类",
input: "信息文本/链接/素材",
output: "画像特征、相关判断",
icon: User,
color: "cyan",
bgColor: "bg-cyan-500/10",
borderColor: "border-cyan-500/30",
iconColor: "text-cyan-500",
dimensions: ["账号画像", "内容倾向", "行为特征"],
},
{
id: "info-spread",
name: "信息传播分析",
description: "分析信息传播路径、传播范围与传播表现",
category: "info",
categoryName: "信息分析类",
input: "信息链接/信息集合",
output: "传播过程、传播节点、传播影响",
icon: Network,
color: "teal",
bgColor: "bg-teal-500/10",
borderColor: "border-teal-500/30",
iconColor: "text-teal-500",
dimensions: ["传播路径", "传播节点", "传播表现", "影响摘要"],
},
// 账号分析类
{
id: "account-pg",
name: "账号PG",
description: "围绕账号内容与行为特征进行PG分析",
category: "account",
categoryName: "账号分析类",
input: "账号ID/账号链接/内容集合",
output: "账号画像、内容倾向、行为特征",
icon: Users,
color: "purple",
bgColor: "bg-purple-500/10",
borderColor: "border-purple-500/30",
iconColor: "text-purple-500",
dimensions: ["账号画像", "内容倾向", "行为特征", "影响判断"],
},
// 事件分析类
{
id: "sentiment",
name: "三情分析",
description: "对事件进行舆情、社情、民情综合分析",
category: "event",
categoryName: "事件分析类",
input: "事件关键词/事件链接/素材",
output: "情绪概览、关注焦点、风险提示",
icon: MessageSquare,
color: "orange",
bgColor: "bg-orange-500/10",
borderColor: "border-orange-500/30",
iconColor: "text-orange-500",
dimensions: ["舆情分析", "社情分析", "民情分析", "风险提示"],
},
{
id: "event",
name: "事件分析",
description: "围绕某个事件进行整体分析",
category: "event",
categoryName: "事件分析类",
input: "事件名称/事件材料/事件链接",
output: "事件脉络、关键节点、风险判断",
icon: Target,
color: "red",
bgColor: "bg-red-500/10",
borderColor: "border-red-500/30",
iconColor: "text-red-500",
dimensions: ["事件脉络", "关键节点", "风险判断", "初步建议"],
},
// 专题分析类
{
id: "topic",
name: "专题分析",
description: "对某专题做集中分析",
category: "topic",
categoryName: "专题分析类",
input: "专题名称/专题材料/文本集合",
output: "专题结构、核心问题、观点聚合、结论建议",
icon: BookOpen,
color: "green",
bgColor: "bg-green-500/10",
borderColor: "border-green-500/30",
iconColor: "text-green-500",
dimensions: ["专题结构", "核心问题", "观点聚合", "结论建议"],
},
];
const categoryConfig = {
info: {
title: "内部应用",
hoverColor: "group-hover:text-blue-500", // 内部应用 hover 蓝色
btnTheme: {
dark: "bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border-blue-500/30",
light: "bg-blue-50 text-blue-600 hover:bg-blue-100 border-blue-200",
},
},
account: {
title: "外部应用",
hoverColor: "group-hover:text-purple-500", // 外部应用 hover 紫色
btnTheme: {
dark: "bg-purple-500/20 text-purple-400 hover:bg-purple-500/30 border-purple-500/30",
light: "bg-red-50 text-red-600 hover:bg-red-100 border-red-200", // 保留你原本外部应用的红色风格
},
},
};
const categoryOrder: AppCategory[] = ["info", "account", "event", "topic"];
const zncmUrl = import.meta.env.VITE_API_BASE_URL || "";
const LightAppSquare: React.FC<LightAppSquareProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === "dark";
const [searchQuery, setSearchQuery] = useState("");
const [categoryFilter, setCategoryFilter] = useState<AppCategory | "all">(
"all"
);
const [recentApps, setRecentApps] = useState<LightAppType[]>([
"sentiment",
"event",
"account-pg",
]);
const [historyApps, setHistoryApps] = useState([]);
const [visible, setVisible] = useState(false);
const [editDataShow, setEditDataShow] = useState(false);
const [analysisData, setAnalysisData] = useState<analysisDataItem>({
appName: "",
appDesc: "",
appIcon: "",
jumpUrl: "",
appType: 2,
});
const [analysisList, setAnalysisList] = useState<analysisListData[]>([
{ key: "info", list: [] },
{ key: "account", list: [] },
]);
const [fileFail, setFileFail] = useState<UploadFile[]>([
// 如果需要默认回显图片,可以在这里初始化
// { name: '默认图片.png', url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'fail' }
]);
const [loading, setLoading] = useState<boolean>(false);
const [pageNum, setPageNum] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
const [total, setTotal] = useState<number>(0);
const state = useRef({
pageNum: 1,
pageSize: 10,
});
const showImageFileName = true;
const service = requestCrudService;
// 解构出 Group 和 Button,方便使用
const { Group: RadioGroup, Button: RadioButton } = Radio;
// 用于格式化接口返回的数据,使其符合 TDesign 的要求
const formatResponse = (res: any) => {
// 假设接口返回 { code: 0, data: { url: '...' } }
// 如果接口报错,可以在这里处理,让组件显示失败状态
if (res.code !== 0) {
return {
error: res.message || "上传失败",
url: "",
name: "",
};
}
return {
url: res.data.url,
name: res.data.name,
};
};
const handleChange: UploadProps["onChange"] = (files) => {
setFileFail(files);
console.log("当前文件列表:", files);
};
// 过滤应用
const filteredApps = useMemo(() => {
return lightApps.filter((app) => {
const matchesSearch =
searchQuery === "" ||
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.input.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory =
categoryFilter === "all" || app.category === categoryFilter;
return matchesSearch && matchesCategory;
});
}, [searchQuery, categoryFilter]);
// 按分类分组
const groupedApps = useMemo(() => {
const groups: Record<AppCategory, LightApp[]> = {
info: [],
account: [],
event: [],
topic: [],
};
filteredApps.forEach((app) => {
groups[app.category].push(app);
});
return groups;
}, [filteredApps]);
// 处理应用启动
const handleLaunchApp = (app: LightApp) => {
console.log("启动应用:", app);
// 更新最近使用
setRecentApps((prev) => {
const filtered = prev.filter((id) => id !== app.id);
return [app.id, ...filtered].slice(0, 5);
});
onNavigate("light-app-run", app.id);
};
// 获取分类颜色
const getCategoryColor = (category: AppCategory) => {
const colors: Record<AppCategory, { text: string; bg: string }> = {
info: {
text: isDark ? "text-blue-400" : "text-blue-600",
bg: isDark ? "bg-blue-500/20" : "bg-blue-50",
},
account: {
text: isDark ? "text-purple-400" : "text-purple-600",
bg: isDark ? "bg-purple-500/20" : "bg-purple-50",
},
event: {
text: isDark ? "text-orange-400" : "text-orange-600",
bg: isDark ? "bg-orange-500/20" : "bg-orange-50",
},
topic: {
text: isDark ? "text-green-400" : "text-green-600",
bg: isDark ? "bg-green-500/20" : "bg-green-50",
},
};
return colors[category];
};
// 获取应用列表
const analysisListSearch = async () => {
const parmy = {
appName: searchQuery,
pageNum: state.current.pageNum,
pageSize: state.current.pageSize,
};
setLoading(true);
const res = await service.post(`${zncmUrl}/app/analysis/list`, parmy);
console.log(res);
setLoading(false);
const result = res.data;
const listData: analysisListData[] = [
{ key: "info", list: [] },
{ key: "account", list: [] },
];
result.records.map((item: analysisListItem) => {
if (item.appType === 1) {
item.category = "info";
item.categoryName = "内部应用";
listData[0].list.push(item);
} else if (item.appType === 2) {
item.category = "account";
item.categoryName = "外部应用";
listData[1].list.push(item);
}
setAnalysisList(listData);
});
setTotal(result.total);
};
// 添加应用
const analysieAddListChange = async () => {
const res = await service.post(`${zncmUrl}/app/analysis/add`, analysisData);
console.log(res);
MessagePlugin.success("添加成功");
await analysisListSearch();
};
// 回车处理
const handleKeyDown = async (e) => {
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
e.preventDefault();
} else if (e.keyCode === 13) {
e.preventDefault();
await analysisListSearch();
} else if (e.key === "/") {
e.preventDefault();
}
};
// 添加应用
const showDialog = () => {
setAnalysisData({
appName: "",
appDesc: "",
appIcon: "",
jumpUrl: "",
appType: 2,
});
setFileFail([]);
setVisible(true);
};
const handleOk = async () => {
await analysieAddListChange();
setVisible(false);
};
const handleClose = () => {
setVisible(false);
};
// 编辑应用
const editShowChange = (item: analysisDataItem) => {
setAnalysisData(item);
if (item.appIcon) {
setFileFail([
{ name: "默认图片.png", url: item.appIcon, status: "fail" },
]);
}
setEditDataShow(true);
};
const handleOkEdit = () => {
editDataChange();
setEditDataShow(false);
};
const handleCloseEdit = () => {
setEditDataShow(false);
};
const getBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const imgUpload: UploadProps["requestMethod"] = async (file: any) => {
console.log("file", file);
const base64 = await getBase64(file.raw);
handleDataChange("appIcon", base64);
return {
status: "success",
// response 对象里的数据会传给 formatResponse
response: {
url: "",
name: "",
},
};
};
// 通用的处理函数
const handleDataChange = (
key: keyof typeof analysisData,
value: string | number
) => {
setAnalysisData((prev) => ({
...prev,
[key]: value,
}));
};
// 删除应用
const delDataChange = async (item: analysisListItem) => {
console.log("删除", item);
setLoading(true);
const res = await service.get(`${zncmUrl}/app/analysis/delete/${item.id}`);
console.log(res);
MessagePlugin.success("操作成功");
await analysisListSearch();
};
const getHistoryApps = async () => {
const reqParam = {
limit: 5,
};
const res = await service.get(`${zncmUrl}/app/history/recent`, reqParam);
if (res && res.data) {
console.log({ resData: res.data });
setHistoryApps(res.data);
}
};
// 编辑应用
const editDataChange = async () => {
setLoading(true);
const res = await service.post(
`${zncmUrl}/app/analysis/update`,
analysisData
);
console.log(res);
MessagePlugin.success("操作成功");
await analysisListSearch();
};
const pageIndexChange = async (value: number) => {
state.current.pageNum = value;
setPageNum(value);
await analysisListSearch();
};
const pageSizeChange = async (value: number) => {
state.current.pageSize = value;
setPageSize(value);
await analysisListSearch();
};
// 发送请求
useEffect(() => {
analysisListSearch();
getHistoryApps();
}, []);
return (
//获取最近使用应用列表
<div className="space-y-6">
{/* 顶部工具栏 */}
<div
className={`rounded-xl border p-4 ${
isDark
? "bg-slate-800/50 border-slate-700"
: "bg-white border-gray-200"
}`}
>
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* 面包屑和标题 */}
<div className="flex items-center gap-2 flex-1">
<Layout
className={`w-5 h-5 ${isDark ? "text-blue-400" : "text-red-500"}`}
/>
<span
className={`text-sm ${
isDark ? "text-slate-400" : "text-gray-500"
}`}
>
轻应用中心 /
</span>
<span
className={`text-sm font-medium ${
isDark ? "text-white" : "text-gray-900"
}`}
>
应用广场
</span>
</div>
{/* 搜索框 */}
<div className="flex-1 max-w-md">
<div className="relative">
<Search
className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${
isDark ? "text-slate-500" : "text-gray-400"
}`}
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => handleKeyDown(e)}
placeholder="搜索应用名称或分析对象..."
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-colors focus:outline-none focus:ring-2 ${
isDark
? "bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500 focus:ring-blue-500/20"
: "bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20"
}`}
/>
</div>
</div>
{/* 筛选和操作 */}
<div className="flex items-center gap-3">
{/* <select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value as AppCategory | 'all')}
className={`px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${isDark
? 'bg-slate-900 border-slate-600 text-slate-200 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-700 focus:border-red-500'
}`}
>
<option value="all">全部分类</option>
<option value="info">信息分析类</option>
<option value="account">账号分析类</option>
<option value="event">事件分析类</option>
<option value="topic">专题分析类</option>
</select> */}
<button
onClick={() => onNavigate("light-app-records")}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
isDark
? "bg-slate-700 text-slate-300 hover:bg-slate-600"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
<Clock className="w-4 h-4" />
运行记录
</button>
<Button theme="primary" onClick={showDialog}>
<span className="flex items-center">
<Plus className="w-4 h-4 mr-[10px]" />
添加
</span>
</Button>
</div>
</div>
</div>
{/* 最近使用 */}
{historyApps.length > 0 &&
searchQuery === "" &&
categoryFilter === "all" && (
<div
className={`rounded-xl border p-4 ${
isDark
? "bg-slate-800/50 border-slate-700"
: "bg-white border-gray-200"
}`}
>
<div className="flex items-center gap-2 mb-4">
<Clock
className={`w-4 h-4 ${
isDark ? "text-blue-400" : "text-red-500"
}`}
/>
<h3
className={`text-sm font-medium ${
isDark ? "text-slate-300" : "text-gray-700"
}`}
>
最近使用
</h3>
</div>
<div className="flex flex-wrap gap-2">
{historyApps.map((app) => {
// const app = historyApps.find((a) => a.id === appId);
// if (!app) return null;
// const Icon = app.icon;
return (
<button
// key={app.id}
// onClick={() => handleLaunchApp(app)}
className={`px-4 py-2 rounded-lg border text-sm font-medium flex items-center gap-2 transition-all hover:scale-105 ${
isDark
? `bg-slate-700/50 border-slate-600 text-slate-300 hover:bg-slate-700`
: `bg-gray-50 border-gray-200 text-gray-700 hover:bg-gray-100`
}`}
>
{/* <Icon className={`w-4 h-4 ${app.iconColor}`} /> */}
{app.appName}
</button>
);
})}
</div>
</div>
)}
{/* 应用分组区 */}
<Loading loading={loading} text="正在加载中...">
<div className="space-y-6">
{analysisList.map((section) => {
const config = categoryConfig[section.key];
const colorStyle = getCategoryColor(section.key);
return (
<div key={section.key} className="space-y-4">
{/* 标题区域 */}
<div className="flex items-center gap-2">
<h2
className={`text-lg font-semibold ${
isDark ? "text-white" : "text-gray-900"
}`}
>
{config.title}
</h2>
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${colorStyle.bg} ${colorStyle.text}`}
>
{section.list.length} 个应用
</span>
</div>
{/* 卡片网格 */}
{section.list.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{section.list.map((app) => (
<AppCard
key={app.id}
app={app}
isDark={isDark}
hoverColor={config.hoverColor}
btnClass={
isDark ? config.btnTheme.dark : config.btnTheme.light
}
delDataChange={delDataChange}
editShowChange={editShowChange}
/>
))}
</div>
) : (
<div
className={`text-center py-10 text-sm ${
isDark ? "text-slate-500" : "text-gray-400"
}`}
>
<Empty description="暂无数据" />
</div>
)}
</div>
);
})}
</div>
</Loading>
<div className="p-4 text-center">
<Pagination
total={total}
current={pageNum}
pageSize={pageSize}
onCurrentChange={(page) => pageIndexChange(Number(page))}
onPageSizeChange={(size) => pageSizeChange(Number(size))} // 每页数量变化时的回调
/>
</div>
{/* 底部提示 */}
<div
className={`p-4 rounded-xl border ${
isDark
? "bg-blue-500/10 border-blue-500/30"
: "bg-blue-50 border-blue-200"
}`}
>
<div className="flex items-start gap-3">
<Sparkles
className={`w-5 h-5 mt-0.5 ${
isDark ? "text-blue-400" : "text-blue-500"
}`}
/>
<div>
<p
className={`text-sm font-medium ${
isDark ? "text-blue-300" : "text-blue-800"
}`}
>
关于轻应用中心
</p>
<p
className={`mt-1 text-sm ${
isDark ? "text-blue-400/80" : "text-blue-700"
}`}
>
轻应用中心提供了一系列快速分析工具,适合处理临时性的分析需求。相比完整的任务级应用,轻应用更加轻量便捷,可以快速调用多种工具完成分析工作。运行结果可保存快照、导出或转为正式任务。
</p>
</div>
</div>
</div>
<Dialog
header="添加应用"
visible={visible}
onClose={handleClose} // 监听关闭事件
onConfirm={handleOk} // 点击确定按钮
onCancel={handleClose} // 点击取消按钮
closeOnOverlayClick={true} // 点击遮罩层是否关闭
>
<div className="mb-[10px]">
<Upload
// 对应 v-model
files={fileFail}
onChange={handleChange}
// 对应 theme="image"
theme="image"
requestMethod={imgUpload}
// 对应 accept
accept="image/*"
tips="系统logo"
// 对应 :show-image-file-name
showImageFileName={showImageFileName}
// 对应 :format-response
formatResponse={formatResponse}
// 可选:限制上传数量(演示单张)
multiple={false}
// 可选:上传失败时的提示(如果接口直接返回 4xx/5xx)
onFail={({ e, file }) => {
MessagePlugin.error(`文件 ${file.name} 上传失败`);
}}
/>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>名称:</div>
</Col>
<Col span={9}>
<Input
placeholder="请输入应用名称"
value={analysisData.appName}
onChange={(val) => handleDataChange("appName", val)}
clearable
/>
</Col>
</Row>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>详情:</div>
</Col>
<Col span={9}>
<Textarea
placeholder="请输入应用详情"
value={analysisData.appDesc}
onChange={(val) => handleDataChange("appDesc", val)}
autosize
/>
</Col>
</Row>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>类型:</div>
</Col>
<Col span={9}>
<RadioGroup
variant="primary-filled" // 对应 variant="primary-filled"
value={analysisData.appType} // 绑定状态值
onChange={(value) => handleDataChange("appType", Number(value))}
>
{/* 3. 使用 RadioButton */}
<RadioButton value={1}>内部应用</RadioButton>
<RadioButton value={2}>外部应用</RadioButton>
<RadioButton value={3}>第三方集成</RadioButton>
</RadioGroup>
</Col>
</Row>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>链接:</div>
</Col>
<Col span={9}>
<Input
placeholder="请输入链接"
value={analysisData.jumpUrl}
onChange={(val) => handleDataChange("jumpUrl", val)}
clearable
/>
</Col>
</Row>
</div>
</Dialog>
<Dialog
header="编辑应用"
visible={editDataShow}
onClose={handleCloseEdit} // 监听关闭事件
onConfirm={handleOkEdit} // 点击确定按钮
onCancel={handleCloseEdit} // 点击取消按钮
closeOnOverlayClick={true} // 点击遮罩层是否关闭
>
<div className="mb-[10px]">
{/* <Upload
// 对应 v-model
files={fileFail}
onChange={handleChange}
// 对应 theme="image"
theme="image"
requestMethod={imgUpload}
// 对应 accept
accept="image/*"
tips="系统logo"
// 对应 :show-image-file-name
showImageFileName={showImageFileName}
// 对应 :format-response
formatResponse={formatResponse}
// 可选:限制上传数量(演示单张)
multiple={false}
// 可选:上传失败时的提示(如果接口直接返回 4xx/5xx)
onFail={({ e, file }) => {
MessagePlugin.error(`文件 ${file.name} 上传失败`);
}} /> */}
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>名称:</div>
</Col>
<Col span={9}>
<Input
placeholder="请输入应用名称"
value={analysisData.appName}
onChange={(val) => handleDataChange("appName", val)}
clearable
/>
</Col>
</Row>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>详情:</div>
</Col>
<Col span={9}>
<Textarea
placeholder="请输入应用详情"
value={analysisData.appDesc}
onChange={(val) => handleDataChange("appDesc", val)}
autosize
/>
</Col>
</Row>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>类型:</div>
</Col>
<Col span={9}>
<RadioGroup
variant="primary-filled" // 对应 variant="primary-filled"
value={analysisData.appType} // 绑定状态值
onChange={(value) => handleDataChange("appType", Number(value))}
>
{/* 3. 使用 RadioButton */}
<RadioButton value={1}>内部应用</RadioButton>
<RadioButton value={2}>外部应用</RadioButton>
<RadioButton value={3}>第三方集成</RadioButton>
</RadioGroup>
</Col>
</Row>
</div>
<div className="mb-[10px] overflow-hidden">
<Row>
<Col span={2}>
<div>链接:</div>
</Col>
<Col span={9}>
<Input
placeholder="请输入链接"
value={analysisData.jumpUrl}
onChange={(val) => handleDataChange("jumpUrl", val)}
clearable
/>
</Col>
</Row>
</div>
</Dialog>
</div>
);
};
export default LightAppSquare;
+37
View File
@@ -0,0 +1,37 @@
import { Outlet, useNavigate } from 'react-router-dom';
import React, { useState, useEffect, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
const zncmUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || '';
import { requestCrudService } from '@/service/request-axios-service';
const service = requestCrudService;
const Login: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const yUserId = searchParams.get('yUserId');
const toolToken = searchParams.get('token');
if (toolToken) {
localStorage.setItem('tools_token', toolToken);
}
const loginSearch = async () => {
const param = {
yuserId: yUserId
}
const res: any = await service.get(`${zncmUrl}/cop/userDetailByYuserId`, param);
console.log('res yUserId', res , yUserId)
const result = res.data;
localStorage.setItem('userId', result.id)
localStorage.setItem('userInfo', JSON.stringify(result));
navigate('/qa-center/qa-from');
}
useEffect(() => {
console.log('登录信息',searchParams.get('yUserId'));
loginSearch();
}, []);
return (
<div className="h-full flex flex-col">
登录中...
</div>
);
}
export default Login
@@ -0,0 +1,44 @@
import React, { useEffect, useState } from "react";
import { useTheme } from "../contexts/ThemeContext";
import { requestCrudService } from "@/service/request-axios-service";
import "./css/KnowledgeBaseResourceManagement.css";
import { PageType } from "../components/Sidebar";
interface NetworkActorResourceManagementProps {
onNavigate: (page: PageType) => void;
}
const NetworkActorResourceManagement: React.FC<
NetworkActorResourceManagementProps
> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === "dark";
const [iframeUrl, setIframeUrl] = useState("");
const b2SearchUrl = import.meta.env.VITE_AIPI_B2ACTOR_URL;
// const service = requestCrudService;
useEffect(() => {
initIframeUrl();
}, []);
const initIframeUrl = async () => {
// const url = `${knowledgeURL}/#/knowledge/knowledge-search?isIframe=true`;
const jsonStr = localStorage.getItem("userInfo");
console.log("jsonStr", jsonStr);
// if (jsonStr) {
// const userInfo = JSON.parse(jsonStr);
// console.log("userInfo", userInfo);
// }
console.log({ iframeUrl: `${b2SearchUrl}/fromB/toXWT?isFrame=1` });
setIframeUrl(`${b2SearchUrl}/fromB/toXWT?isFrame=1`);
};
return (
<div className="iframe-container">
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
</div>
);
};
export default NetworkActorResourceManagement;
+810
View File
@@ -0,0 +1,810 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
Activity,
AlertTriangle,
Clock,
Filter,
RefreshCw,
ChevronLeft,
ChevronRight,
X,
Link2,
ArrowRight,
Trash2,
} from 'lucide-react';
// ------------------------------------------------------------------ //
// Types //
// ------------------------------------------------------------------ //
interface CallTrace {
id: string;
trace_id?: string;
task_id?: string;
parent_call_id?: string;
call_type: 'model' | 'mcp' | 'skill';
status: 'success' | 'failed' | 'cancelled' | 'timeout';
started_at: number;
ended_at?: number;
duration_ms: number;
name: string;
action: string;
mcp_key?: string;
mcp_name?: string;
skill_key?: string;
skill_name?: string;
model?: string;
input_summary?: string;
output_summary?: string;
error_id?: string;
error_message?: string;
meta?: Record<string, unknown>;
}
interface ErrorLog {
id: string;
trace_id?: string;
task_id?: string;
call_id?: string;
source: string;
level: 'warning' | 'error';
happened_at: number;
message: string;
recoverable: boolean;
mcp_key?: string;
mcp_name?: string;
skill_key?: string;
skill_name?: string;
model?: string;
action?: string;
input_summary?: string;
params_summary?: Record<string, unknown>;
}
interface PagedResponse<T> {
items: T[];
total: number;
page: number;
page_size: number;
}
interface Options {
models: string[];
mcps: { key: string; name: string }[];
skills: { key: string; name: string }[];
statuses: string[];
sources: string[];
}
// ------------------------------------------------------------------ //
// Helpers //
// ------------------------------------------------------------------ //
function formatTs(ms: number): string {
return new Date(ms).toLocaleString('zh-CN', { hour12: false });
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
const STATUS_COLORS: Record<string, string> = {
success: 'bg-green-500/20 text-green-400',
failed: 'bg-red-500/20 text-red-400',
timeout: 'bg-yellow-500/20 text-yellow-400',
cancelled: 'bg-slate-500/20 text-slate-400',
warning: 'bg-yellow-500/20 text-yellow-400',
error: 'bg-red-500/20 text-red-400',
};
const TYPE_COLORS: Record<string, string> = {
model: 'bg-blue-500/20 text-blue-400',
mcp: 'bg-purple-500/20 text-purple-400',
skill: 'bg-cyan-500/20 text-cyan-400',
};
const SOURCE_COLORS: Record<string, string> = {
model: 'bg-blue-500/20 text-blue-400',
llm: 'bg-blue-500/20 text-blue-400',
mcp: 'bg-purple-500/20 text-purple-400',
skill: 'bg-cyan-500/20 text-cyan-400',
orchestrator: 'bg-orange-500/20 text-orange-400',
network: 'bg-pink-500/20 text-pink-400',
};
const backUrl = 'https://ch1.b.uat.4.cn/zncmback'
function Badge({ label, colorClass }: { label: string; colorClass: string }) {
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
{label}
</span>
);
}
// ------------------------------------------------------------------ //
// API //
// ------------------------------------------------------------------ //
async function fetchOptions(): Promise<Options> {
const res = await fetch(`${backUrl}/api/observability/options`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function fetchCalls(params: Record<string, string | number | undefined>): Promise<PagedResponse<CallTrace>> {
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') qs.append(k, String(v));
});
const res = await fetch(`${backUrl}/api/observability/calls?${qs}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function fetchErrors(params: Record<string, string | number | undefined>): Promise<PagedResponse<ErrorLog>> {
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') qs.append(k, String(v));
});
const res = await fetch(`${backUrl}/api/observability/errors?${qs}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// ------------------------------------------------------------------ //
// Shared UI atoms //
// ------------------------------------------------------------------ //
function Select({
value, onChange, options, placeholder, cls,
}: {
value: string; onChange: (v: string) => void;
options: { value: string; label: string }[]; placeholder?: string; cls?: string;
}) {
return (
<select value={value} onChange={(e) => onChange(e.target.value)}
className={`text-sm rounded-lg border px-3 py-1.5 outline-none ${cls ?? ''}`}>
{placeholder && <option value="">{placeholder}</option>}
{options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
);
}
function Pagination({ page, pageSize, total, onPage, isDark }: {
page: number; pageSize: number; total: number; onPage: (p: number) => void; isDark: boolean;
}) {
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const textMuted = isDark ? 'text-slate-400' : 'text-gray-500';
const btn = `p-1.5 rounded-lg transition-all disabled:opacity-40 ${isDark ? 'text-slate-400 hover:text-white hover:bg-slate-700' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-100'}`;
return (
<div className={`flex items-center gap-3 text-sm ${textMuted}`}>
<span>共 {total} 条</span>
<div className="flex items-center gap-1">
<button className={btn} disabled={page <= 1} onClick={() => onPage(page - 1)}><ChevronLeft className="w-4 h-4" /></button>
<span className="px-2">{page} / {totalPages}</span>
<button className={btn} disabled={page >= totalPages} onClick={() => onPage(page + 1)}><ChevronRight className="w-4 h-4" /></button>
</div>
</div>
);
}
// ------------------------------------------------------------------ //
// Linked items — shown inside detail modals //
// ------------------------------------------------------------------ //
function LinkedErrors({ callId, traceId, isDark }: { callId: string; traceId?: string; isDark: boolean }) {
const [items, setItems] = useState<ErrorLog[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchErrors({ call_id: callId, page: 1, page_size: 10 })
.then((r) => setItems(r.items))
.catch(() => setItems([]))
.finally(() => setLoading(false));
}, [callId]);
const label = isDark ? 'text-slate-400' : 'text-gray-500';
const rowBg = isDark ? 'bg-red-900/20 border-red-800/30' : 'bg-red-50 border-red-200';
if (loading) return <p className={`text-xs ${label}`}>加载相关错误…</p>;
if (items.length === 0) return <p className={`text-xs ${label}`}>无相关错误日志</p>;
return (
<div className="space-y-1.5">
{items.map((e) => (
<div key={e.id} className={`rounded-lg border p-2.5 ${rowBg}`}>
<div className="flex items-center gap-2 flex-wrap">
<Badge label={e.level} colorClass={STATUS_COLORS[e.level] ?? ''} />
<Badge label={e.source} colorClass={SOURCE_COLORS[e.source] ?? ''} />
<span className={`text-xs ${label}`}>{formatTs(e.happened_at)}</span>
</div>
<p className={`text-sm mt-1 ${isDark ? 'text-slate-200' : 'text-gray-800'}`}>{e.message}</p>
</div>
))}
</div>
);
}
function LinkedCall({ callId, isDark }: { callId: string; isDark: boolean }) {
const [item, setItem] = useState<CallTrace | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// call_id in error = the id of the CallTrace record
fetchCalls({ trace_id: callId, page: 1, page_size: 1 })
.then((r) => setItem(r.items[0] ?? null))
.catch(() => setItem(null))
.finally(() => setLoading(false));
}, [callId]);
const label = isDark ? 'text-slate-400' : 'text-gray-500';
const rowBg = isDark ? 'bg-blue-900/20 border-blue-800/30' : 'bg-blue-50 border-blue-200';
if (loading) return <p className={`text-xs ${label}`}>加载关联调用…</p>;
if (!item) return <p className={`text-xs ${label}`}>未找到关联调用记录(ID: {callId})</p>;
return (
<div className={`rounded-lg border p-2.5 ${rowBg}`}>
<div className="flex items-center gap-2 flex-wrap">
<Badge label={item.call_type} colorClass={TYPE_COLORS[item.call_type] ?? ''} />
<Badge label={item.status} colorClass={STATUS_COLORS[item.status] ?? ''} />
<span className={`text-xs font-mono ${label}`}>{formatDuration(item.duration_ms)}</span>
<span className={`text-xs ${label}`}>{formatTs(item.started_at)}</span>
</div>
<p className={`text-sm mt-1 font-medium ${isDark ? 'text-slate-200' : 'text-gray-800'}`}>{item.name}</p>
<p className={`text-xs mt-0.5 ${label}`}>{item.action}</p>
</div>
);
}
// ------------------------------------------------------------------ //
// Detail Modals //
// ------------------------------------------------------------------ //
function Section({ title, isDark, children }: { title: string; isDark: boolean; children: React.ReactNode }) {
const divider = isDark ? 'border-blue-800/20' : 'border-gray-200';
const label = isDark ? 'text-slate-400' : 'text-gray-500';
return (
<div className={`border-t pt-4 mt-4 ${divider}`}>
<div className={`flex items-center gap-1.5 mb-2 text-xs font-semibold uppercase tracking-wide ${label}`}>
<Link2 className="w-3.5 h-3.5" />
{title}
</div>
{children}
</div>
);
}
function FieldRow({ label, value, isDark }: { label: string; value?: string | null; isDark: boolean }) {
if (!value) return null;
const rowBg = isDark ? 'bg-slate-800/40' : 'bg-gray-50';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-500';
return (
<div className={`rounded-lg p-2.5 ${rowBg}`}>
<span className={`text-xs font-medium ${textSecondary}`}>{label}</span>
<p className={`text-sm mt-0.5 break-all whitespace-pre-wrap ${textPrimary}`}>{value}</p>
</div>
);
}
function CallDetailModal({ item, onClose, isDark }: { item: CallTrace; onClose: () => void; isDark: boolean }) {
const bg = isDark ? 'bg-slate-900 border-blue-800/30' : 'bg-white border-gray-200';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div className={`relative w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-xl border shadow-2xl p-6 ${bg}`}
onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className={`font-semibold text-base ${textPrimary}`}>调用详情</h3>
<button onClick={onClose} className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700' : 'hover:bg-gray-100'}`}>
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-1.5">
<FieldRow label="ID" value={item.id} isDark={isDark} />
<FieldRow label="Trace ID" value={item.trace_id} isDark={isDark} />
<FieldRow label="Task ID" value={item.task_id} isDark={isDark} />
<FieldRow label="Parent Call ID" value={item.parent_call_id} isDark={isDark} />
<FieldRow label="类型" value={item.call_type} isDark={isDark} />
<FieldRow label="状态" value={item.status} isDark={isDark} />
<FieldRow label="名称" value={item.name} isDark={isDark} />
<FieldRow label="操作" value={item.action} isDark={isDark} />
<FieldRow label="开始时间" value={formatTs(item.started_at)} isDark={isDark} />
<FieldRow label="结束时间" value={item.ended_at ? formatTs(item.ended_at) : undefined} isDark={isDark} />
<FieldRow label="耗时" value={formatDuration(item.duration_ms)} isDark={isDark} />
<FieldRow label="模型" value={item.model} isDark={isDark} />
<FieldRow label="MCP Key" value={item.mcp_key} isDark={isDark} />
<FieldRow label="MCP 名称" value={item.mcp_name} isDark={isDark} />
<FieldRow label="Skill Key" value={item.skill_key} isDark={isDark} />
<FieldRow label="Skill 名称" value={item.skill_name} isDark={isDark} />
<FieldRow label="输入摘要" value={item.input_summary} isDark={isDark} />
<FieldRow label="输出摘要" value={item.output_summary} isDark={isDark} />
<FieldRow label="错误信息" value={item.error_message} isDark={isDark} />
<FieldRow label="Meta" value={item.meta && Object.keys(item.meta).length ? JSON.stringify(item.meta, null, 2) : undefined} isDark={isDark} />
</div>
<Section title="相关错误日志" isDark={isDark}>
<LinkedErrors callId={item.id} traceId={item.trace_id} isDark={isDark} />
</Section>
</div>
</div>
);
}
function ErrorDetailModal({ item, onClose, isDark }: { item: ErrorLog; onClose: () => void; isDark: boolean }) {
const bg = isDark ? 'bg-slate-900 border-blue-800/30' : 'bg-white border-gray-200';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div className={`relative w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-xl border shadow-2xl p-6 ${bg}`}
onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className={`font-semibold text-base ${textPrimary}`}>错误详情</h3>
<button onClick={onClose} className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700' : 'hover:bg-gray-100'}`}>
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-1.5">
<FieldRow label="ID" value={item.id} isDark={isDark} />
<FieldRow label="Trace ID" value={item.trace_id} isDark={isDark} />
<FieldRow label="Task ID" value={item.task_id} isDark={isDark} />
<FieldRow label="Call ID" value={item.call_id} isDark={isDark} />
<FieldRow label="来源" value={item.source} isDark={isDark} />
<FieldRow label="级别" value={item.level} isDark={isDark} />
<FieldRow label="发生时间" value={formatTs(item.happened_at)} isDark={isDark} />
<FieldRow label="可恢复" value={item.recoverable ? '是' : '否'} isDark={isDark} />
<FieldRow label="消息" value={item.message} isDark={isDark} />
<FieldRow label="MCP Key" value={item.mcp_key} isDark={isDark} />
<FieldRow label="MCP 名称" value={item.mcp_name} isDark={isDark} />
<FieldRow label="Skill Key" value={item.skill_key} isDark={isDark} />
<FieldRow label="Skill 名称" value={item.skill_name} isDark={isDark} />
<FieldRow label="模型" value={item.model} isDark={isDark} />
<FieldRow label="操作" value={item.action} isDark={isDark} />
<FieldRow label="输入摘要" value={item.input_summary} isDark={isDark} />
<FieldRow label="参数摘要" value={item.params_summary && Object.keys(item.params_summary).length ? JSON.stringify(item.params_summary, null, 2) : undefined} isDark={isDark} />
</div>
{item.call_id && (
<Section title="关联调用记录" isDark={isDark}>
<LinkedCall callId={item.call_id} isDark={isDark} />
</Section>
)}
</div>
</div>
);
}
// ------------------------------------------------------------------ //
// Task ID pill — clickable, stops row-click propagation //
// ------------------------------------------------------------------ //
function TaskIdPill({
taskId, onFilter, isDark,
}: { taskId?: string | null; onFilter: (id: string) => void; isDark: boolean }) {
if (!taskId) return <span className={isDark ? 'text-slate-600' : 'text-gray-400'}>-</span>;
return (
<button
title="按此 Task ID 筛选"
onClick={(e) => { e.stopPropagation(); onFilter(taskId); }}
className={`font-mono text-xs px-1.5 py-0.5 rounded transition-all max-w-[100px] truncate block
${isDark ? 'text-blue-400 hover:bg-blue-500/20' : 'text-blue-600 hover:bg-blue-50'}`}
>
{taskId}
</button>
);
}
// ------------------------------------------------------------------ //
// 调用耗时统计 Tab //
// ------------------------------------------------------------------ //
interface CallsTabProps {
opts: Options;
isDark: boolean;
externalTaskId: string;
onSwitchToErrors: (taskId: string) => void;
}
function CallsTab({ opts, isDark, externalTaskId, onSwitchToErrors }: CallsTabProps) {
const [filters, setFilters] = useState({
call_type: '', model: '', mcp_key: '', skill_key: '', status: '',
task_id: externalTaskId, sort_by: 'started_at', sort_order: 'desc',
});
const [page, setPage] = useState(1);
const [data, setData] = useState<PagedResponse<CallTrace> | null>(null);
const [loading, setLoading] = useState(false);
const [fetchError, setFetchError] = useState('');
const [selected, setSelected] = useState<CallTrace | null>(null);
const PAGE_SIZE = 20;
// sync external task_id changes
useEffect(() => {
setFilters((prev) => ({ ...prev, task_id: externalTaskId }));
}, [externalTaskId]);
const load = useCallback(async (p: number, f = filters) => {
setLoading(true);
setFetchError('');
try {
const result = await fetchCalls({ ...f, page: p, page_size: PAGE_SIZE });
setData(result);
setPage(p);
} catch (e) {
setFetchError(String(e));
} finally {
setLoading(false);
}
}, [filters]);
useEffect(() => { load(1); }, [filters]);
const setF = (key: string, val: string) => setFilters((prev) => ({ ...prev, [key]: val }));
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
const selectCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200' : 'bg-white border-gray-300 text-gray-700';
const inputCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200 placeholder-slate-500' : 'bg-white border-gray-300 text-gray-700 placeholder-gray-400';
const trHover = isDark ? 'hover:bg-slate-700/30' : 'hover:bg-gray-50';
const thCls = `text-left text-xs font-medium uppercase tracking-wide py-2.5 px-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`;
const tdCls = `py-2.5 px-3 text-sm ${textPrimary}`;
const divider = isDark ? 'border-blue-800/20' : 'border-gray-200';
return (
<div className="space-y-4">
<div className={`${cardBg} border ${cardBorder} rounded-xl p-4`}>
<div className="flex items-center gap-2 mb-3">
<Filter className="w-4 h-4 text-blue-400" />
<span className={`text-sm font-medium ${textPrimary}`}>筛选条件</span>
{filters.task_id && (
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
Task: {filters.task_id}
<button onClick={() => setF('task_id', '')} className="ml-0.5 hover:text-white">
<X className="w-3 h-3" />
</button>
</span>
)}
</div>
<div className="flex flex-wrap gap-3">
<Select value={filters.call_type} onChange={(v) => setF('call_type', v)} placeholder="全部类型" cls={selectCls}
options={[{ value: 'model', label: '模型' }, { value: 'mcp', label: 'MCP' }, { value: 'skill', label: 'Skill' }]} />
<Select value={filters.model} onChange={(v) => setF('model', v)} placeholder="全部模型" cls={selectCls}
options={opts.models.map((m) => ({ value: m, label: m }))} />
<Select value={filters.mcp_key} onChange={(v) => setF('mcp_key', v)} placeholder="全部MCP" cls={selectCls}
options={opts.mcps.map((m) => ({ value: m.key, label: m.name || m.key }))} />
<Select value={filters.skill_key} onChange={(v) => setF('skill_key', v)} placeholder="全部Skill" cls={selectCls}
options={opts.skills.map((s) => ({ value: s.key, label: s.name || s.key }))} />
<Select value={filters.status} onChange={(v) => setF('status', v)} placeholder="全部状态" cls={selectCls}
options={opts.statuses.map((s) => ({ value: s, label: s }))} />
<input value={filters.task_id} onChange={(e) => setF('task_id', e.target.value)}
placeholder="Task ID" className={`text-sm rounded-lg border px-3 py-1.5 outline-none w-36 ${inputCls}`} />
<Select value={filters.sort_by} onChange={(v) => setF('sort_by', v)} cls={selectCls}
options={[{ value: 'started_at', label: '按时间' }, { value: 'duration_ms', label: '按耗时' }]} />
<Select value={filters.sort_order} onChange={(v) => setF('sort_order', v)} cls={selectCls}
options={[{ value: 'desc', label: '降序' }, { value: 'asc', label: '升序' }]} />
<button onClick={() => load(1)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-red-600 hover:bg-red-500 text-white'}`}>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />刷新
</button>
</div>
</div>
<div className={`${cardBg} border ${cardBorder} rounded-xl overflow-hidden`}>
{fetchError && <div className="p-4 text-red-400 text-sm">{fetchError}</div>}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className={`border-b ${divider}`}>
<th className={thCls}>时间</th>
<th className={thCls}>类型</th>
<th className={thCls}>名称</th>
<th className={thCls}>操作</th>
<th className={thCls}>耗时</th>
<th className={thCls}>状态</th>
<th className={thCls}>Task ID</th>
<th className={thCls}>输入摘要</th>
<th className={thCls}>输出摘要</th>
</tr>
</thead>
<tbody>
{loading && !data && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>加载中…</td></tr>}
{!loading && data?.items.length === 0 && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>暂无数据</td></tr>}
{data?.items.map((row) => (
<tr key={row.id} className={`border-b ${divider} cursor-pointer transition-colors ${trHover}`}
onClick={() => setSelected(row)}>
<td className={`${tdCls} whitespace-nowrap text-xs ${textSecondary}`}>{formatTs(row.started_at)}</td>
<td className={tdCls}><Badge label={row.call_type} colorClass={TYPE_COLORS[row.call_type] ?? ''} /></td>
<td className={`${tdCls} max-w-[120px] truncate`}>{row.name}</td>
<td className={`${tdCls} max-w-[160px] truncate ${textSecondary} text-xs`}>{row.action}</td>
<td className={`${tdCls} whitespace-nowrap font-mono`}>{formatDuration(row.duration_ms)}</td>
<td className={tdCls}><Badge label={row.status} colorClass={STATUS_COLORS[row.status] ?? ''} /></td>
<td className={tdCls}>
<div className="flex items-center gap-1">
<TaskIdPill taskId={row.task_id} onFilter={(id) => setF('task_id', id)} isDark={isDark} />
{row.task_id && (
<button title="在报错日志中查看" onClick={(e) => { e.stopPropagation(); onSwitchToErrors(row.task_id!); }}
className={`p-0.5 rounded transition-all ${isDark ? 'text-slate-500 hover:text-red-400' : 'text-gray-400 hover:text-red-500'}`}>
<ArrowRight className="w-3 h-3" />
</button>
)}
</div>
</td>
<td className={`${tdCls} max-w-[160px] truncate text-xs ${textSecondary}`}>{row.input_summary ?? '-'}</td>
<td className={`${tdCls} max-w-[160px] truncate text-xs ${textSecondary}`}>{row.output_summary ?? '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
{data && (
<div className={`flex items-center justify-end px-4 py-3 border-t ${divider}`}>
<Pagination page={data.page} pageSize={data.page_size} total={data.total} onPage={(p) => load(p)} isDark={isDark} />
</div>
)}
</div>
{selected && <CallDetailModal item={selected} onClose={() => setSelected(null)} isDark={isDark} />}
</div>
);
}
// ------------------------------------------------------------------ //
// 报错日志 Tab //
// ------------------------------------------------------------------ //
interface ErrorsTabProps {
opts: Options;
isDark: boolean;
externalTaskId: string;
onSwitchToCalls: (taskId: string) => void;
}
function ErrorsTab({ opts, isDark, externalTaskId, onSwitchToCalls }: ErrorsTabProps) {
const [filters, setFilters] = useState({
source: '', model: '', mcp_key: '', skill_key: '', level: '',
task_id: externalTaskId, sort_order: 'desc',
});
const [page, setPage] = useState(1);
const [data, setData] = useState<PagedResponse<ErrorLog> | null>(null);
const [loading, setLoading] = useState(false);
const [fetchError, setFetchError] = useState('');
const [selected, setSelected] = useState<ErrorLog | null>(null);
const PAGE_SIZE = 20;
useEffect(() => {
setFilters((prev) => ({ ...prev, task_id: externalTaskId }));
}, [externalTaskId]);
const load = useCallback(async (p: number, f = filters) => {
setLoading(true);
setFetchError('');
try {
const result = await fetchErrors({ ...f, page: p, page_size: PAGE_SIZE });
setData(result);
setPage(p);
} catch (e) {
setFetchError(String(e));
} finally {
setLoading(false);
}
}, [filters]);
useEffect(() => { load(1); }, [filters]);
const setF = (key: string, val: string) => setFilters((prev) => ({ ...prev, [key]: val }));
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
const selectCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200' : 'bg-white border-gray-300 text-gray-700';
const inputCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200 placeholder-slate-500' : 'bg-white border-gray-300 text-gray-700 placeholder-gray-400';
const trHover = isDark ? 'hover:bg-slate-700/30' : 'hover:bg-gray-50';
const thCls = `text-left text-xs font-medium uppercase tracking-wide py-2.5 px-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`;
const tdCls = `py-2.5 px-3 text-sm ${textPrimary}`;
const divider = isDark ? 'border-blue-800/20' : 'border-gray-200';
return (
<div className="space-y-4">
<div className={`${cardBg} border ${cardBorder} rounded-xl p-4`}>
<div className="flex items-center gap-2 mb-3">
<Filter className="w-4 h-4 text-red-400" />
<span className={`text-sm font-medium ${textPrimary}`}>筛选条件</span>
{filters.task_id && (
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
Task: {filters.task_id}
<button onClick={() => setF('task_id', '')} className="ml-0.5 hover:text-white">
<X className="w-3 h-3" />
</button>
</span>
)}
</div>
<div className="flex flex-wrap gap-3">
<Select value={filters.source} onChange={(v) => setF('source', v)} placeholder="全部来源" cls={selectCls}
options={opts.sources.map((s) => ({ value: s, label: s }))} />
<Select value={filters.level} onChange={(v) => setF('level', v)} placeholder="全部级别" cls={selectCls}
options={[{ value: 'warning', label: '警告' }, { value: 'error', label: '错误' }]} />
<Select value={filters.model} onChange={(v) => setF('model', v)} placeholder="全部模型" cls={selectCls}
options={opts.models.map((m) => ({ value: m, label: m }))} />
<Select value={filters.mcp_key} onChange={(v) => setF('mcp_key', v)} placeholder="全部MCP" cls={selectCls}
options={opts.mcps.map((m) => ({ value: m.key, label: m.name || m.key }))} />
<Select value={filters.skill_key} onChange={(v) => setF('skill_key', v)} placeholder="全部Skill" cls={selectCls}
options={opts.skills.map((s) => ({ value: s.key, label: s.name || s.key }))} />
<input value={filters.task_id} onChange={(e) => setF('task_id', e.target.value)}
placeholder="Task ID" className={`text-sm rounded-lg border px-3 py-1.5 outline-none w-36 ${inputCls}`} />
<Select value={filters.sort_order} onChange={(v) => setF('sort_order', v)} cls={selectCls}
options={[{ value: 'desc', label: '降序' }, { value: 'asc', label: '升序' }]} />
<button onClick={() => load(1)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-red-600 hover:bg-red-500 text-white'}`}>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />刷新
</button>
</div>
</div>
<div className={`${cardBg} border ${cardBorder} rounded-xl overflow-hidden`}>
{fetchError && <div className="p-4 text-red-400 text-sm">{fetchError}</div>}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className={`border-b ${divider}`}>
<th className={thCls}>时间</th>
<th className={thCls}>级别</th>
<th className={thCls}>来源</th>
<th className={thCls}>消息</th>
<th className={thCls}>可恢复</th>
<th className={thCls}>Task ID</th>
<th className={thCls}>MCP</th>
<th className={thCls}>Skill</th>
<th className={thCls}>模型</th>
</tr>
</thead>
<tbody>
{loading && !data && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>加载中…</td></tr>}
{!loading && data?.items.length === 0 && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>暂无数据</td></tr>}
{data?.items.map((row) => (
<tr key={row.id} className={`border-b ${divider} cursor-pointer transition-colors ${trHover}`}
onClick={() => setSelected(row)}>
<td className={`${tdCls} whitespace-nowrap text-xs ${textSecondary}`}>{formatTs(row.happened_at)}</td>
<td className={tdCls}><Badge label={row.level} colorClass={STATUS_COLORS[row.level] ?? ''} /></td>
<td className={tdCls}><Badge label={row.source} colorClass={SOURCE_COLORS[row.source] ?? ''} /></td>
<td className={`${tdCls} max-w-[240px] truncate`}>{row.message}</td>
<td className={`${tdCls} text-xs`}>
<span className={row.recoverable ? 'text-green-400' : 'text-red-400'}>{row.recoverable ? '是' : '否'}</span>
</td>
<td className={tdCls}>
<div className="flex items-center gap-1">
<TaskIdPill taskId={row.task_id} onFilter={(id) => setF('task_id', id)} isDark={isDark} />
{row.task_id && (
<button title="在调用耗时中查看" onClick={(e) => { e.stopPropagation(); onSwitchToCalls(row.task_id!); }}
className={`p-0.5 rounded transition-all ${isDark ? 'text-slate-500 hover:text-blue-400' : 'text-gray-400 hover:text-blue-500'}`}>
<ArrowRight className="w-3 h-3" />
</button>
)}
</div>
</td>
<td className={`${tdCls} text-xs ${textSecondary}`}>{row.mcp_name ?? row.mcp_key ?? '-'}</td>
<td className={`${tdCls} text-xs ${textSecondary}`}>{row.skill_name ?? row.skill_key ?? '-'}</td>
<td className={`${tdCls} text-xs ${textSecondary}`}>{row.model ?? '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
{data && (
<div className={`flex items-center justify-end px-4 py-3 border-t ${divider}`}>
<Pagination page={data.page} pageSize={data.page_size} total={data.total} onPage={(p) => load(p)} isDark={isDark} />
</div>
)}
</div>
{selected && <ErrorDetailModal item={selected} onClose={() => setSelected(null)} isDark={isDark} />}
</div>
);
}
// ------------------------------------------------------------------ //
// Main Page //
// ------------------------------------------------------------------ //
const MANUAL_PURGE_DAYS = 14;
async function callPurge(retainDays: number): Promise<{ deleted_call_traces: number; deleted_error_logs: number }> {
const res = await fetch(`${backUrl}/api/observability/purge?retain_days=${retainDays}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export default function ObservabilityPage() {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [activeTab, setActiveTab] = useState<'calls' | 'errors'>('calls');
const [opts, setOpts] = useState<Options>({ models: [], mcps: [], skills: [], statuses: [], sources: [] });
const [sharedTaskId, setSharedTaskId] = useState('');
const [purging, setPurging] = useState(false);
const [purgeMsg, setPurgeMsg] = useState('');
useEffect(() => {
fetchOptions().then(setOpts).catch(console.error);
}, []);
const handlePurge = async () => {
if (!window.confirm(`确认清理 ${MANUAL_PURGE_DAYS} 天前的旧记录?此操作不可撤销。`)) return;
setPurging(true);
setPurgeMsg('');
try {
const r = await callPurge(MANUAL_PURGE_DAYS);
setPurgeMsg(`已清理:调用记录 ${r.deleted_call_traces} 条,错误日志 ${r.deleted_error_logs} 条`);
} catch (e) {
setPurgeMsg(`清理失败:${e}`);
} finally {
setPurging(false);
}
};
const handleSwitchToErrors = (taskId: string) => {
setSharedTaskId(taskId);
setActiveTab('errors');
};
const handleSwitchToCalls = (taskId: string) => {
setSharedTaskId(taskId);
setActiveTab('calls');
};
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
const tabActive = isDark ? 'bg-blue-600/20 text-blue-400 border border-blue-500/30' : 'bg-red-50 text-red-600 border border-red-200';
const tabInactive = isDark ? 'text-slate-400 hover:text-white hover:bg-slate-800/50' : 'text-gray-600 hover:text-red-600 hover:bg-red-50';
return (
<div className="space-y-6">
<div className={`${cardBg} backdrop-blur-xl border ${cardBorder} rounded-xl p-4`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${isDark ? 'bg-blue-500/20' : 'bg-red-50'}`}>
<Activity className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
</div>
<div>
<h1 className={`font-bold text-lg ${textPrimary}`}>可观测性</h1>
<p className={`text-sm ${textSecondary}`}>调用耗时统计 · 报错日志</p>
</div>
</div>
<div className="flex items-center gap-3">
{purgeMsg && (
<span className={`text-xs px-2 py-1 rounded-lg ${isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'}`}>
{purgeMsg}
</span>
)}
<button onClick={handlePurge} disabled={purging}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-all
${isDark ? 'border-red-800/40 text-red-400 hover:bg-red-900/20 disabled:opacity-40' : 'border-red-200 text-red-600 hover:bg-red-50 disabled:opacity-40'}`}>
<Trash2 className={`w-3.5 h-3.5 ${purging ? 'animate-spin' : ''}`} />
清理 {MANUAL_PURGE_DAYS} 天前数据
</button>
</div>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => setActiveTab('calls')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === 'calls' ? tabActive : tabInactive}`}>
<Clock className="w-4 h-4" />调用耗时统计
</button>
<button onClick={() => setActiveTab('errors')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === 'errors' ? tabActive : tabInactive}`}>
<AlertTriangle className="w-4 h-4" />报错日志
</button>
</div>
{activeTab === 'calls' ? (
<CallsTab opts={opts} isDark={isDark} externalTaskId={sharedTaskId} onSwitchToErrors={handleSwitchToErrors} />
) : (
<ErrorsTab opts={opts} isDark={isDark} externalTaskId={sharedTaskId} onSwitchToCalls={handleSwitchToCalls} />
)}
</div>
);
}
File diff suppressed because it is too large. Load diff
+758
View File
@@ -0,0 +1,758 @@
import React, { useState } from 'react';
import {
Search,
Bookmark,
BookmarkCheck,
Copy,
Download,
MessageSquare,
Bot,
History,
Calendar,
Filter,
ChevronDown,
GitCompare,
FileText,
ArrowRight,
X,
Star,
CheckCircle,
AlertCircle,
Send,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
type RecordType = 'all' | 'general' | 'agent';
type ModeType = 'all' | 'single-round' | 'multi-round' | 'deep-search' | 'compare';
type CollectionStatus = 'all' | 'collected';
interface QARecord {
id: string;
type: 'general' | 'agent';
mode: 'single-round' | 'multi-round' | 'deep-search' | 'compare';
question: string;
answer: string;
agentName?: string;
createTime: Date;
isCollected: boolean;
isHighValue: boolean;
isReusable: boolean;
isConvertible: boolean;
notes?: string;
evaluation?: string;
sources?: string[];
}
const QARecords: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 模拟问答记录数据
const mockRecords: QARecord[] = [
{
id: '1',
type: 'general',
mode: 'compare',
question: 'RAG技术的工作原理是什么?',
answer: '单轮RAG回答:直接检索并生成答案,响应速度快...',
createTime: new Date('2026-03-19 14:30'),
isCollected: true,
isHighValue: true,
isReusable: true,
isConvertible: true,
sources: ['RAG技术白皮书', 'LLM应用实践'],
},
{
id: '2',
type: 'agent',
mode: 'single-round',
question: '目标受众应如何分层?',
answer: '基于受众分析智能体的专业能力,识别出核心受众特征...',
agentName: 'AudienceAgent',
createTime: new Date('2026-03-19 10:20'),
isCollected: false,
isHighValue: true,
isReusable: true,
isConvertible: true,
},
{
id: '3',
type: 'general',
mode: 'deep-search',
question: '品牌年轻化传播策略有哪些?',
answer: '通过深度检索分析,总结出以下品牌年轻化传播策略...',
createTime: new Date('2026-03-18 16:45'),
isCollected: true,
isHighValue: false,
isReusable: false,
isConvertible: false,
notes: '需要进一步补充具体案例',
},
{
id: '4',
type: 'agent',
mode: 'multi-round',
question: '如何制定有效的用户激活策略?',
answer: '通过多轮对话深入分析,制定以下用户激活策略...',
agentName: 'ChannelAgent',
createTime: new Date('2026-03-18 11:15'),
isCollected: false,
isHighValue: true,
isReusable: true,
isConvertible: true,
evaluation: '策略实用性强,建议转化任务执行',
},
{
id: '5',
type: 'general',
mode: 'single-round',
question: '内容营销的最佳实践是什么?',
answer: '内容营销最佳实践包括:明确目标受众、创造高质量内容...',
createTime: new Date('2026-03-17 09:00'),
isCollected: false,
isHighValue: false,
isReusable: true,
isConvertible: false,
},
];
// 状态
const [records, setRecords] = useState<QARecord[]>(mockRecords);
const [selectedRecord, setSelectedRecord] = useState<QARecord | null>(mockRecords[0]);
const [searchQuery, setSearchQuery] = useState('');
const [recordType, setRecordType] = useState<RecordType>('all');
const [modeType, setModeType] = useState<ModeType>('all');
const [collectionStatus, setCollectionStatus] = useState<CollectionStatus>('all');
const [timeRange, setTimeRange] = useState('all');
const [notesInput, setNotesInput] = useState('');
// 筛选记录
const filteredRecords = records.filter((record) => {
// 搜索过滤
if (searchQuery && !record.question.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
// 类型过滤
if (recordType !== 'all' && record.type !== recordType) {
return false;
}
// 模式过滤
if (modeType !== 'all' && record.mode !== modeType) {
return false;
}
// 收藏状态过滤
if (collectionStatus === 'collected' && !record.isCollected) {
return false;
}
return true;
});
// 获取模式标签
const getModeLabel = (mode: QARecord['mode']) => {
switch (mode) {
case 'single-round': return '单轮RAG';
case 'multi-round': return '多轮RAG';
case 'deep-search': return '深度检索';
case 'compare': return '对比';
}
};
// 获取模式颜色
const getModeColor = (mode: QARecord['mode']) => {
switch (mode) {
case 'single-round':
return isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600';
case 'multi-round':
return isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600';
case 'deep-search':
return isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600';
case 'compare':
return isDark ? 'bg-orange-600/30 text-orange-400' : 'bg-orange-100 text-orange-600';
}
};
// 切换收藏
const toggleCollect = (recordId: string) => {
setRecords(records.map(record =>
record.id === recordId ? { ...record, isCollected: !record.isCollected } : record
));
if (selectedRecord?.id === recordId) {
setSelectedRecord({ ...selectedRecord, isCollected: !selectedRecord.isCollected });
}
};
// 复制问题
const copyQuestion = (question: string) => {
navigator.clipboard.writeText(question);
alert('问题已复制');
};
// 导出记录
const exportRecord = (record: QARecord) => {
alert(`导出记录: ${record.question.substring(0, 20)}...`);
};
// 转到任务
const convertToTask = (record: QARecord, taskType: 'single' | 'consult' | 'scene') => {
const messages = {
single: '单智能体任务',
consult: '多智能体会商',
scene: '场景任务',
};
alert(`已转为${messages[taskType]}`);
};
// 格式化时间
const formatTime = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return `今天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
} else if (days === 1) {
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
};
return (
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
{/* 顶部筛选栏 */}
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className="flex items-center gap-4">
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
问答记录
</h1>
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
查看历史问答、对比结果与高价值记录
</span>
</div>
{/* 筛选控件 */}
<div className="flex items-center gap-3">
{/* 搜索框 */}
<div className={`relative ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'} rounded-lg`}>
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索问题关键词..."
className={`w-64 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none ${
isDark
? 'bg-transparent text-white placeholder-slate-500'
: 'bg-transparent text-gray-800 placeholder-gray-400'
}`}
/>
</div>
{/* 记录类型 */}
<div className="relative">
<select
value={recordType}
onChange={(e) => setRecordType(e.target.value as RecordType)}
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
isDark
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
: 'bg-gray-100 text-gray-700 border border-gray-200'
}`}
>
<option value="all">全部类型</option>
<option value="general">通用问答</option>
<option value="agent">智能体问答</option>
</select>
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
isDark ? 'text-slate-400' : 'text-gray-400'
}`} />
</div>
{/* 模式 */}
<div className="relative">
<select
value={modeType}
onChange={(e) => setModeType(e.target.value as ModeType)}
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
isDark
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
: 'bg-gray-100 text-gray-700 border border-gray-200'
}`}
>
<option value="all">全部模式</option>
<option value="single-round">单轮RAG</option>
<option value="multi-round">多轮RAG</option>
<option value="deep-search">深度检索</option>
<option value="compare">对比记录</option>
</select>
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
isDark ? 'text-slate-400' : 'text-gray-400'
}`} />
</div>
{/* 收藏状态 */}
<div className="relative">
<select
value={collectionStatus}
onChange={(e) => setCollectionStatus(e.target.value as CollectionStatus)}
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
isDark
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
: 'bg-gray-100 text-gray-700 border border-gray-200'
}`}
>
<option value="all">全部</option>
<option value="collected">已收藏</option>
</select>
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
isDark ? 'text-slate-400' : 'text-gray-400'
}`} />
</div>
{/* 时间范围 */}
<div className="relative">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
isDark
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
: 'bg-gray-100 text-gray-700 border border-gray-200'
}`}
>
<option value="all">全部时间</option>
<option value="today">今天</option>
<option value="week">本周</option>
<option value="month">本月</option>
</select>
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
isDark ? 'text-slate-400' : 'text-gray-400'
}`} />
</div>
</div>
</div>
{/* 主体内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧记录列表 */}
<div className={`w-80 border-r flex-shrink-0 overflow-y-auto ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className="p-4">
<div className="space-y-2">
{filteredRecords.map((record) => (
<button
key={record.id}
onClick={() => {
setSelectedRecord(record);
setNotesInput(record.notes || '');
}}
className={`w-full text-left p-3 rounded-lg transition-all ${
selectedRecord?.id === record.id
? isDark
? 'bg-blue-600/20 border border-blue-500/50'
: 'bg-red-50 border border-red-200'
: isDark
? 'bg-slate-700/30 border border-transparent hover:bg-slate-700/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
<div className="flex items-start gap-2 mb-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
record.type === 'general'
? isDark
? 'bg-blue-600/30 text-blue-400'
: 'bg-blue-100 text-blue-600'
: isDark
? 'bg-purple-600/30 text-purple-400'
: 'bg-purple-100 text-purple-600'
}`}>
{record.type === 'general' ? '通用问答' : '智能体'}
</span>
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${getModeColor(record.mode)}`}>
{getModeLabel(record.mode)}
</span>
{record.isCollected && (
<BookmarkCheck className={`w-3.5 h-3.5 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
)}
</div>
<p className={`text-sm line-clamp-2 ${isDark ? 'text-slate-200' : 'text-gray-800'}`}>
{record.question}
</p>
<div className={`flex items-center gap-2 mt-2 text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<Calendar className="w-3 h-3" />
<span>{formatTime(record.createTime)}</span>
{record.agentName && (
<>
<span>·</span>
<Bot className="w-3 h-3" />
<span>{record.agentName}</span>
</>
)}
</div>
{/* 标签 */}
<div className="flex items-center gap-1 mt-2">
{record.isHighValue && (
<span className={`px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-yellow-600/30 text-yellow-400' : 'bg-yellow-100 text-yellow-600'
}`}>
高价值
</span>
)}
{record.isReusable && (
<span className={`px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
}`}>
可复用
</span>
)}
{record.isConvertible && (
<span className={`px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}>
可转任务
</span>
)}
</div>
</button>
))}
</div>
{filteredRecords.length === 0 && (
<div className={`text-center py-8 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<History className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>暂无匹配记录</p>
</div>
)}
</div>
</div>
{/* 中间记录详情 */}
<div className="flex-1 overflow-y-auto p-6">
{selectedRecord ? (
<div className="max-w-3xl mx-auto">
{/* 问题 */}
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<div className="flex items-center gap-2 mb-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
isDark ? 'bg-slate-600 text-slate-300' : 'bg-gray-200 text-gray-600'
}`}>
问题
</span>
</div>
<p className={`text-lg ${isDark ? 'text-white' : 'text-gray-900'}`}>
{selectedRecord.question}
</p>
</div>
{/* 回答 */}
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<div className="flex items-center gap-2 mb-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}>
{selectedRecord.type === 'general' ? 'RAG回答' : '智能体回答'}
</span>
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${getModeColor(selectedRecord.mode)}`}>
{getModeLabel(selectedRecord.mode)}
</span>
{selectedRecord.agentName && (
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}>
{selectedRecord.agentName}
</span>
)}
</div>
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{selectedRecord.answer}
</p>
</div>
{/* 来源摘要 */}
{selectedRecord.sources && selectedRecord.sources.length > 0 && (
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<div className="flex items-center gap-2 mb-2">
<FileText className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-500'}`} />
<span className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
来源摘要
</span>
</div>
<div className="flex flex-wrap gap-2">
{selectedRecord.sources.map((source, idx) => (
<span
key={idx}
className={`px-3 py-1 rounded-lg text-sm ${
isDark ? 'bg-slate-600/50 text-slate-300' : 'bg-white text-gray-600 border border-gray-200'
}`}
>
{source}
</span>
))}
</div>
</div>
)}
{/* 评价 */}
{selectedRecord.evaluation && (
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-yellow-600/20 border border-yellow-600/30' : 'bg-yellow-50 border border-yellow-200'}`}>
<div className="flex items-center gap-2 mb-2">
<Star className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
<span className={`text-sm font-medium ${isDark ? 'text-yellow-400' : 'text-yellow-700'}`}>
人工评价
</span>
</div>
<p className={`text-sm ${isDark ? 'text-yellow-300' : 'text-yellow-700'}`}>
{selectedRecord.evaluation}
</p>
</div>
)}
</div>
) : (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<MessageSquare className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">选择一条记录查看详情</p>
</div>
)}
</div>
{/* 右侧操作与标签区 */}
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'}`}>
{selectedRecord ? (
<>
{/* 记录标签 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
记录标签
</h3>
<div className="space-y-2">
<button
onClick={() => toggleCollect(selectedRecord.id)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
selectedRecord.isCollected
? isDark
? 'bg-yellow-600/20 border border-yellow-600/50'
: 'bg-yellow-50 border border-yellow-200'
: isDark
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
{selectedRecord.isCollected ? (
<BookmarkCheck className={`w-5 h-5 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
) : (
<Bookmark className={`w-5 h-5 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
)}
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{selectedRecord.isCollected ? '已收藏' : '收藏'}
</span>
</button>
<button
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
selectedRecord.isHighValue
? isDark
? 'bg-yellow-600/20 border border-yellow-600/50'
: 'bg-yellow-50 border border-yellow-200'
: isDark
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
<Star className={`w-5 h-5 ${selectedRecord.isHighValue ? (isDark ? 'text-yellow-400' : 'text-yellow-500') : (isDark ? 'text-slate-400' : 'text-gray-400')}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
高价值
</span>
</button>
<button
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
selectedRecord.isReusable
? isDark
? 'bg-green-600/20 border border-green-600/50'
: 'bg-green-50 border border-green-200'
: isDark
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
<Copy className={`w-5 h-5 ${selectedRecord.isReusable ? (isDark ? 'text-green-400' : 'text-green-500') : (isDark ? 'text-slate-400' : 'text-gray-400')}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
可复用
</span>
</button>
<button
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
selectedRecord.isConvertible
? isDark
? 'bg-blue-600/20 border border-blue-600/50'
: 'bg-blue-50 border border-blue-200'
: isDark
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
<ArrowRight className={`w-5 h-5 ${selectedRecord.isConvertible ? (isDark ? 'text-blue-400' : 'text-blue-500') : (isDark ? 'text-slate-400' : 'text-gray-400')}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
可转任务
</span>
</button>
</div>
</div>
{/* 操作按钮 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
操作
</h3>
<div className="space-y-2">
<button
onClick={() => toggleCollect(selectedRecord.id)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
isDark
? 'bg-slate-700/50 hover:bg-slate-600/50 text-slate-300'
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200'
}`}
>
{selectedRecord.isCollected ? (
<>
<BookmarkCheck className="w-5 h-5 text-yellow-500" />
<span className="text-sm">取消收藏</span>
</>
) : (
<>
<Bookmark className="w-5 h-5" />
<span className="text-sm">收藏</span>
</>
)}
</button>
<button
onClick={() => exportRecord(selectedRecord)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
isDark
? 'bg-slate-700/50 hover:bg-slate-600/50 text-slate-300'
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200'
}`}
>
<Download className="w-5 h-5" />
<span className="text-sm">导出</span>
</button>
<button
onClick={() => copyQuestion(selectedRecord.question)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
isDark
? 'bg-slate-700/50 hover:bg-slate-600/50 text-slate-300'
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200'
}`}
>
<Copy className="w-5 h-5" />
<span className="text-sm">复制问题</span>
</button>
</div>
</div>
{/* 转任务 */}
{selectedRecord.isConvertible && (
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
转为任务
</h3>
<div className="space-y-2">
<button
onClick={() => convertToTask(selectedRecord, 'single')}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
isDark
? 'bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/50'
: 'bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200'
}`}
>
<Bot className="w-5 h-5" />
<span className="text-sm flex-1 text-left">转单智能体任务</span>
<ArrowRight className="w-4 h-4" />
</button>
<button
onClick={() => convertToTask(selectedRecord, 'consult')}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
isDark
? 'bg-purple-600/20 hover:bg-purple-600/30 text-purple-400 border border-purple-600/50'
: 'bg-purple-50 hover:bg-purple-100 text-purple-600 border border-purple-200'
}`}
>
<MessageSquare className="w-5 h-5" />
<span className="text-sm flex-1 text-left">转多智能体会商</span>
<ArrowRight className="w-4 h-4" />
</button>
<button
onClick={() => convertToTask(selectedRecord, 'scene')}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
isDark
? 'bg-green-600/20 hover:bg-green-600/30 text-green-400 border border-green-600/50'
: 'bg-green-50 hover:bg-green-100 text-green-600 border border-green-200'
}`}
>
<GitCompare className="w-5 h-5" />
<span className="text-sm flex-1 text-left">转场景任务</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* 备注 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
备注
</h3>
<textarea
value={notesInput}
onChange={(e) => setNotesInput(e.target.value)}
placeholder="添加人工评价或后续处理建议..."
rows={4}
className={`w-full p-3 rounded-lg text-sm resize-none focus:outline-none ${
isDark
? 'bg-slate-700/50 text-slate-300 placeholder-slate-500 border border-slate-600'
: 'bg-white text-gray-700 placeholder-gray-400 border border-gray-200'
}`}
/>
{notesInput !== (selectedRecord.notes || '') && (
<button
onClick={() => {
setRecords(records.map(r =>
r.id === selectedRecord.id ? { ...r, notes: notesInput } : r
));
setSelectedRecord({ ...selectedRecord, notes: notesInput });
alert('备注已保存');
}}
className={`w-full mt-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isDark
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
保存备注
</button>
)}
</div>
</>
) : (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<MessageSquare className="w-12 h-12 mb-3 opacity-50" />
<p className="text-sm">选择记录后可操作</p>
</div>
)}
</div>
</div>
</div>
);
};
export default QARecords;
+543
View File
@@ -0,0 +1,543 @@
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;
+551
View File
@@ -0,0 +1,551 @@
import React, { useState } from 'react';
import {
FileText,
Search,
Filter,
Plus,
Download,
Edit3,
Send,
Clock,
CheckCircle,
AlertCircle,
ChevronRight,
Target,
Brain,
TrendingUp,
Compass,
Database,
MoreHorizontal,
FolderOpen,
Folder,
BarChart3,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
type ResultCategory = 'planning' | 'mnty' | 'effect-pg' | 'xk' | 'dq-research' | 'other';
interface ResultTask {
id: string;
title: string;
category: ResultCategory;
subCategory: string;
status: 'completed' | 'pending' | 'draft';
createdAt: string;
updatedAt: string;
reportType: 'strategy' | 'consult' | 'analysis' | 'summary';
agentCount: number;
description: string;
}
interface ResultManagementProps {
onNavigate: (page: PageType, taskId?: string) => void;
initialCategory?: ResultCategory;
}
// 任务类别配置
const categoryConfig: Record<ResultCategory, { label: string; icon: React.ElementType; color: string; subCategories: string[] }> = {
'planning': {
label: '规划任务',
icon: Target,
color: 'blue',
subCategories: ['目标分析', '品牌策略', '内容规划', '渠道规划', '叙事策略']
},
'mnty': {
label: 'MNTY任务',
icon: Brain,
color: 'purple',
subCategories: ['机制研究', '方案设计', 'ZSTY分析']
},
'effect-pg': {
label: '效果PG任务',
icon: TrendingUp,
color: 'amber',
subCategories: ['行为PG', '效果评估', '信息PG', '账号PG']
},
'xk': {
label: 'XK任务',
icon: Compass,
color: 'cyan',
subCategories: ['任务JC', '任务调度']
},
'dq-research': {
label: 'DQ研究类任务',
icon: Database,
color: 'green',
subCategories: ['受众分析', '知识库研究', '趋势研究']
},
'other': {
label: '其他类任务',
icon: MoreHorizontal,
color: 'slate',
subCategories: ['通用分析', '临时任务', '其他']
}
};
// 模拟任务数据
const mockTasks: ResultTask[] = [
{
id: 'task-001',
title: '五四青年节宣传活动策略',
category: 'planning',
subCategory: '叙事策略',
status: 'completed',
createdAt: '2026-03-19 10:30',
updatedAt: '2026-03-19 15:30',
reportType: 'strategy',
agentCount: 6,
description: '针对五四青年节的品牌传播策略方案'
},
{
id: 'task-002',
title: '品牌年轻化受众分析',
category: 'dq-research',
subCategory: '受众分析',
status: 'completed',
createdAt: '2026-03-18 14:20',
updatedAt: '2026-03-18 16:45',
reportType: 'analysis',
agentCount: 3,
description: '分析Z世代用户的消费行为和品牌偏好'
},
{
id: 'task-003',
title: 'UGC激励机制设计',
category: 'effect-pg',
subCategory: '效果评估',
status: 'completed',
createdAt: '2026-03-17 09:00',
updatedAt: '2026-03-17 11:20',
reportType: 'strategy',
agentCount: 4,
description: '设计用户生成内容激励机制方案'
},
{
id: 'task-004',
title: '短视频平台效果预测',
category: 'mnty',
subCategory: '方案设计',
status: 'pending',
createdAt: '2026-03-20 08:00',
updatedAt: '2026-03-20 08:00',
reportType: 'consult',
agentCount: 5,
description: '预测不同短视频平台的传播效果'
},
{
id: 'task-005',
title: '活动传播风险评估',
category: 'planning',
subCategory: '目标分析',
status: 'draft',
createdAt: '2026-03-20 10:00',
updatedAt: '2026-03-20 10:30',
reportType: 'analysis',
agentCount: 2,
description: '评估五四活动可能面临的传播风险'
},
{
id: 'task-006',
title: '竞品营销策略研究',
category: 'dq-research',
subCategory: '趋势研究',
status: 'completed',
createdAt: '2026-03-16 13:00',
updatedAt: '2026-03-16 18:00',
reportType: 'analysis',
agentCount: 3,
description: '分析主要竞品的营销策略和效果'
},
];
const ResultManagement: React.FC<ResultManagementProps> = ({ onNavigate, initialCategory = 'planning' }) => {
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 inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
// 状态
const [selectedCategory, setSelectedCategory] = useState<ResultCategory>(initialCategory);
const [selectedSubCategory, setSelectedSubCategory] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'completed' | 'pending' | 'draft'>('all');
const [expandedCategories, setExpandedCategories] = useState<Set<ResultCategory>>(
new Set([initialCategory])
);
// 切换分类展开状态
const toggleCategory = (category: ResultCategory) => {
setExpandedCategories(prev => {
const next = new Set(prev);
if (next.has(category)) {
next.delete(category);
} else {
next.add(category);
}
return next;
});
};
// 过滤任务
const filteredTasks = mockTasks.filter(task => {
const matchesCategory = task.category === selectedCategory;
const matchesSubCategory = !selectedSubCategory || task.subCategory === selectedSubCategory;
const matchesSearch = !searchQuery ||
task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
task.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
return matchesCategory && matchesSubCategory && matchesSearch && matchesStatus;
});
// 获取状态样式
const getStatusStyle = (status: ResultTask['status']) => {
switch (status) {
case 'completed':
return {
bg: isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700',
icon: CheckCircle,
label: '已完成'
};
case 'pending':
return {
bg: isDark ? 'bg-yellow-500/20 text-yellow-400' : 'bg-yellow-100 text-yellow-700',
icon: Clock,
label: '进行中'
};
case 'draft':
return {
bg: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-100 text-gray-700',
icon: Edit3,
label: '草稿'
};
}
};
// 获取报告类型样式
const getReportTypeStyle = (type: ResultTask['reportType']) => {
const styles = {
'strategy': { color: 'blue', label: '策略报告' },
'consult': { color: 'purple', label: '会商报告' },
'analysis': { color: 'cyan', label: '分析报告' },
'summary': { color: 'green', label: '总结报告' }
};
return styles[type];
};
// 查看任务详情
const handleViewDetail = (taskId: string) => {
onNavigate('result-detail', taskId);
};
const currentCategoryConfig = categoryConfig[selectedCategory];
const CategoryIcon = currentCategoryConfig.icon;
return (
<div className="h-full flex gap-6">
{/* 左侧分类导航 */}
<div className={`w-72 flex-shrink-0 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 overflow-hidden flex flex-col`}>
<div className="flex items-center justify-between mb-4">
<h2 className={`${textPrimary} font-semibold text-base`}>结果管理</h2>
<button className={`p-1.5 rounded-lg ${btnSecondary}`}>
<Plus className="w-4 h-4" />
</button>
</div>
{/* 分类列表 */}
<div className="flex-1 overflow-y-auto space-y-2 custom-scrollbar">
{(Object.entries(categoryConfig) as [ResultCategory, typeof categoryConfig[ResultCategory]][]).map(([key, config]) => {
const Icon = config.icon;
const isExpanded = expandedCategories.has(key);
const isActive = selectedCategory === key;
return (
<div key={key} className="space-y-1">
{/* 一级分类 */}
<button
onClick={() => {
setSelectedCategory(key);
setSelectedSubCategory(null);
if (!expandedCategories.has(key)) {
toggleCategory(key);
}
}}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all text-left ${
isActive
? isDark
? 'bg-blue-600/20 text-blue-400 border border-blue-500/30'
: 'bg-red-50 text-red-600 border border-red-200'
: isDark
? 'text-slate-400 hover:text-white hover:bg-slate-700/50'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
}`}
>
{isExpanded ? (
<FolderOpen className={`w-4 h-4 ${isActive ? '' : 'opacity-60'}`} />
) : (
<Folder className={`w-4 h-4 ${isActive ? '' : 'opacity-60'}`} />
)}
<span className="text-sm font-medium flex-1">{config.label}</span>
<ChevronRight className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-90' : ''} ${isActive ? '' : 'opacity-60'}`} />
</button>
{/* 二级分类 */}
{isExpanded && (
<div className="ml-4 pl-4 border-l space-y-1">
{/* 全部 */}
<button
onClick={() => {
setSelectedCategory(key);
setSelectedSubCategory(null);
}}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all text-left ${
selectedCategory === key && !selectedSubCategory
? isDark
? 'bg-blue-500/20 text-blue-400'
: 'bg-red-100 text-red-600'
: isDark
? 'text-slate-500 hover:text-slate-300'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<BarChart3 className="w-3.5 h-3.5" />
<span className="text-xs font-medium">全部</span>
<span className={`ml-auto px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-500'
}`}>
{mockTasks.filter(t => t.category === key).length}
</span>
</button>
{/* 各子分类 */}
{config.subCategories.map(sub => {
const count = mockTasks.filter(t => t.category === key && t.subCategory === sub).length;
if (count === 0) return null;
return (
<button
key={sub}
onClick={() => {
setSelectedCategory(key);
setSelectedSubCategory(sub);
}}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all text-left ${
selectedCategory === key && selectedSubCategory === sub
? isDark
? 'bg-blue-500/20 text-blue-400'
: 'bg-red-100 text-red-600'
: isDark
? 'text-slate-500 hover:text-slate-300'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<div className={`w-1.5 h-1.5 rounded-full ${
isActive ? `bg-${config.color}-400` : (isDark ? 'bg-slate-600' : 'bg-gray-300')
}`} />
<span className="text-xs">{sub}</span>
<span className={`ml-auto px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-500'
}`}>
{count}
</span>
</button>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
{/* 右侧任务列表 */}
<div className="flex-1 flex flex-col min-w-0">
{/* 顶部工具栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 mb-4`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl bg-${currentCategoryConfig.color}-500/20 flex items-center justify-center`}>
<CategoryIcon className={`w-5 h-5 text-${currentCategoryConfig.color}-400`} />
</div>
<div>
<h2 className={`${textPrimary} font-semibold`}>
{selectedSubCategory || currentCategoryConfig.label}
</h2>
<p className={`${textMuted} text-xs`}>
共 {filteredTasks.length} 个结果
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button className={`flex items-center gap-2 px-3 py-2 rounded-lg ${btnSecondary} text-sm`}>
<Download className="w-4 h-4" />
批量导出
</button>
</div>
</div>
{/* 搜索和筛选 */}
<div className="flex items-center gap-3">
<div className={`flex-1 relative ${inputBg} rounded-lg`}>
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${textMuted}`} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索任务标题或描述..."
className={`w-full pl-10 pr-4 py-2.5 rounded-lg text-sm bg-transparent focus:outline-none ${
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
}`}
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
className={`px-3 py-2.5 rounded-lg text-sm ${inputBg} ${inputBorder} focus:outline-none ${
isDark ? 'text-slate-300' : 'text-gray-700'
}`}
>
<option value="all">全部状态</option>
<option value="completed">已完成</option>
<option value="pending">进行中</option>
<option value="draft">草稿</option>
</select>
</div>
</div>
{/* 任务列表 */}
<div className={`flex-1 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl overflow-hidden`}>
{filteredTasks.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center">
<FileText className={`w-16 h-16 ${textMuted} mb-4`} />
<p className={`${textSecondary} text-lg`}>暂无结果</p>
<p className={`${textMuted} text-sm mt-1`}>在对应任务中生成结果后,将在此显示</p>
</div>
) : (
<div className="h-full overflow-y-auto custom-scrollbar">
<table className="w-full">
<thead className={`sticky top-0 ${isDark ? 'bg-slate-800/80' : 'bg-gray-50'}`}>
<tr className={`text-xs ${textMuted} border-b ${cardBorder}`}>
<th className="text-left font-medium px-4 py-3">任务名称</th>
<th className="text-left font-medium px-4 py-3">报告类型</th>
<th className="text-left font-medium px-4 py-3">智能体数</th>
<th className="text-left font-medium px-4 py-3">状态</th>
<th className="text-left font-medium px-4 py-3">更新时间</th>
<th className="text-left font-medium px-4 py-3">操作</th>
</tr>
</thead>
<tbody>
{filteredTasks.map((task) => {
const statusInfo = getStatusStyle(task.status);
const StatusIcon = statusInfo.icon;
const reportTypeInfo = getReportTypeStyle(task.reportType);
return (
<tr
key={task.id}
className={`border-b cursor-pointer hover:${
isDark ? 'bg-slate-700/30' : 'bg-gray-50'
} transition-colors ${cardBorder}`}
onClick={() => handleViewDetail(task.id)}
>
<td className="px-4 py-4">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg bg-${reportTypeInfo.color}-500/20 flex items-center justify-center flex-shrink-0`}>
<FileText className={`w-5 h-5 text-${reportTypeInfo.color}-400`} />
</div>
<div className="min-w-0">
<p className={`${textPrimary} text-sm font-medium truncate`}>{task.title}</p>
<p className={`${textMuted} text-xs truncate`}>{task.description}</p>
</div>
</div>
</td>
<td className="px-4 py-4">
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
isDark ? `bg-${reportTypeInfo.color}-500/20 text-${reportTypeInfo.color}-400` : `bg-${reportTypeInfo.color}-100 text-${reportTypeInfo.color}-700`
}`}>
{reportTypeInfo.label}
</span>
</td>
<td className="px-4 py-4">
<span className={`${textSecondary} text-sm`}>{task.agentCount}</span>
</td>
<td className="px-4 py-4">
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs font-medium ${statusInfo.bg}`}>
<StatusIcon className="w-3 h-3" />
{statusInfo.label}
</span>
</td>
<td className="px-4 py-4">
<span className={`${textMuted} text-sm`}>{task.updatedAt}</span>
</td>
<td className="px-4 py-4" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-1">
<button
onClick={() => handleViewDetail(task.id)}
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
title="查看详情"
>
<FileText className={`w-4 h-4 ${textSecondary}`} />
</button>
<button
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
title="编辑"
>
<Edit3 className={`w-4 h-4 ${textSecondary}`} />
</button>
<button
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
title="下载"
>
<Download className={`w-4 h-4 ${textSecondary}`} />
</button>
<button
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
title="呈报"
>
<Send className={`w-4 h-4 ${textSecondary}`} />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* 自定义滚动条样式 */}
<style>{`
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: ${isDark ? 'rgba(148, 163, 184, 0.3)' : 'rgba(0, 0, 0, 0.15)'};
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: ${isDark ? 'rgba(148, 163, 184, 0.5)' : 'rgba(0, 0, 0, 0.25)'};
}
`}</style>
</div>
);
};
export default ResultManagement;
+556
View File
@@ -0,0 +1,556 @@
import React, { useState } from 'react';
import { ArrowLeft, Save, RotateCcw, Play, Pause, Settings, FileText, GitBranch, ListChecks, Shield, Bot, Clock, CheckCircle, X, Plus, ChevronDown, AlertTriangle } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
interface SceneDetailProps {
sceneId: string | null;
onBack: () => void;
}
interface FlowNode {
id: number;
name: string;
agent: string;
status: 'completed' | 'active' | 'pending';
inputs: number;
outputs: number;
inputSource: string[];
outputFields: string[];
releaseCondition: string;
rollbackNode: string;
allowParallel: boolean;
description: string;
}
const SceneDetail: React.FC<SceneDetailProps> = ({ sceneId, 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-red-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 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 btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
const [activeTab, setActiveTab] = useState('flow');
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
const tabs = [
{ id: 'basic', label: '基础设置', icon: FileText },
{ id: 'flow', label: '流程编排', icon: GitBranch },
{ id: 'template', label: '输入输出模板', icon: ListChecks },
{ id: 'rules', label: '运行规则', icon: Shield },
];
const flowNodes: FlowNode[] = [
{ id: 1, name: '目标分析', agent: 'GoalAgent', status: 'completed', inputs: 2, outputs: 3, inputSource: ['task_background', 'primary_goal_hint'], outputFields: ['goal_model.primary_goal', 'goal_model.secondary_goal'], releaseCondition: '目标明确且可量化', rollbackNode: '无', allowParallel: false, description: '分析并确定宣传的核心目标' },
{ id: 2, name: '受众分析', agent: 'AudienceAgent', status: 'completed', inputs: 3, outputs: 4, inputSource: ['goal_model', 'task_background'], outputFields: ['audience_model.core_audience', 'audience_model.secondary_audience'], releaseCondition: '核心受众明确', rollbackNode: '目标分析', allowParallel: false, description: '分析目标受众特征和需求' },
{ id: 3, name: '行为分析', agent: 'BehaviorAgent', status: 'active', inputs: 4, outputs: 3, inputSource: ['audience_model', 'goal_model'], outputFields: ['behavior_model.target_behavior', 'behavior_model.conversion_path'], releaseCondition: '目标行为明确', rollbackNode: '受众分析', allowParallel: false, description: '设计目标受众的预期行为' },
{ id: 4, name: '驱动分析', agent: 'DriverAgent', status: 'pending', inputs: 3, outputs: 2, inputSource: ['behavior_model', 'audience_model'], outputFields: ['driver_model.primary_driver', 'driver_model.secondary_driver'], releaseCondition: '核心驱动明确', rollbackNode: '行为分析', allowParallel: true, description: '分析影响受众行为的关键驱动因素' },
{ id: 5, name: '叙事分析', agent: 'NarrativeAgent', status: 'pending', inputs: 2, outputs: 4, inputSource: ['driver_model', 'audience_model'], outputFields: ['narrative_model.core_narrative', 'narrative_model.message_points'], releaseCondition: '叙事框架明确', rollbackNode: '驱动分析', allowParallel: false, description: '设计传播叙事策略' },
{ id: 6, name: '逻辑链收敛', agent: 'LogicChainAgent', status: 'pending', inputs: 4, outputs: 2, inputSource: ['goal_model', 'audience_model', 'behavior_model', 'narrative_model'], outputFields: ['strategy_chain.logic_statement', 'strategy_chain.consistency_score'], releaseCondition: '逻辑链校验通过', rollbackNode: '叙事分析', allowParallel: false, description: '验证策略链完整性和一致性' },
{ id: 7, name: '风险评估', agent: 'RiskAgent', status: 'pending', inputs: 2, outputs: 3, inputSource: ['strategy_chain', 'task_constraints'], outputFields: ['risk_model.risk_items', 'risk_model.mitigation_suggestions'], releaseCondition: '高风险项已处理', rollbackNode: '逻辑链收敛', allowParallel: true, description: '评估策略执行风险' },
{ id: 8, name: '方案生成', agent: 'DraftAgent', status: 'pending', inputs: 3, outputs: 5, inputSource: ['strategy_chain', 'narrative_model', 'risk_model'], outputFields: ['final_plan', 'execution_timeline', 'evaluation_metrics'], releaseCondition: '方案完整可执行', rollbackNode: '风险评估', allowParallel: false, description: '生成最终宣传方案' },
];
const sceneSummary = {
flowNodesCount: 8,
boundAgentsCount: 8,
serialNodes: 6,
parallelNodes: 2,
logicChainEnabled: true,
lastPublishedTime: '2026-03-18 14:30',
};
const inputTemplate = [
{ name: '任务标题', type: 'STRING', required: true },
{ name: '任务背景', type: 'TEXT', required: true },
{ name: '上级要求', type: 'TEXT', required: false },
{ name: '时间要求', type: 'DATE', required: true },
{ name: '资源条件', type: 'JSON', required: false },
{ name: '风险边界', type: 'TEXT', required: false },
];
const outputTemplate = {
middle: ['goal_model', 'audience_model', 'behavior_model', 'driver_model', 'narrative_model', 'strategy_chain'],
final: ['讨论结果摘要', '最终策略链', '正式方案', '风险说明', '评估指标'],
};
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={onBack}
className={`p-2 ${isDark ? 'hover:bg-slate-800/50' : 'hover:bg-gray-100'} rounded-lg transition-colors`}
>
<ArrowLeft className={`w-5 h-5 ${textSecondary}`} />
</button>
<div>
<div className="flex items-center gap-3">
<h1 className={`text-2xl font-bold ${textPrimary}`}>业务链条配置详情</h1>
<span className="px-2.5 py-1 bg-green-500/20 text-green-400 rounded-full text-xs font-medium">
启用
</span>
</div>
<p className={`${textSecondary} text-sm mt-1`}></p>
</div>
</div>
{/* <div className="flex items-center gap-3">
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg transition-colors`}>
<RotateCcw className="w-4 h-4" />
<span className="text-sm">重置</span>
</button>
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} ${textPrimary} rounded-lg transition-colors`}>
<Save className="w-4 h-4" />
<span className="text-sm">保存配置</span>
</button>
</div> */}
</div>
{/* Tab切换 */}
{/* <div className={`flex items-center gap-2 border-b ${cardBorder}`}>
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-3 border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-400'
: `border-transparent ${textSecondary} hover:${textPrimary}`
}`}
>
<Icon className="w-4 h-4" />
<span className="text-sm font-medium">{tab.label}</span>
</button>
);
})}
</div> */}
{/* 内容区 */}
<div className="grid grid-cols-4 gap-6">
{/* 主内容区 */}
<div className="col-span-3">
{activeTab === 'basic' && (
<div className="grid grid-cols-2 gap-6">
{/* 左侧表单区 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6 space-y-4`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>基础信息</h3>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>场景名称</label>
<input
type="text"
defaultValue="宣传策略生成"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>场景编码</label>
<input
type="text"
defaultValue="SCENE_001"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>场景说明</label>
<textarea
rows={3}
defaultValue="用于生成完整的宣传策略方案,包含目标、受众、行为、驱动、叙事等维度的分析"
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50 resize-none`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>场景分类</label>
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}>
<option>策略生成</option>
<option>方案推演</option>
<option>活动复盘</option>
</select>
</div>
</div>
{/* 右侧说明区 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6 space-y-4`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>场景说明</h3>
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<p className="text-blue-400 text-sm font-medium mb-2">场景简介</p>
<p className={`${textSecondary} text-sm`}>完整的宣传策略生成流程,覆盖从目标分析到方案输出的全链路。</p>
</div>
<div className={`p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<p className={`${textSecondary} text-sm font-medium mb-2`}>最近修改记录</p>
<p className={`${textSecondary} text-sm`}>2026-03-18 14:30 由 张策略 修改</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg text-center">
<p className="text-cyan-400 text-2xl font-bold">12</p>
<p className={`${textSecondary} text-xs`}>关联任务</p>
</div>
<div className="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg text-center">
<p className="text-purple-400 text-2xl font-bold">8</p>
<p className={`${textSecondary} text-xs`}>关联智能体</p>
</div>
</div>
<div className={`flex items-center justify-between py-3 border-t ${cardBorder}`}>
<span className={`${textSecondary} text-sm`}>设为默认场景</span>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
</div>
)}
{activeTab === 'flow' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<div className="flex items-center justify-between mb-6">
<h3 className={`${textPrimary} font-semibold`}>流程编排</h3>
<button className="flex items-center gap-2 px-3 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 text-blue-400 rounded-lg text-sm transition-colors">
<Plus className="w-4 h-4" />
添加节点
</button>
</div>
{/* 流程图区域 */}
<div className="relative">
{/* 流程节点 */}
<div className="space-y-4">
{flowNodes.map((node, index) => (
<div key={node.id} className="relative">
<div className="flex items-center gap-4">
{/* 连接线 */}
{index > 0 && (
<div className="absolute left-6 -top-4 w-0.5 h-4 bg-blue-500/50" />
)}
{/* 节点卡片 */}
<button
onClick={() => setSelectedNode(node)}
className={`flex-1 flex items-center gap-4 p-4 rounded-xl border-2 transition-all text-left ${
selectedNode?.id === node.id
? 'bg-blue-500/20 border-blue-500 shadow-lg shadow-blue-500/20'
: node.status === 'completed'
? 'bg-green-500/10 border-green-500/30 hover:border-green-500/50'
: node.status === 'active'
? 'bg-blue-500/10 border-blue-500/50 hover:border-blue-500'
: `${inputBg} ${cardBorder} ${isDark ? 'hover:border-blue-700' : 'hover:border-red-300'}`
}`}
>
{/* 节点序号 */}
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold ${
node.status === 'completed'
? 'bg-green-500 text-white'
: node.status === 'active'
? 'bg-blue-500 text-white'
: isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-600'
}`}>
{node.status === 'completed' ? <CheckCircle className="w-5 h-5" /> : node.id}
</div>
{/* 节点信息 */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<h4 className={`${textPrimary} font-medium`}>{node.name}</h4>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
node.status === 'completed' ? 'bg-green-500/20 text-green-400' :
node.status === 'active' ? 'bg-blue-500/20 text-blue-400' :
isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-600'
}`}>
{node.status === 'completed' ? '已完成' : node.status === 'active' ? '进行中' : '待执行'}
</span>
{node.allowParallel && (
<span className="px-2 py-0.5 bg-amber-500/20 text-amber-400 rounded text-xs">
可并行
</span>
)}
</div>
<div className={`flex items-center gap-4 text-sm ${textSecondary}`}>
<span className="flex items-center gap-1">
<Bot className="w-3 h-3" />
{node.agent}
</span>
<span>输入 {node.inputs}</span>
<span>输出 {node.outputs}</span>
</div>
</div>
{/* 放行条件 */}
<div className="text-right">
<p className={`${textMuted} text-xs mb-1`}>放行条件</p>
<p className={`${textSecondary} text-sm`}>{node.releaseCondition}</p>
</div>
<ChevronDown className={`w-5 h-5 ${textMuted}`} />
</button>
{/* 回退虚线 */}
{node.rollbackNode !== '无' && (
<div className="absolute -right-16 top-1/2 transform -translate-y-1/2">
<div className="flex items-center gap-1 text-amber-400 text-xs">
<span className="w-8 border-t-2 border-dashed border-amber-400/50" />
<RotateCcw className="w-3 h-3" />
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
{/* 图例 */}
<div className={`mt-6 flex items-center gap-6 pt-6 border-t ${cardBorder}`}>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-400" />
<span className={`${textSecondary} text-sm`}>已完成</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-400" />
<span className={`${textSecondary} text-sm`}>进行中</span>
</div>
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${isDark ? 'bg-slate-600' : 'bg-gray-400'}`} />
<span className={`${textSecondary} text-sm`}>待执行</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-amber-400" />
<span className={`${textSecondary} text-sm`}>可并行</span>
</div>
</div>
</div>
)}
{activeTab === 'template' && (
<div className="space-y-6">
{/* 任务输入模板 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>任务输入模板</h3>
<div className="grid grid-cols-3 gap-4">
{inputTemplate.map((field) => (
<div key={field.name} className={`p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center justify-between mb-2">
<p className={`${textPrimary} text-sm font-medium`}>{field.name}</p>
{field.required && (
<span className="px-2 py-0.5 bg-red-500/20 text-red-400 rounded text-xs">必填</span>
)}
</div>
<p className={`${textMuted} text-xs`}>{field.type}</p>
</div>
))}
</div>
</div>
{/* 中间输出模板 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>中间输出模板</h3>
<div className="grid grid-cols-3 gap-4">
{outputTemplate.middle.map((field) => (
<div key={field} className="p-4 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
<p className="text-cyan-400 text-sm font-mono">{field}</p>
</div>
))}
</div>
</div>
{/* 最终输出模板 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>最终输出模板</h3>
<div className="grid grid-cols-3 gap-4">
{outputTemplate.final.map((field) => (
<div key={field} className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-green-400 text-sm">{field}</p>
</div>
))}
</div>
</div>
</div>
)}
{activeTab === 'rules' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6 space-y-6`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>运行规则配置</h3>
{[
{ title: '启用状态门控', desc: '每个阶段需要明确放行才能进入下一阶段', enabled: true },
{ title: '启用逻辑链校验', desc: '自动检查策略链的完整性和一致性', enabled: true },
{ title: '启用风险强制复核', desc: '高风险项必须人工确认才能继续', enabled: false },
{ title: '允许局部并行', desc: '部分独立阶段可以并行执行', enabled: true },
].map((rule, index) => (
<div key={index} className={`flex items-center justify-between py-4 border-b ${cardBorder} last:border-0`}>
<div>
<div className="flex items-center gap-2">
<p className={`${textPrimary} font-medium`}>{rule.title}</p>
{rule.enabled && (
<CheckCircle className="w-4 h-4 text-green-400" />
)}
</div>
<p className={`${textMuted} text-sm mt-1`}>{rule.desc}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked={rule.enabled} />
<div className="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
))}
<div className="grid grid-cols-2 gap-6 pt-4">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>最大讨论轮数</label>
<input
type="number"
defaultValue={10}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>最大回退次数</label>
<input
type="number"
defaultValue={3}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
/>
</div>
</div>
<div className="pt-4">
<label className={`block ${textSecondary} text-sm mb-2`}>异常中断处理方式</label>
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}>
<option>自动保存并暂停</option>
<option>自动回退到上一节点</option>
<option>通知负责人并等待</option>
</select>
</div>
</div>
)}
</div>
{/* 右侧场景摘要区 */}
<div className="space-y-4">
{/* 场景统计卡片 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>场景摘要</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<span className={`${textSecondary} text-sm`}>流程节点数</span>
<span className="text-blue-400 font-bold">{sceneSummary.flowNodesCount}</span>
</div>
<div className="flex items-center justify-between p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
<span className={`${textSecondary} text-sm`}>绑定智能体</span>
<span className="text-purple-400 font-bold">{sceneSummary.boundAgentsCount}</span>
</div>
<div className={`flex items-center justify-between p-3 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-300'} rounded-lg`}>
<span className={`${textSecondary} text-sm`}>串行节点</span>
<span className={`${textSecondary}`}>{sceneSummary.serialNodes}</span>
</div>
<div className="flex items-center justify-between p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<span className={`${textSecondary} text-sm`}>并行节点</span>
<span className="text-amber-400">{sceneSummary.parallelNodes}</span>
</div>
<div className="flex items-center justify-between p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<span className={`${textSecondary} text-sm`}>逻辑链校验</span>
<span className="text-green-400">{sceneSummary.logicChainEnabled ? '已启用' : '未启用'}</span>
</div>
</div>
</div>
{/* 最近发布时间 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<div className={`flex items-center gap-2 ${textSecondary} text-sm mb-2`}>
<Clock className="w-4 h-4" />
最近发布时间
</div>
<p className={`${textPrimary}`}>{sceneSummary.lastPublishedTime}</p>
</div>
{/* 节点属性面板 */}
{selectedNode && (
<div className={`${cardBg} backdrop-blur-xl border border-blue-500/30 rounded-xl p-5`}>
<div className="flex items-center justify-between mb-4">
<h3 className={`${textPrimary} font-semibold`}>节点属性</h3>
<button
onClick={() => setSelectedNode(null)}
className={`p-1 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-100'} rounded`}
>
<X className={`w-4 h-4 ${textSecondary}`} />
</button>
</div>
<div className="space-y-4">
<div>
<p className={`${textMuted} text-xs mb-1`}>节点名称</p>
<p className={`${textPrimary}`}>{selectedNode.name}</p>
</div>
<div>
<p className={`${textMuted} text-xs mb-1`}>绑定智能体</p>
<p className="text-cyan-400">{selectedNode.agent}</p>
</div>
<div>
<p className={`${textMuted} text-xs mb-1`}>上游依赖</p>
<div className="space-y-1">
{selectedNode.inputSource.map((input) => (
<div key={input} className={`${textSecondary} text-sm ${inputBg} px-2 py-1 rounded`}>
{input}
</div>
))}
</div>
</div>
<div>
<p className={`${textMuted} text-xs mb-1`}>输出字段</p>
<div className="space-y-1">
{selectedNode.outputFields.map((output) => (
<div key={output} className="text-green-400 text-sm bg-green-500/10 px-2 py-1 rounded">
{output}
</div>
))}
</div>
</div>
<div>
<p className={`${textMuted} text-xs mb-1`}>放行条件</p>
<p className="text-amber-400 text-sm">{selectedNode.releaseCondition}</p>
</div>
<div>
<p className={`${textMuted} text-xs mb-1`}>回退节点</p>
<p className={`${textSecondary} text-sm`}>{selectedNode.rollbackNode}</p>
</div>
<div className="flex items-center justify-between">
<span className={`${textMuted} text-sm`}>允许并行</span>
<span className={selectedNode.allowParallel ? 'text-green-400' : `${textSecondary}`}>
{selectedNode.allowParallel ? '是' : '否'}
</span>
</div>
</div>
<div className={`mt-4 pt-4 border-t ${cardBorder} space-y-2`}>
<button className={`w-full flex items-center justify-center gap-2 px-3 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors`}>
<Settings className="w-4 h-4" />
编辑节点
</button>
<div className="flex gap-2">
<button className={`flex-1 flex items-center justify-center gap-1 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Plus className="w-3 h-3" />
前置
</button>
<button className={`flex-1 flex items-center justify-center gap-1 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
<Plus className="w-3 h-3" />
后置
</button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default SceneDetail;
+482
View File
@@ -0,0 +1,482 @@
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Search, Eye, Edit, Copy, Trash2, Play, Pause, ChevronRight, Layers } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
import { requestCrudService } from '@/service/request-axios-service';
import { Pagination, Dialog, Input, Textarea, Radio, Button, MessagePlugin, Popconfirm, Loading } from 'tdesign-react';
import { useNavigate } from 'react-router-dom';
interface SceneListProps {
onViewDetail: (id: string) => void;
onNavigate: (page: PageType) => void;
}
interface sceneItem {
id?: number;
status?: number;
applicableTask?: string;
sceneName?: string;
sceneType: string;
sceneIntro: string;
sceneCode: string;
relatedAgent: string;
deleted?: number;
updateTime?: string;
taskJson?: string;
version: string;
}
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
const sceneStatus = {
0: '执行中',
1: '成功',
2: '失败',
3: '部分成功'
}
const SceneList: React.FC<SceneListProps> = ({ onViewDetail, onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [visible, setVisible] = useState(false);
const [editDataShow, setEditDataShow] = useState(false);
const [sceneName, setSceneName] = useState('');
const [scenes, setScenes] = useState<sceneItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editLoading, setEditLoading] = useState<boolean>(false);
const [pageNum, setPageNum] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [total, setTotal] = useState<number>(0);
const [formData, setFormData] = useState<sceneItem>({
applicableTask: '',
relatedAgent: '',
sceneCode: '',
sceneIntro: '',
sceneName: '',
sceneType: '',
taskJson: '',
version: '',
});
const state = useRef({
pageNum: 1,
pageSize: 10
});
const navigate = useNavigate();
const scenesDataSearch = async (pageNumParam?: number, pageSizeParam?: number, sceneNameParam?: string) => {
const parmy = {
pageNum: pageNumParam ?? state.current.pageNum,
pageSize: pageSizeParam ?? state.current.pageSize,
sceneName: sceneNameParam ?? sceneName
}
setLoading(true);
const res = await requestCrudService.get(`${zncmUrl}/scene/scenes`, parmy);
setLoading(false);
res.data.records.forEach((item) => {
if (item.taskJson) {
item.taskJson = JSON.parse(item.taskJson);
}
})
console.log('res.data.records', res.data.records)
setScenes(res.data.records || []);
setTotal(res.data.total || 0);
}
const addScene = async () => {
const res = await requestCrudService.post(`${zncmUrl}/scene/sceneAdd`, formData);
setVisible(false);
MessagePlugin.success('新增场景成功');
await scenesDataSearch();
}
const updateScene = async () => {
setEditLoading(true);
const res = await requestCrudService.post(`${zncmUrl}/scene/sceneUpdate`, {
...formData,
});
setEditLoading(false);
setEditDataShow(false);
MessagePlugin.success('更新场景成功');
await scenesDataSearch();
}
const handleDataChange = (key: keyof typeof formData, value: string | number) => {
setFormData((prev) => ({
...prev,
[key]: value,
}));
};
const showDialog = () => {
setFormData({
applicableTask: '',
relatedAgent: '',
sceneCode: '',
sceneIntro: '',
sceneName: '',
sceneType: '',
version: ''
});
setVisible(true);
};
const editShowChange = async (item: sceneItem) => {
const response = await fetch('https://ch1.b.uat.4.cn/zncmweb/templates/6b_template.json');
const data = await response.json();
console.log('response', JSON.stringify(data));
// let taskJson =
setFormData({
applicableTask: item.applicableTask,
deleted: item.deleted,
id: item.id,
relatedAgent: item.relatedAgent,
sceneCode: item.sceneCode,
sceneIntro: item.sceneIntro,
sceneName: item.sceneName,
sceneType: item.sceneType,
status: item.status,
taskJson: JSON.stringify(data),
version: item.version
});
setEditDataShow(true);
}
const { Group: RadioGroup, Button: RadioButton } = Radio;
const delDataChange = async (item: sceneItem) => {
const res = await requestCrudService.delete(`${zncmUrl}/scene/sceneDelete/${item.id}`)
console.log(res);
MessagePlugin.success('场景删除成功');
await scenesDataSearch();
}
const handleConfirm = (item: sceneItem) => {
// 确认删除逻辑
console.log('用户点击了确认');
delDataChange(item);
return true; // 返回 true 表示确认执行
};
const handleCancel = () => {
// 取消删除逻辑
console.log('用户点击了取消');
return true; // 返回 true 表示取消
};
const pageIndexChange = async (value: number) => {
state.current.pageNum = value;
setPageNum(value);
await scenesDataSearch();
}
const pageSizeChange = async (value: number) => {
state.current.pageSize = value;
setPageSize(value);
await scenesDataSearch();
}
// 回车处理
const handleKeyDown = async (e) => {
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
e.preventDefault();
} else if (e.keyCode === 13) {
e.preventDefault();
await scenesDataSearch();
} else if (e.key === "/") {
e.preventDefault();
}
};
useEffect(() => {
scenesDataSearch();
}, [])
return (
<div className="space-y-6">
{/* 页面标题区 */}
<div className="flex items-center justify-between">
<div>
<h1 className={`text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'} mb-2`}>业务链条管理</h1>
<p className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>管理和配置宣传策略流程模板</p>
</div>
<Button theme="primary" onClick={showDialog}>
<Plus className="w-4 h-4 mr-2" />
<span className="text-sm font-medium">新建场景</span>
</Button>
</div>
{/* 搜索和筛选区 */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
<input
type="text"
value={sceneName}
onChange={(e) => setSceneName(e.target.value)}
onKeyDown={(e) => handleKeyDown(e)}
placeholder="搜索场景名称..."
className={`w-full pl-10 pr-4 py-2.5 ${isDark ? 'bg-slate-800/30' : 'bg-gray-50'} border ${isDark ? 'border-blue-800/30' : 'border-gray-300'} rounded-lg ${isDark ? 'text-white' : 'text-gray-900'} placeholder-${isDark ? 'text-slate-500' : 'text-gray-500'} text-sm focus:outline-none focus:border-blue-500/50 transition-colors`}
/>
</div>
{/* <button className={`px-4 py-2.5 ${isDark ? 'bg-slate-800/50 border-blue-800/30' : 'bg-gray-100 border-gray-300'} rounded-lg ${isDark ? 'bg-slate-700/50 border-blue-800/30 text-slate-300 hover:bg-slate-600/50' : 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'} transition-colors`}>
全部类型
</button>
<button className={`px-4 py-2.5 ${isDark ? 'bg-slate-800/50 border-blue-800/30' : 'bg-gray-100 border-gray-300'} rounded-lg ${isDark ? 'bg-slate-700/50 border-blue-800/30 text-slate-300 hover:bg-slate-600/50' : 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'} transition-colors`}>
全部状态
</button> */}
</div>
{/* 场景列表 */}
<div className={`${isDark ? 'bg-slate-800/30' : 'bg-white'} backdrop-blur-xl border ${isDark ? 'border-blue-800/20' : 'border-gray-200'} rounded-xl overflow-hidden`}>
<Loading loading={loading} text="正在加载...">
<table className="w-full">
<thead>
<tr className={`border-b ${isDark ? 'border-blue-800/20' : 'border-gray-200'}`}>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>场景名称</th>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>场景类型</th>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>适用任务</th>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>版本</th>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>更新时间</th>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>状态</th>
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>操作</th>
</tr>
</thead>
<tbody>
{scenes.map((scene) => (
<tr key={scene.id} className={`border-b ${isDark ? 'border-blue-800/20' : 'border-gray-200'} hover:bg-slate-700/20 transition-colors`}>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
<Layers className="w-5 h-5 text-blue-400" />
</div>
<span className={`${isDark ? 'text-white' : 'text-gray-900'} font-medium`}>{scene.sceneName}</span>
</div>
</td>
<td className="px-6 py-4">
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.sceneType}</span>
</td>
<td className="px-6 py-4">
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.sceneIntro}</span>
</td>
<td className="px-6 py-4">
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.version}</span>
</td>
<td className="px-6 py-4">
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.updateTime}</span>
</td>
<td className="px-6 py-4">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${scene.status === 0
? 'bg-green-500/20 text-green-400'
: scene.status === 1
? 'bg-yellow-500/20 text-yellow-400'
: `${isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-500/20 text-gray-500'}`
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${scene.status === 0
? 'bg-green-400'
: scene.status === 1
? 'bg-yellow-400'
: isDark ? 'bg-slate-400' : 'bg-gray-400'
}`}
/>
{sceneStatus[scene.status]}
</span>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button
onClick={() => {
navigate(`/scene-detail/${scene.id}`);
}}
className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors group"
title="查看"
>
<Eye className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:${isDark ? 'text-white' : 'text-gray-900'}`} />
</button>
{/* <button className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors group" title="编辑" onClick={() => editShowChange(scene)}>
<Edit className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:${isDark ? 'text-white' : 'text-gray-900'}`} />
</button> */}
{/* <button className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors group" title="复制">
<Copy className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:${isDark ? 'text-white' : 'text-gray-900'}`} />
</button> */}
{/* <button
onClick={() => onNavigate('task-scene')}
className="p-2 hover:bg-blue-500/20 rounded-lg transition-colors group"
title="启动"
>
<Play className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:text-blue-400`} />
</button> */}
{/* <Popconfirm
content="确认删除吗"
onConfirm={() => handleConfirm(scene)}
onCancel={handleCancel}
>
<button className="p-2 hover:bg-red-500/20 rounded-lg transition-colors group" title="删除">
<Trash2 className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:text-red-400`} />
</button>
</Popconfirm> */}
</div>
</td>
</tr>
))}
</tbody>
</table>
</Loading>
</div>
{/* 分页 */}
<div className="p-4 text-center">
<Pagination
total={total}
current={pageNum}
pageSize={pageSize}
onCurrentChange={(page) => pageIndexChange(Number(page))}
onPageSizeChange={(size) => pageSizeChange(Number(size))}
/>
</div>
{/* 新增对话框 */}
<Dialog
header="新增场景"
visible={visible}
onConfirm={addScene}
onCancel={() => setVisible(false)}
onClose={() => setVisible(false)}
closeOnOverlayClick={true}
>
<div className="mb-4">
<Input
label="场景名称"
value={formData.sceneName}
onChange={(val) => handleDataChange('sceneName', val)}
placeholder="请输入场景名称"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="场景类型"
value={formData.sceneType}
onChange={(val) => handleDataChange('sceneType', val)}
placeholder="请输入场景类型"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="适用任务"
value={formData.applicableTask}
onChange={(val) => handleDataChange('applicableTask', val)}
placeholder="请输入适用任务"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="场景代码"
value={formData.sceneCode}
onChange={(val) => handleDataChange('sceneCode', val)}
placeholder="请输入场景代码"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="版本"
value={formData.version}
onChange={(val) => handleDataChange('version', val)}
placeholder="请输入版本"
className="w-full"
/>
</div>
<div>
<Textarea
value={formData.sceneIntro}
onChange={(val) => handleDataChange('sceneIntro', val)}
placeholder="请输入场景简介"
className="w-full"
/>
</div>
</Dialog>
{/* 编辑对话框 */}
<Dialog
header="编辑场景"
visible={editDataShow}
onConfirm={updateScene}
confirmBtn={{
content: '确定',
theme: 'primary',
loading: editLoading,
}}
onCancel={() => setEditDataShow(false)}
onClose={() => setEditDataShow(false)}
closeOnOverlayClick={true}
>
<div className="mb-4">
<Input
label="场景名称"
value={formData.sceneName}
onChange={(val) => handleDataChange('sceneName', val)}
placeholder="请输入场景名称"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="场景类型"
value={formData.sceneType}
onChange={(val) => handleDataChange('sceneType', val)}
placeholder="请输入场景类型"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="适用任务"
value={formData.applicableTask}
onChange={(val) => handleDataChange('applicableTask', val)}
placeholder="请输入适用任务"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="场景代码"
value={formData.sceneCode}
onChange={(val) => handleDataChange('sceneCode', val)}
placeholder="请输入场景代码"
className="w-full"
/>
</div>
<div className="mb-4">
<Input
label="版本"
value={formData.version}
onChange={(val) => handleDataChange('version', val)}
placeholder="请输入版本"
className="w-full"
/>
</div>
<div className="mb-4">
<Textarea
value={formData.sceneIntro}
onChange={(val) => handleDataChange('sceneIntro', val)}
placeholder="请输入场景简介"
className="w-full"
/>
</div>
{/* <div>
<Textarea
value={formData.taskJson}
onChange={(val) => handleDataChange('taskJson', val)}
placeholder="请输入场景JSON"
className="w-full"
/>
</div> */}
</Dialog>
</div>
);
};
export default SceneList;
+347
View File
@@ -0,0 +1,347 @@
import React, { useState } from 'react';
import {
FileText,
Clock,
CheckCircle,
AlertTriangle,
ChevronDown,
ChevronRight,
Target,
Users,
TrendingUp,
MessageSquare,
Megaphone,
Shield,
ArrowRight,
Download,
RefreshCw,
Eye,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
interface StrategyResultProps {
onNavigate: (page: PageType) => void;
}
const StrategyResult: React.FC<StrategyResultProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 主题颜色类
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-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 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 btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
const [expandedSection, setExpandedSection] = useState<string | null>('goal');
const strategyChain = [
{ id: 1, name: '目标', icon: Target, color: 'blue', status: 'completed' },
{ id: 2, name: '受众', icon: Users, color: 'cyan', status: 'completed' },
{ id: 3, name: '行为', icon: TrendingUp, color: 'green', status: 'completed' },
{ id: 4, name: '驱动', icon: MessageSquare, color: 'yellow', status: 'completed' },
{ id: 5, name: '叙事', icon: Megaphone, color: 'purple', status: 'completed' },
{ id: 6, name: '风险', icon: Shield, color: 'red', status: 'completed' },
];
const 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: ['初始版本生成'] },
];
const toggleSection = (section: string) => {
setExpandedSection(expandedSection === section ? null : section);
};
return (
<div className="space-y-6">
{/* 顶部信息栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500/20 to-cyan-500/20 flex items-center justify-center">
<FileText className="w-6 h-6 text-blue-400" />
</div>
<div>
<h2 className={`${textPrimary} font-bold text-lg`}>五四青年节宣传活动策略</h2>
<p className={`${textSecondary} text-sm`}>策略讨论结果 · 已收敛</p>
</div>
</div>
<div className="h-12 w-px bg-blue-800/30" />
<div className="grid grid-cols-4 gap-6 text-sm">
<div>
<p className={`${textMuted}`}>使用场景</p>
<p className={`${textPrimary} font-medium`}>宣传策略生成</p>
</div>
<div>
<p className={`${textMuted}`}>运行时长</p>
<p className={`${textPrimary} font-medium`}>5小时32分</p>
</div>
<div>
<p className={`${textMuted}`}>当前版本</p>
<p className="text-cyan-400 font-medium">V3</p>
</div>
<div>
<p className={`${textMuted}`}>完成度</p>
<p className="text-green-400 font-medium flex items-center gap-1">
<CheckCircle className="w-4 h-4" /> 100%
</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => onNavigate('task-scene')}
className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}
>
<Eye className="w-4 h-4" />
返回工作台
</button>
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
<FileText className="w-4 h-4" />
生成最终方案
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-6">
{/* 左侧 - 策略链和详情 (70%) */}
<div className="col-span-2 space-y-6">
{/* 策略链可视化 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>策略链结果</h3>
<div className="flex items-center justify-between">
{strategyChain.map((item, index) => {
const Icon = item.icon;
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 < strategyChain.length - 1 && (
<ArrowRight className="w-6 h-6 text-slate-600 mb-6" />
)}
</React.Fragment>
);
})}
</div>
</div>
{/* 详细结果 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl overflow-hidden`}>
{[
{
id: 'goal',
title: '目标',
icon: Target,
color: 'blue',
content: {
primary: '提升品牌在Z世代用户中的认知度',
secondary: '建立品牌年轻化形象,增强用户情感连接',
},
},
{
id: 'audience',
title: '受众',
icon: Users,
color: 'cyan',
content: {
primary: '核心受众:18-25岁基层青年',
secondary: '次级受众:25-30岁职场新人',
insights: ['社交媒体重度用户', '注重性价比', '偏好真实、有态度的品牌'],
},
},
{
id: 'behavior',
title: '行为',
icon: TrendingUp,
color: 'green',
content: {
primary: '主动参与话题讨论、生成UGC内容',
conversion: '浏览→点赞→评论→转发→创作',
},
},
{
id: 'driver',
title: '驱动',
icon: MessageSquare,
color: 'yellow',
content: {
primary: '社交认同与自我表达',
secondary: ['归属感需求', '表达欲驱动', '群体认同激励'],
},
},
{
id: 'narrative',
title: '叙事',
icon: Megaphone,
color: 'purple',
content: {
primary: '真实、有态度的青年故事',
approach: '人物+事件结合,以真实故事传递品牌价值观',
},
},
{
id: 'risk',
title: '风险',
icon: Shield,
color: 'red',
content: {
risks: ['预算有限,需控制成本', '青年群体注意力分散', 'UGC质量难以保证'],
suggestions: ['提前布局预热', '设置激励机制', '建立内容审核机制'],
},
},
].map((section) => {
const Icon = section.icon;
const isExpanded = expandedSection === section.id;
return (
<div key={section.id} className={`border-b ${cardBorder} last:border-0`}>
<button
onClick={() => toggleSection(section.id)}
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.title}</p>
<p className={`text-${section.color}-400 text-sm`}>{section.content.primary}</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 ${inputBg} rounded-lg space-y-3`}>
{Object.entries(section.content).map(([key, value]) => (
<div key={key}>
<p className={`${textMuted} text-xs uppercase mb-1`}>{key}</p>
{Array.isArray(value) ? (
<ul className={`list-disc list-inside ${textSecondary} text-sm space-y-1`}>
{value.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
) : (
<p className={`${textPrimary} text-sm`}>{value}</p>
)}
</div>
))}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
{/* 右侧 - 摘要和版本记录 (30%) */}
<div className="space-y-6">
{/* 关键结论摘要 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>关键结论摘要</h3>
<div className="space-y-3">
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<p className="text-blue-400 text-xs font-medium mb-1">为什么选择这类受众</p>
<p className={`${textPrimary} text-sm`}>基层青年是品牌年轻化战略的核心突破口,具有强社交传播属性</p>
</div>
<div className="p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-green-400 text-xs font-medium mb-1">为什么设计这种行为目标</p>
<p className={`${textPrimary} text-sm`}>UGC是最高效的信任传递方式,用户生成内容具有真实性和可信度</p>
</div>
<div className="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
<p className="text-purple-400 text-xs font-medium mb-1">为什么采用这种叙事</p>
<p className={`${textPrimary} text-sm`}>真实故事比硬广更能打动年轻人,人物视角增强代入感</p>
</div>
</div>
</div>
{/* 版本对比记录 */}
<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`}>版本迭代记录</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">
{versionHistory.map((version, index) => (
<div
key={version.version}
className={`p-3 rounded-lg border ${
index === 0
? 'bg-cyan-500/10 border-cyan-500/30'
: `${inputBg} border-slate-700/30`
}`}
>
<div className="flex items-center justify-between mb-2">
<span className={`font-bold ${
index === 0 ? 'text-cyan-400' : 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-xs flex items-start gap-1`}>
<span className={textSecondary}>•</span>
{change}
</li>
))}
</ul>
</div>
))}
</div>
</div>
{/* 导出选项 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
<h3 className={`${textPrimary} font-semibold mb-4`}>导出选项</h3>
<div className="space-y-2">
<button className={`w-full flex items-center justify-between p-3 ${inputBg} hover:bg-slate-800/50 ${inputBorder} rounded-lg transition-colors`}>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-blue-400" />
<span className={`${textPrimary} text-sm`}>导出讨论记录</span>
</div>
<Download className={`w-4 h-4 ${textSecondary}`} />
</button>
<button className={`w-full flex items-center justify-between p-3 ${inputBg} hover:bg-slate-800/50 ${inputBorder} rounded-lg transition-colors`}>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-green-400" />
<span className={`${textPrimary} text-sm`}>导出策略结果</span>
</div>
<Download className={`w-4 h-4 ${textSecondary}`} />
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default StrategyResult;
+558
View File
@@ -0,0 +1,558 @@
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
import {
Settings,
User,
Bell,
Shield,
Database,
Palette,
Key,
Globe,
Monitor,
Smartphone,
Mail,
MessageSquare,
Clock,
Save,
RefreshCw,
CheckCircle,
AlertTriangle,
ChevronRight,
Building,
Users,
Bot,
Workflow,
FileText,
Zap,
} from 'lucide-react';
interface SystemSettingsProps {
onNavigate: (page: PageType) => void;
}
const SystemSettings: React.FC<SystemSettingsProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 主题颜色类
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-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 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 btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
const [activeTab, setActiveTab] = useState('general');
const [settings, setSettings] = useState({
// 通用设置
systemName: '智能策略系统',
systemVersion: 'V1.0.0',
language: 'zh-CN',
timezone: 'Asia/Shanghai',
// 通知设置
emailNotification: true,
inAppNotification: true,
smsNotification: false,
// 安全设置
sessionTimeout: 30,
passwordExpiration: 90,
twoFactorAuth: false,
// 运行规则
maxDiscussionRounds: 10,
maxRollbackTimes: 3,
enableStateGate: true,
enableLogicChainCheck: true,
enableRiskReview: true,
allowPartialParallel: true,
});
const tabs = [
{ id: 'general', label: '通用设置', icon: Settings },
{ id: 'user', label: '用户管理', icon: Users },
{ id: 'agent', label: '智能体管理', icon: Bot },
{ id: 'notification', label: '通知设置', icon: Bell },
{ id: 'security', label: '安全设置', icon: Shield },
{ id: 'workflow', label: '运行规则', icon: Workflow },
{ id: 'template', label: '模板管理', icon: FileText },
];
return (
<div className="space-y-6">
{/* 顶部标题栏 */}
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
<Settings className="w-5 h-5 text-blue-400" />
</div>
<div>
<h1 className={`${textPrimary} font-bold text-lg`}>系统设置</h1>
<p className={`${textSecondary} text-sm`}>管理智能策略系统的各项配置</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1.5 bg-green-500/20 text-green-400 rounded-full text-sm font-medium flex items-center gap-1.5">
<span className="w-2 h-2 bg-green-400 rounded-full" />
系统运行正常
</span>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-6">
{/* 左侧导航 (20%) */}
<div className="col-span-3">
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 sticky top-6`}>
<h3 className={`${textSecondary} text-xs font-medium mb-3 uppercase tracking-wider`}>设置分类</h3>
<nav className="space-y-1">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm transition-colors ${
activeTab === tab.id
? 'bg-blue-600/20 text-blue-400 border border-blue-500/30'
: `${textSecondary} hover:bg-slate-700/50 hover:${textPrimary}`
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</nav>
{/* 系统信息 */}
<div className="mt-6 pt-4 border-t border-blue-800/30">
<h3 className={`${textSecondary} text-xs font-medium mb-3 uppercase tracking-wider`}>系统信息</h3>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className={textMuted}>版本号</span>
<span className="text-cyan-400">{settings.systemVersion}</span>
</div>
<div className="flex justify-between">
<span className={textMuted}>运行环境</span>
<span className={textSecondary}>生产环境</span>
</div>
<div className="flex justify-between">
<span className={textMuted}>最后更新</span>
<span className={textSecondary}>2026-03-19</span>
</div>
</div>
</div>
</div>
</div>
{/* 右侧设置区 (80%) */}
<div className="col-span-9">
{/* 通用设置 */}
{activeTab === 'general' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
<Settings className="w-5 h-5 text-blue-400" />
通用设置
</h2>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-6">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>系统名称</label>
<input
type="text"
value={settings.systemName}
onChange={(e) => setSettings({ ...settings, systemName: e.target.value })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>系统版本</label>
<input
type="text"
value={settings.systemVersion}
disabled
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textMuted} text-sm`}
/>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>语言</label>
<select
value={settings.language}
onChange={(e) => setSettings({ ...settings, language: e.target.value })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
>
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>时区</label>
<select
value={settings.timezone}
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
>
<option value="Asia/Shanghai">北京时间 (UTC+8)</option>
<option value="UTC">世界时间 (UTC)</option>
</select>
</div>
</div>
<div className="pt-4 border-t border-blue-800/30">
<div className="flex items-center justify-between p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<div className="flex items-center gap-3">
<Zap className="w-5 h-5 text-blue-400" />
<div>
<p className={`${textPrimary} text-sm font-medium`}>自动保存设置</p>
<p className={`${textSecondary} text-xs`}>开启后,系统会自动保存您的设置更改</p>
</div>
</div>
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors`}>
已开启
</button>
</div>
</div>
</div>
</div>
)}
{/* 用户管理 */}
{activeTab === 'user' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<div className="flex items-center justify-between mb-6">
<h2 className={`${textPrimary} font-semibold text-lg flex items-center gap-2`}>
<Users className="w-5 h-5 text-cyan-400" />
用户管理
</h2>
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
<User className="w-4 h-4" />
添加用户
</button>
</div>
<div className="space-y-3">
{[
{ name: '张策略', role: '管理员', email: 'zhang@company.com', status: 'active' },
{ name: '李智能', role: '策略分析师', email: 'li@company.com', status: 'active' },
{ name: '王智能体', role: '智能体管理员', email: 'wang@company.com', status: 'active' },
{ name: '赵运维', role: '运维工程师', email: 'zhao@company.com', status: 'inactive' },
].map((user, i) => (
<div key={i} className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-blue-500/20 flex items-center justify-center">
<User className="w-5 h-5 text-blue-400" />
</div>
<div>
<p className={`${textPrimary} text-sm font-medium`}>{user.name}</p>
<p className={`${textSecondary} text-xs`}>{user.email}</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-xs ${
user.role === '管理员' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
}`}>
{user.role}
</span>
<span className={`px-3 py-1 rounded-full text-xs ${
user.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
}`}>
{user.status === 'active' ? '在线' : '离线'}
</span>
<button className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
<ChevronRight className={`w-4 h-4 ${textSecondary}`} />
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* 智能体管理 */}
{activeTab === 'agent' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<div className="flex items-center justify-between mb-6">
<h2 className={`${textPrimary} font-semibold text-lg flex items-center gap-2`}>
<Bot className="w-5 h-5 text-purple-400" />
智能体管理
</h2>
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
<Bot className="w-4 h-4" />
添加智能体
</button>
</div>
<div className="grid grid-cols-2 gap-4">
{[
{ name: 'GoalAgent', type: '目标分析', status: 'running', tasks: 156 },
{ name: 'AudienceAgent', type: '受众分析', status: 'running', tasks: 142 },
{ name: 'BehaviorAgent', type: '行为分析', status: 'idle', tasks: 98 },
{ name: 'DriverAgent', type: '驱动分析', status: 'idle', tasks: 87 },
{ name: 'NarrativeAgent', type: '叙事分析', status: 'idle', tasks: 76 },
{ name: 'LogicChainAgent', type: '逻辑链收敛', status: 'idle', tasks: 65 },
].map((agent, i) => (
<div key={i} className={`p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Bot className="w-4 h-4 text-purple-400" />
<span className={`${textPrimary} text-sm font-medium`}>{agent.name}</span>
</div>
<span className={`px-2 py-0.5 rounded text-xs ${
agent.status === 'running' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
}`}>
{agent.status === 'running' ? '运行中' : '空闲'}
</span>
</div>
<p className={`${textSecondary} text-xs mb-3`}>{agent.type}</p>
<div className="flex items-center justify-between text-xs">
<span className={textMuted}>已完成任务</span>
<span className="text-cyan-400">{agent.tasks}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* 通知设置 */}
{activeTab === 'notification' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
<Bell className="w-5 h-5 text-amber-400" />
通知设置
</h2>
<div className="space-y-4">
{[
{ id: 'email', label: '邮件通知', description: '接收任务完成、风险提醒等邮件通知', icon: Mail, enabled: settings.emailNotification },
{ id: 'inApp', label: '站内通知', description: '在系统内显示通知消息', icon: MessageSquare, enabled: settings.inAppNotification },
{ id: 'sms', label: '短信通知', description: '发送重要事项的短信提醒', icon: Smartphone, enabled: settings.smsNotification },
].map((item) => (
<div key={item.id} className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
<item.icon className="w-5 h-5 text-blue-400" />
</div>
<div>
<p className={`${textPrimary} text-sm font-medium`}>{item.label}</p>
<p className={`${textSecondary} text-xs`}>{item.description}</p>
</div>
</div>
<button
onClick={() => setSettings({ ...settings, [item.id === 'email' ? 'emailNotification' : item.id === 'inApp' ? 'inAppNotification' : 'smsNotification']: !item.enabled })}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
item.enabled
? 'bg-green-600 hover:bg-green-500 text-white'
: btnSecondary
}`}
>
{item.enabled ? '已开启' : '已关闭'}
</button>
</div>
))}
<div className="pt-4 border-t border-blue-800/30">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>通知时段</label>
<div className="flex items-center gap-4">
<select className={`px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}>
<option>09:00</option>
<option>10:00</option>
<option>11:00</option>
</select>
<span className={textMuted}>至</span>
<select className={`px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}>
<option>18:00</option>
<option>19:00</option>
<option>20:00</option>
</select>
</div>
</div>
</div>
</div>
</div>
)}
{/* 安全设置 */}
{activeTab === 'security' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
<Shield className="w-5 h-5 text-red-400" />
安全设置
</h2>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-6">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>会话超时时间(分钟)</label>
<select
value={settings.sessionTimeout}
onChange={(e) => setSettings({ ...settings, sessionTimeout: Number(e.target.value) })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
>
<option value={15}>15 分钟</option>
<option value={30}>30 分钟</option>
<option value={60}>1 小时</option>
<option value={120}>2 小时</option>
</select>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>密码过期时间(天)</label>
<select
value={settings.passwordExpiration}
onChange={(e) => setSettings({ ...settings, passwordExpiration: Number(e.target.value) })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
>
<option value={30}>30 天</option>
<option value={60}>60 天</option>
<option value={90}>90 天</option>
<option value={180}>180 天</option>
</select>
</div>
</div>
<div className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<div className="flex items-center gap-4">
<Key className="w-5 h-5 text-purple-400" />
<div>
<p className={`${textPrimary} text-sm font-medium`}>双因素认证</p>
<p className={`${textSecondary} text-xs`}>登录时需要额外的验证码验证</p>
</div>
</div>
<button
onClick={() => setSettings({ ...settings, twoFactorAuth: !settings.twoFactorAuth })}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
settings.twoFactorAuth
? 'bg-green-600 hover:bg-green-500 text-white'
: btnSecondary
}`}
>
{settings.twoFactorAuth ? '已开启' : '已关闭'}
</button>
</div>
<div className="pt-4 border-t border-blue-800/30">
<button className="flex items-center gap-2 px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 border border-red-500/30 text-red-400 rounded-lg text-sm transition-colors">
<Shield className="w-4 h-4" />
重置所有安全设置
</button>
</div>
</div>
</div>
)}
{/* 运行规则 */}
{activeTab === 'workflow' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
<Workflow className="w-5 h-5 text-green-400" />
运行规则配置
</h2>
<div className="space-y-4">
{[
{ id: 'enableStateGate', label: '启用状态门控', description: '智能体输出需要通过状态门控检查才能进入下一节点' },
{ id: 'enableLogicChainCheck', label: '启用逻辑链校验', description: '在逻辑链收敛阶段进行一致性检查' },
{ id: 'enableRiskReview', label: '启用风险强制复核', description: '高风险项必须经过人工确认才能继续' },
{ id: 'allowPartialParallel', label: '允许局部并行', description: '独立的流程节点可以并行执行' },
].map((item) => (
<div key={item.id} className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
<div>
<p className={`${textPrimary} text-sm font-medium`}>{item.label}</p>
<p className={`${textSecondary} text-xs mt-1`}>{item.description}</p>
</div>
<button
onClick={() => setSettings({ ...settings, [item.id]: !settings[item.id as keyof typeof settings] as boolean })}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
settings[item.id as keyof typeof settings]
? 'bg-green-600 hover:bg-green-500 text-white'
: btnSecondary
}`}
>
{settings[item.id as keyof typeof settings] ? '已开启' : '已关闭'}
</button>
</div>
))}
<div className="grid grid-cols-2 gap-6 pt-4 border-t border-blue-800/30">
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>最大讨论轮数</label>
<input
type="number"
value={settings.maxDiscussionRounds}
onChange={(e) => setSettings({ ...settings, maxDiscussionRounds: Number(e.target.value) })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
/>
<p className={`${textMuted} text-xs mt-1`}>超出此轮数将自动收敛</p>
</div>
<div>
<label className={`block ${textSecondary} text-sm mb-2`}>最大回退次数</label>
<input
type="number"
value={settings.maxRollbackTimes}
onChange={(e) => setSettings({ ...settings, maxRollbackTimes: Number(e.target.value) })}
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
/>
<p className={`${textMuted} text-xs mt-1`}>超出此次数将终止任务</p>
</div>
</div>
</div>
</div>
)}
{/* 模板管理 */}
{activeTab === 'template' && (
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
<div className="flex items-center justify-between mb-6">
<h2 className={`${textPrimary} font-semibold text-lg flex items-center gap-2`}>
<FileText className="w-5 h-5 text-cyan-400" />
模板管理
</h2>
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
<FileText className="w-4 h-4" />
新建模板
</button>
</div>
<div className="grid grid-cols-3 gap-4">
{[
{ name: '宣传策略模板', type: '策略生成', fields: 24, updated: '2026-03-15' },
{ name: '活动方案模板', type: '方案推演', fields: 32, updated: '2026-03-10' },
{ name: '市场分析模板', type: '市场调研', fields: 18, updated: '2026-03-08' },
{ name: '产品推广模板', type: '产品营销', fields: 28, updated: '2026-03-05' },
{ name: '品牌公关模板', type: '公关传播', fields: 22, updated: '2026-03-01' },
{ name: '用户调研模板', type: '用户洞察', fields: 16, updated: '2026-02-28' },
].map((template, i) => (
<div key={i} className={`p-4 ${inputBg} ${inputBorder} rounded-lg hover:border-blue-500/50 transition-colors cursor-pointer`}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-cyan-400" />
<span className={`${textPrimary} text-sm font-medium`}>{template.name}</span>
</div>
<ChevronRight className={`w-4 h-4 ${textSecondary}`} />
</div>
<p className={`${textSecondary} text-xs mb-2`}>{template.type}</p>
<div className="flex items-center justify-between text-xs">
<span className={textMuted}>{template.fields} 个字段</span>
<span className={textMuted}>更新于 {template.updated}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* 保存按钮 */}
<div className="mt-6 flex items-center justify-end gap-3">
<button className={`px-6 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
<RefreshCw className="w-4 h-4" />
重置
</button>
<button className={`px-6 py-2.5 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
<Save className="w-4 h-4" />
保存设置
</button>
</div>
</div>
</div>
</div>
);
};
export default SystemSettings;
+166
View File
@@ -0,0 +1,166 @@
import { Outlet } from 'react-router-dom';
export interface SceneInfo {
applicableTask: string;
createTime: string;
deleted: number;
id: number;
relatedAgent: string;
sceneCode: string;
sceneIntro: string;
sceneName: string;
sceneType: string;
status: number;
updateTime: string;
version: string;
}
export interface TaskListItem {
createBy: number;
createTime: string;
deptId: null;
id: number;
isTop: number;
issuingDept: string;
keywords: null;
overview: string;
planTaskTop: number;
platform: null;
ptaskId: null;
recommendedBasis: null;
scene: string; // 或 SceneInfo,如果你已经解析
sourceType: number;
step: null;
taskContent: string;
taskDirection: string;
taskEndDate: string;
taskLevel: number;
taskName: string;
taskNo: string;
taskStartDate: string;
taskStatus: number;
taskSuperiorFlag: number;
taskTimeLimit: null;
taskType: number;
column: string;
updateBy: number;
updateTime: string;
endCount: number;
sumAll: number;
progress: number;
taskStatusData: any;
}
export interface ActionListItem {
actionDay: number;
actionEndDate: string;
actionManager: number;
actionName: string;
actionNo: any;
actionStartDate: string;
actionStatus: number;
actionTerm: number;
actionTitle: any;
actorName: any;
audienceGroup: any
audienceGroupId: any;
audienceRecReason: any
behavior: any
createBy: number
createTime: string;
deleteFlag: number;
deptId: any;
deptName: string;
drivingFactor: any;
expectedBehaviorId: any;
id: number
keywords: string
level: number
scene: string
sourceType: number
step: null
taskId: number
updateBy: number
updateTime: number
useActionId: null
}
export interface actionColumnItem {
icon: string;
columnName: string;
actionList: ActionListItem[]
}
export const taskStatusList = {
'0': {
label: '待签收',
color: '#0062d9',
icon: '待签收.svg'
},
'10': {
label: '待分析',
color: '#2f9acc',
icon: '待CEH.svg'
},
'20': {
label: '分析中',
color: '#d97b00',
icon: '进行中.svg'
},
'25': {
label: '分析完成已提交',
color: '#1eb07f',
icon: '完成.svg'
},
'30': {
label: '已分析待审核',
color: '#1eb07f',
icon: '完成.svg'
},
'40': {
label: '分析审核通过',
color: '#1eb07f',
icon: '完成.svg'
},
'50': {
label: '分析审核未通过',
color: '#cf1322',
icon: '未通过.svg'
},
'60': {
label: '行动已下发待签收',
color: '#1eb07f',
icon: '完成.svg'
},
'70': {
label: '行动已签收待策划',
color: '#1eb07f',
icon: '完成.svg'
},
'80': {
label: '行动策划已提交',
color: '#1eb07f',
icon: '完成.svg'
},
'100': {
label: '作战方案待审核',
color: '#1eb07f',
icon: '完成.svg'
},
'110': {
label: '作战方案审核通过',
color: '#1eb07f',
icon: '完成.svg'
},
'120': {
label: '作战方案未通过',
color: '#cf1322',
icon: '未通过.svg'
}
};
export default function TaskManagement() {
return (
<div className="h-full flex flex-col">
<Outlet />
</div>
);
}
+546
View File
@@ -0,0 +1,546 @@
import React, { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
FileBox,
Search,
Plus,
Edit,
Trash2,
Download,
Upload,
Filter,
Grid3X3,
List,
MoreVertical,
ChevronRight,
FileText,
Image,
Film,
Music,
Archive,
ExternalLink,
Copy,
Share2,
} from 'lucide-react';
import { PageType } from '../components/Sidebar';
interface ZZResource {
id: string;
name: string;
type: 'document' | 'image' | 'video' | 'audio' | 'archive' | 'other';
size: string;
category: string;
tags: string[];
createdAt: string;
updatedAt: string;
author: string;
status: 'active' | 'archived' | 'draft';
description: string;
}
interface ZZResourceManagementProps {
onNavigate: (page: PageType) => void;
}
const mockResources: ZZResource[] = [
{
id: 'zz-001',
name: '品牌视觉规范手册',
type: 'document',
size: '15.2 MB',
category: '品牌资产',
tags: ['品牌', 'VI', '规范'],
createdAt: '2024-01-15',
updatedAt: '2024-03-10',
author: '张三',
status: 'active',
description: '公司品牌视觉识别系统标准手册,包含LOGO、色彩、字体等规范'
},
{
id: 'zz-002',
name: '产品宣传视频素材',
type: 'video',
size: '256 MB',
category: '营销素材',
tags: ['视频', '宣传', '产品'],
createdAt: '2024-02-20',
updatedAt: '2024-03-08',
author: '李四',
status: 'active',
description: '2024年第一季度产品宣传视频原始素材'
},
{
id: 'zz-003',
name: '活动背景音乐库',
type: 'audio',
size: '1.2 GB',
category: '音频素材',
tags: ['音乐', '背景', '活动'],
createdAt: '2024-01-08',
updatedAt: '2024-02-28',
author: '王五',
status: 'active',
description: '各类活动背景音乐合集,包含不同风格和时长'
},
{
id: 'zz-004',
name: '图标矢量素材包',
type: 'image',
size: '45 MB',
category: '设计素材',
tags: ['图标', '矢量', 'UI'],
createdAt: '2024-03-01',
updatedAt: '2024-03-15',
author: '赵六',
status: 'active',
description: '500+高质量矢量图标素材,适用于各类项目'
},
{
id: 'zz-005',
name: '历史项目归档',
type: 'archive',
size: '2.5 GB',
category: '归档文件',
tags: ['归档', '历史', '项目'],
createdAt: '2023-12-01',
updatedAt: '2024-01-20',
author: '系统',
status: 'archived',
description: '2023年度已完成项目文件归档'
},
{
id: 'zz-006',
name: 'PPT模板套装',
type: 'document',
size: '88 MB',
category: '模板素材',
tags: ['PPT', '模板', '演示'],
createdAt: '2024-02-10',
updatedAt: '2024-03-12',
author: '钱七',
status: 'draft',
description: '商务演示PPT模板,包含多种风格和场景'
},
];
const typeIcons: Record<ZZResource['type'], React.ReactNode> = {
document: <FileText className="w-5 h-5" />,
image: <Image className="w-5 h-5" />,
video: <Film className="w-5 h-5" />,
audio: <Music className="w-5 h-5" />,
archive: <Archive className="w-5 h-5" />,
other: <FileBox className="w-5 h-5" />,
};
const typeColors: Record<ZZResource['type'], string> = {
document: 'bg-blue-500/20 text-blue-400',
image: 'bg-green-500/20 text-green-400',
video: 'bg-purple-500/20 text-purple-400',
audio: 'bg-orange-500/20 text-orange-400',
archive: 'bg-gray-500/20 text-gray-400',
other: 'bg-slate-500/20 text-slate-400',
};
const statusColors: Record<ZZResource['status'], { bg: string; text: string; label: string }> = {
active: { bg: 'bg-green-500/20', text: 'text-green-400', label: '使用中' },
archived: { bg: 'bg-gray-500/20', text: 'text-gray-400', label: '已归档' },
draft: { bg: 'bg-yellow-500/20', text: 'text-yellow-400', label: '草稿' },
};
const categories = ['全部', '品牌资产', '营销素材', '音频素材', '设计素材', '模板素材', '归档文件'];
const ZZResourceManagement: React.FC<ZZResourceManagementProps> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('全部');
const [selectedStatus, setSelectedStatus] = useState<string>('all');
const [selectedResource, setSelectedResource] = useState<ZZResource | null>(null);
const [showUploadModal, setShowUploadModal] = useState(false);
const filteredResources = mockResources.filter(resource => {
const matchesSearch = resource.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
resource.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
resource.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCategory = selectedCategory === '全部' || resource.category === selectedCategory;
const matchesStatus = selectedStatus === 'all' || resource.status === selectedStatus;
return matchesSearch && matchesCategory && matchesStatus;
});
const stats = {
total: mockResources.length,
active: mockResources.filter(r => r.status === 'active').length,
archived: mockResources.filter(r => r.status === 'archived').length,
draft: mockResources.filter(r => r.status === 'draft').length,
};
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'}`}>
<FileBox className={`w-6 h-6 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
</div>
<div>
<h1 className={`text-2xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
ZZ资源管理
</h1>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
管理各类制作资源和素材文件
</p>
</div>
</div>
<button
onClick={() => setShowUploadModal(true)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
isDark
? 'bg-blue-600 hover:bg-blue-500 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
<Upload className="w-4 h-4" />
上传资源
</button>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-blue-800/30' : 'border-red-100'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>资源总数</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.total}</p>
</div>
<div className={`p-3 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-50'}`}>
<FileBox className={`w-6 h-6 ${isDark ? 'text-blue-400' : 'text-red-400'}`} />
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-green-800/30' : 'border-green-100'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>使用中</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.active}</p>
</div>
<div className={`p-3 rounded-lg bg-green-500/20`}>
<FileText className="w-6 h-6 text-green-400" />
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>已归档</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.archived}</p>
</div>
<div className={`p-3 rounded-lg bg-gray-500/20`}>
<Archive className="w-6 h-6 text-gray-400" />
</div>
</div>
</div>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-yellow-800/30' : 'border-yellow-100'}`}>
<div className="flex items-center justify-between">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>草稿</p>
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.draft}</p>
</div>
<div className={`p-3 rounded-lg bg-yellow-500/20`}>
<Edit className="w-6 h-6 text-yellow-400" />
</div>
</div>
</div>
</div>
{/* 筛选和搜索 */}
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-blue-800/30' : 'border-red-100'}`}>
<div className="flex items-center gap-4 flex-wrap">
{/* 搜索框 */}
<div className="flex-1 min-w-[200px] relative">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<input
type="text"
placeholder="搜索资源名称、描述或标签..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white placeholder-slate-500 focus:border-blue-500'
: 'bg-gray-50 border-gray-200 text-gray-900 placeholder-gray-400 focus:border-red-400'
}`}
/>
</div>
{/* 分类筛选 */}
<div className="flex items-center gap-2">
<Filter className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className={`px-3 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white'
: 'bg-gray-50 border-gray-200 text-gray-900'
}`}
>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
{/* 状态筛选 */}
<div className="flex items-center gap-2">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className={`px-3 py-2 rounded-lg border transition-all ${
isDark
? 'bg-slate-900/50 border-slate-700 text-white'
: 'bg-gray-50 border-gray-200 text-gray-900'
}`}
>
<option value="all">全部状态</option>
<option value="active">使用中</option>
<option value="archived">已归档</option>
<option value="draft">草稿</option>
</select>
</div>
{/* 视图切换 */}
<div className={`flex items-center rounded-lg p-1 ${isDark ? 'bg-slate-900/50' : 'bg-gray-100'}`}>
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-md transition-all ${
viewMode === 'grid'
? isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-white text-red-500 shadow-sm'
: isDark ? 'text-slate-400' : 'text-gray-500'
}`}
>
<Grid3X3 className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-md transition-all ${
viewMode === 'list'
? isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-white text-red-500 shadow-sm'
: isDark ? 'text-slate-400' : 'text-gray-500'
}`}
>
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* 资源列表 */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-3 gap-4">
{filteredResources.map(resource => (
<div
key={resource.id}
className={`p-4 rounded-xl cursor-pointer transition-all hover:scale-[1.02] ${
isDark ? 'bg-slate-800/50 hover:bg-slate-800' : 'bg-white hover:bg-gray-50'
} border ${isDark ? 'border-blue-800/30' : 'border-red-100'} hover:border-blue-500/50`}
onClick={() => setSelectedResource(resource)}
>
<div className="flex items-start justify-between mb-3">
<div className={`p-3 rounded-lg ${typeColors[resource.type]}`}>
{typeIcons[resource.type]}
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[resource.status].bg} ${statusColors[resource.status].text}`}>
{statusColors[resource.status].label}
</span>
</div>
<h3 className={`font-medium mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
{resource.name}
</h3>
<p className={`text-sm mb-3 line-clamp-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{resource.description}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{resource.tags.slice(0, 2).map(tag => (
<span
key={tag}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
}`}
>
{tag}
</span>
))}
</div>
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{resource.size}
</span>
</div>
</div>
))}
</div>
) : (
<div className={`rounded-xl overflow-hidden border ${isDark ? 'bg-slate-800/50 border-blue-800/30' : 'bg-white border-red-100'}`}>
<table className="w-full">
<thead className={isDark ? 'bg-slate-900/50' : 'bg-gray-50'}>
<tr>
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>资源名称</th>
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>类型</th>
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>分类</th>
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>大小</th>
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>状态</th>
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>更新日期</th>
<th className={`px-4 py-3 text-right text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>操作</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-slate-700">
{filteredResources.map(resource => (
<tr
key={resource.id}
className={`cursor-pointer transition-colors ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'}`}
onClick={() => setSelectedResource(resource)}
>
<td className={`px-4 py-3 ${isDark ? 'text-white' : 'text-gray-900'}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${typeColors[resource.type]}`}>
{typeIcons[resource.type]}
</div>
<div>
<p className="font-medium">{resource.name}</p>
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{resource.author}</p>
</div>
</div>
</td>
<td className={`px-4 py-3 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{resource.type === 'document' ? '文档' :
resource.type === 'image' ? '图片' :
resource.type === 'video' ? '视频' :
resource.type === 'audio' ? '音频' :
resource.type === 'archive' ? '归档' : '其他'}
</td>
<td className={`px-4 py-3 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{resource.category}</td>
<td className={`px-4 py-3 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{resource.size}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[resource.status].bg} ${statusColors[resource.status].text}`}>
{statusColors[resource.status].label}
</span>
</td>
<td className={`px-4 py-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{resource.updatedAt}</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-2" onClick={(e) => e.stopPropagation()}>
<button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<Download className="w-4 h-4" />
</button>
<button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<Edit className="w-4 h-4" />
</button>
<button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-red-500/20 text-red-400' : 'hover:bg-red-50 text-red-500'}`}>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* 资源详情弹窗 */}
{selectedResource && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={() => setSelectedResource(null)}>
<div
className={`w-full max-w-2xl rounded-2xl ${isDark ? 'bg-slate-800' : 'bg-white'} shadow-xl`}
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-lg ${typeColors[selectedResource.type]}`}>
{typeIcons[selectedResource.type]}
</div>
<div>
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{selectedResource.name}
</h2>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{selectedResource.category} · {selectedResource.size}
</p>
</div>
</div>
<button
onClick={() => setSelectedResource(null)}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
<div className="p-6 space-y-4">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>描述</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>创建者</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.author}</p>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>创建时间</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.createdAt}</p>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>更新时间</p>
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.updatedAt}</p>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>状态</p>
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[selectedResource.status].bg} ${statusColors[selectedResource.status].text}`}>
{statusColors[selectedResource.status].label}
</span>
</div>
</div>
<div>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>标签</p>
<div className="flex flex-wrap gap-2">
{selectedResource.tags.map(tag => (
<span
key={tag}
className={`px-3 py-1 rounded-full text-sm ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
}`}
>
{tag}
</span>
))}
</div>
</div>
</div>
<div className={`p-6 border-t ${isDark ? 'border-slate-700' : 'border-gray-200'} flex justify-end gap-3`}>
<button
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
isDark ? 'bg-slate-700 hover:bg-slate-600 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<Download className="w-4 h-4 inline mr-2" />
下载
</button>
<button
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
isDark ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
<Edit className="w-4 h-4 inline mr-2" />
编辑
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ZZResourceManagement;
@@ -0,0 +1,10 @@
.iframe-container {
width: 100%;
height: calc(100vh - 100px); /* 可根据需求调整 */
}
.full-size-iframe {
width: 100%;
height: 100%;
border: none;
}