546 lines
20 KiB
TypeScript
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;
|