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

546 lines
20 KiB
TypeScript

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;