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

682 lines
24 KiB
TypeScript

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;