Files
FilesReadSystem/frontend/src/pages/TaskHistory.tsx
KiriAky 107 858b594171 添加任务状态双写机制和历史记录功能
- 实现任务状态同时写入Redis和MongoDB的双写机制
- 添加MongoDB任务集合及CRUD操作接口
- 新增任务历史记录查询、列表展示和删除功能
- 重构任务状态更新逻辑,统一使用update_task_status函数
- 添加模板填服务中AI审核字段值的功能
- 优化前端任务历史页面显示和交互体验
2026-04-10 01:15:53 +08:00

302 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Clock,
CheckCircle2,
XCircle,
RefreshCcw,
Download,
FileText,
FileSpreadsheet,
Loader2,
ChevronDown,
ChevronUp,
Trash2,
AlertCircle,
HelpCircle
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { backendApi } from '@/db/backend-api';
import { format } from 'date-fns';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { Skeleton } from '@/components/ui/skeleton';
type Task = {
task_id: string;
status: 'pending' | 'processing' | 'success' | 'failure' | 'unknown';
created_at: string;
updated_at?: string;
message?: string;
result?: any;
error?: string;
task_type?: string;
};
const TaskHistory: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [expandedTask, setExpandedTask] = useState<string | null>(null);
// 获取任务历史数据
const fetchTasks = async () => {
try {
setLoading(true);
const response = await backendApi.getTasks(50, 0);
if (response.success && response.tasks) {
// 转换后端数据格式为前端格式
const convertedTasks: Task[] = response.tasks.map((t: any) => ({
task_id: t.task_id,
status: t.status || 'unknown',
created_at: t.created_at || new Date().toISOString(),
updated_at: t.updated_at,
message: t.message || '',
result: t.result,
error: t.error,
task_type: t.task_type || 'document_parse'
}));
setTasks(convertedTasks);
} else {
setTasks([]);
}
} catch (error) {
console.error('获取任务列表失败:', error);
toast.error('获取任务列表失败');
setTasks([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
const getStatusBadge = (status: string) => {
switch (status) {
case 'success':
return <Badge className="bg-emerald-500 text-white text-[10px]"><CheckCircle2 size={12} className="mr-1" /></Badge>;
case 'failure':
return <Badge className="bg-destructive text-white text-[10px]"><XCircle size={12} className="mr-1" /></Badge>;
case 'processing':
return <Badge className="bg-amber-500 text-white text-[10px]"><Loader2 size={12} className="mr-1 animate-spin" /></Badge>;
case 'unknown':
return <Badge className="bg-gray-500 text-white text-[10px]"><HelpCircle size={12} className="mr-1" /></Badge>;
default:
return <Badge className="bg-gray-500 text-white text-[10px]"><Clock size={12} className="mr-1" /></Badge>;
}
};
const getTaskTypeLabel = (type?: string) => {
switch (type) {
case 'document_parse':
return '文档解析';
case 'excel_analysis':
return 'Excel 分析';
case 'template_fill':
return '智能填表';
case 'rag_index':
return 'RAG 索引';
default:
return '未知任务';
}
};
const getTaskIcon = (type?: string) => {
switch (type) {
case 'document_parse':
case 'rag_index':
return <FileText size={20} />;
case 'excel_analysis':
return <FileSpreadsheet size={20} />;
default:
return <Clock size={20} />;
}
};
const handleRetry = async (taskId: string) => {
toast.info('任务重试功能开发中...');
};
const handleDelete = async (taskId: string) => {
try {
await backendApi.deleteTask(taskId);
setTasks(prev => prev.filter(t => t.task_id !== taskId));
toast.success('任务已删除');
} catch (error) {
console.error('删除任务失败:', error);
toast.error('删除任务失败');
}
};
const stats = {
total: tasks.length,
success: tasks.filter(t => t.status === 'success').length,
processing: tasks.filter(t => t.status === 'processing').length,
failure: tasks.filter(t => t.status === 'failure').length,
unknown: tasks.filter(t => t.status === 'unknown').length
};
return (
<div className="space-y-8 pb-10">
<section className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="space-y-1">
<h1 className="text-3xl font-extrabold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<Button variant="outline" className="rounded-xl gap-2" onClick={() => fetchTasks()}>
<RefreshCcw size={18} />
<span></span>
</Button>
</section>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ label: '总任务数', value: stats.total, icon: Clock, color: 'text-blue-500', bg: 'bg-blue-500/10' },
{ label: '成功', value: stats.success, icon: CheckCircle2, color: 'text-emerald-500', bg: 'bg-emerald-500/10' },
{ label: '处理中', value: stats.processing, icon: Loader2, color: 'text-amber-500', bg: 'bg-amber-500/10' },
{ label: '失败', value: stats.failure, icon: XCircle, color: 'text-destructive', bg: 'bg-destructive/10' }
].map((stat, i) => (
<Card key={i} className="border-none shadow-sm">
<CardContent className="p-4 flex items-center gap-4">
<div className={cn("w-12 h-12 rounded-xl flex items-center justify-center", stat.bg)}>
<stat.icon size={24} className={stat.color} />
</div>
<div>
<p className="text-2xl font-bold">{stat.value}</p>
<p className="text-xs text-muted-foreground">{stat.label}</p>
</div>
</CardContent>
</Card>
))}
</div>
{/* Task List */}
<div className="space-y-4">
{loading ? (
Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full rounded-2xl" />
))
) : tasks.length > 0 ? (
tasks.map((task) => (
<Card key={task.task_id} className="border-none shadow-sm overflow-hidden">
<div className="flex flex-col">
<div className="p-6 flex flex-col md:flex-row md:items-center gap-4">
<div className={cn(
"w-12 h-12 rounded-xl flex items-center justify-center shrink-0",
task.status === 'success' ? "bg-emerald-500/10 text-emerald-500" :
task.status === 'failure' ? "bg-destructive/10 text-destructive" :
task.status === 'processing' ? "bg-amber-500/10 text-amber-500" :
"bg-gray-500/10 text-gray-500"
)}>
{task.status === 'processing' ? (
<Loader2 size={24} className="animate-spin" />
) : (
getTaskIcon(task.task_type)
)}
</div>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center gap-3 flex-wrap">
<h3 className="font-bold">{getTaskTypeLabel(task.task_type)}</h3>
{getStatusBadge(task.status)}
<Badge variant="outline" className="text-[10px]">
{task.task_id}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{task.message || (task.status === 'unknown' ? '无法获取状态' : '任务执行中...')}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock size={12} />
{task.created_at ? format(new Date(task.created_at), 'yyyy-MM-dd HH:mm:ss') : '时间未知'}
</span>
{task.updated_at && task.status !== 'processing' && (
<span>
: {format(new Date(task.updated_at), 'HH:mm:ss')}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
{task.status === 'failure' && (
<Button
variant="outline"
size="sm"
className="rounded-xl gap-1 h-9"
onClick={() => handleRetry(task.task_id)}
>
<RefreshCcw size={14} />
<span></span>
</Button>
)}
<Button
variant="ghost"
size="icon"
className="rounded-lg text-destructive hover:bg-destructive/10"
onClick={() => handleDelete(task.task_id)}
>
<Trash2 size={18} />
</Button>
{task.result && (
<Button
variant="ghost"
size="sm"
className="rounded-xl gap-1 h-9"
onClick={() => setExpandedTask(expandedTask === task.task_id ? null : task.task_id)}
>
{expandedTask === task.task_id ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
<span></span>
</Button>
)}
</div>
</div>
{/* Expanded Details */}
{expandedTask === task.task_id && task.result && (
<div className="px-6 pb-6 pt-2 border-t border-dashed animate-in slide-in-from-top-2 duration-300">
<div className="p-4 bg-muted/30 rounded-xl">
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-3"></p>
<pre className="text-sm whitespace-pre-wrap font-mono">
{JSON.stringify(task.result, null, 2)}
</pre>
</div>
</div>
)}
{/* Error Details */}
{task.status === 'failure' && task.error && (
<div className="px-6 pb-6 pt-2 border-t border-dashed">
<div className="p-4 bg-destructive/5 rounded-xl border border-destructive/20">
<p className="text-xs font-bold uppercase tracking-widest text-destructive mb-3 flex items-center gap-2">
<AlertCircle size={14} />
</p>
<p className="text-sm text-destructive">{task.error}</p>
</div>
</div>
)}
</div>
</Card>
))
) : (
<div className="py-20 flex flex-col items-center justify-center text-center space-y-4">
<div className="w-24 h-24 rounded-full bg-muted flex items-center justify-center text-muted-foreground/30">
<Clock size={48} />
</div>
<div className="space-y-1">
<p className="text-xl font-bold"></p>
<p className="text-muted-foreground"></p>
</div>
</div>
)}
</div>
</div>
);
};
export default TaskHistory;