Summary: - Fix methodology score display issue in task list (show score instead of 'warn') - Add methodology_score field to database schema - Fix report display when only methodology agent is selected - Implement Word document export using docx library - Update documentation to v3.0/v3.1 Backend changes: - Add methodologyScore to Prisma schema and TaskSummary type - Update reviewWorker to save methodologyScore - Update getTaskList to return methodologyScore Frontend changes: - Install docx and file-saver libraries - Implement handleExportReport with Word generation - Fix activeTab auto-selection based on available data - Add proper imports for docx components Documentation: - Update RVW module status to 90% (Phase 1-5 complete) - Update system status document to v3.0 Tested: All review workflows verified, Word export functional
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
/**
|
||
* 任务表格组件
|
||
*/
|
||
import { FileText, FileType2, Loader2, Play, Eye, RefreshCw, Trash2 } from 'lucide-react';
|
||
import type { ReviewTask } from '../types';
|
||
import { formatFileSize, formatTime } from '../api';
|
||
|
||
interface TaskTableProps {
|
||
tasks: ReviewTask[];
|
||
selectedIds: string[];
|
||
onSelectChange: (ids: string[]) => void;
|
||
onViewReport: (task: ReviewTask) => void;
|
||
onRunTask: (task: ReviewTask) => void;
|
||
onDeleteTask: (task: ReviewTask) => void;
|
||
}
|
||
|
||
export default function TaskTable({
|
||
tasks,
|
||
selectedIds,
|
||
onSelectChange,
|
||
onViewReport,
|
||
onRunTask,
|
||
onDeleteTask
|
||
}: TaskTableProps) {
|
||
const allSelected = tasks.length > 0 && selectedIds.length === tasks.length;
|
||
|
||
const toggleSelectAll = () => {
|
||
if (allSelected) {
|
||
onSelectChange([]);
|
||
} else {
|
||
onSelectChange(tasks.map(t => t.id));
|
||
}
|
||
};
|
||
|
||
const toggleSelect = (id: string) => {
|
||
if (selectedIds.includes(id)) {
|
||
onSelectChange(selectedIds.filter(i => i !== id));
|
||
} else {
|
||
onSelectChange([...selectedIds, id]);
|
||
}
|
||
};
|
||
|
||
// 获取文件图标
|
||
const getFileIcon = (fileName: string) => {
|
||
if (fileName.endsWith('.pdf')) {
|
||
return <FileText className="w-5 h-5" />;
|
||
}
|
||
return <FileType2 className="w-5 h-5" />;
|
||
};
|
||
|
||
// 获取文件图标容器样式
|
||
const getFileIconStyle = (fileName: string) => {
|
||
if (fileName.endsWith('.pdf')) {
|
||
return 'bg-red-50 text-red-600 border-red-100';
|
||
}
|
||
return 'bg-blue-50 text-blue-600 border-blue-100';
|
||
};
|
||
|
||
// 渲染智能体标签
|
||
const renderAgentTags = (task: ReviewTask) => {
|
||
if (!task.selectedAgents || task.selectedAgents.length === 0) {
|
||
return <span className="tag tag-gray">未运行</span>;
|
||
}
|
||
|
||
return (
|
||
<div className="flex gap-1.5">
|
||
{task.selectedAgents.includes('editorial') && (
|
||
<span className="tag tag-blue">规范性</span>
|
||
)}
|
||
{task.selectedAgents.includes('methodology') && (
|
||
<span className="tag tag-purple">方法学</span>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 渲染结果摘要
|
||
const renderResultSummary = (task: ReviewTask) => {
|
||
if (task.status === 'pending') {
|
||
return <span className="text-xs text-slate-400 italic">等待发起...</span>;
|
||
}
|
||
|
||
if (task.status === 'extracting' || task.status === 'reviewing') {
|
||
return (
|
||
<div className="flex items-center gap-2 text-xs text-indigo-600">
|
||
<Loader2 className="w-3 h-3 animate-spin" />
|
||
<span>{task.status === 'extracting' ? '提取文本中...' : '审查中...'}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (task.status === 'failed') {
|
||
return <span className="text-xs text-red-500">失败</span>;
|
||
}
|
||
|
||
if (task.status === 'completed') {
|
||
return (
|
||
<div className="flex flex-col gap-1.5">
|
||
{task.editorialScore !== undefined && (
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<div className={`w-2 h-2 rounded-full ${task.editorialScore >= 80 ? 'bg-green-500' : task.editorialScore >= 60 ? 'bg-amber-500' : 'bg-red-500'}`} />
|
||
<span className="text-slate-600">规范性:</span>
|
||
<span className={`font-bold ${task.editorialScore >= 80 ? 'text-green-700' : task.editorialScore >= 60 ? 'text-amber-700' : 'text-red-700'}`}>
|
||
{task.editorialScore}分
|
||
</span>
|
||
</div>
|
||
)}
|
||
{task.methodologyStatus && (
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<div className={`w-2 h-2 rounded-full ${
|
||
task.methodologyStatus === '通过' ? 'bg-green-500' :
|
||
task.methodologyStatus === '存疑' ? 'bg-amber-500' : 'bg-red-500'
|
||
}`} />
|
||
<span className="text-slate-600">方法学:</span>
|
||
<span className={`font-bold ${
|
||
task.methodologyStatus === '通过' ? 'text-green-700' :
|
||
task.methodologyStatus === '存疑' ? 'text-amber-700' : 'text-red-700'
|
||
}`}>
|
||
{task.methodologyStatus}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
// 渲染操作按钮
|
||
const renderActions = (task: ReviewTask) => {
|
||
// 待审稿:[开始审稿] [删除]
|
||
if (task.status === 'pending') {
|
||
return (
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
onClick={() => onRunTask(task)}
|
||
className="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded-md transition-colors text-xs font-medium flex items-center gap-1"
|
||
>
|
||
<Play className="w-3 h-3" />
|
||
开始审稿
|
||
</button>
|
||
<button
|
||
onClick={() => onDeleteTask(task)}
|
||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-md transition-colors"
|
||
title="删除"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 处理中:[查看进度]
|
||
if (['extracting', 'reviewing', 'reviewing_editorial', 'reviewing_methodology'].includes(task.status)) {
|
||
return (
|
||
<button
|
||
onClick={() => onViewReport(task)}
|
||
className="text-indigo-600 hover:bg-indigo-50 px-3 py-1.5 rounded-md transition-colors text-xs font-medium flex items-center gap-1"
|
||
>
|
||
<Loader2 className="w-3 h-3 animate-spin" />
|
||
查看进度
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// 已完成:[查看报告] [重新审稿] [删除]
|
||
if (task.status === 'completed') {
|
||
return (
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
onClick={() => onViewReport(task)}
|
||
className="text-indigo-600 font-bold hover:bg-indigo-50 px-3 py-1.5 rounded-md transition-colors text-xs flex items-center gap-1"
|
||
>
|
||
<Eye className="w-3 h-3" />
|
||
查看报告
|
||
</button>
|
||
<button
|
||
onClick={() => onRunTask(task)}
|
||
className="text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 p-1.5 rounded-md transition-colors"
|
||
title="重新审稿"
|
||
>
|
||
<RefreshCw className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => onDeleteTask(task)}
|
||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-md transition-colors"
|
||
title="删除"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 失败:[重新审稿] [删除]
|
||
if (task.status === 'failed') {
|
||
return (
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
onClick={() => onRunTask(task)}
|
||
className="border border-indigo-200 text-indigo-600 hover:bg-indigo-50 px-3 py-1.5 rounded-md transition-colors text-xs font-medium flex items-center gap-1"
|
||
>
|
||
<RefreshCw className="w-3 h-3" />
|
||
重新审稿
|
||
</button>
|
||
<button
|
||
onClick={() => onDeleteTask(task)}
|
||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-md transition-colors"
|
||
title="删除"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
if (tasks.length === 0) {
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-12 text-center">
|
||
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||
<p className="text-slate-500">暂无稿件,请上传新稿件开始审查</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||
<table className="w-full text-left text-sm">
|
||
<thead className="bg-gray-50 border-b border-gray-200 text-gray-500 font-semibold uppercase tracking-wider text-xs">
|
||
<tr>
|
||
<th className="px-6 py-4 w-12">
|
||
<input
|
||
type="checkbox"
|
||
checked={allSelected}
|
||
onChange={toggleSelectAll}
|
||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||
/>
|
||
</th>
|
||
<th className="px-6 py-4 w-1/3">文件名称 / 信息</th>
|
||
<th className="px-6 py-4">上传时间</th>
|
||
<th className="px-6 py-4">审稿维度</th>
|
||
<th className="px-6 py-4">结果摘要</th>
|
||
<th className="px-6 py-4 text-right">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{tasks.map(task => (
|
||
<tr
|
||
key={task.id}
|
||
className={`hover:bg-slate-50 group transition-colors ${selectedIds.includes(task.id) ? 'bg-blue-50' : ''}`}
|
||
>
|
||
<td className="px-6 py-4">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedIds.includes(task.id)}
|
||
onChange={() => toggleSelect(task.id)}
|
||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||
/>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`p-2.5 rounded-lg border ${getFileIconStyle(task.fileName)}`}>
|
||
{getFileIcon(task.fileName)}
|
||
</div>
|
||
<div>
|
||
<div
|
||
className={`font-bold text-slate-800 text-base mb-0.5 ${task.status === 'completed' ? 'cursor-pointer hover:text-indigo-600' : ''}`}
|
||
onClick={() => task.status === 'completed' && onViewReport(task)}
|
||
>
|
||
{task.fileName}
|
||
</div>
|
||
<div className="text-xs text-slate-400 flex items-center gap-2">
|
||
<span className="bg-slate-100 px-1.5 rounded">{formatFileSize(task.fileSize)}</span>
|
||
{task.wordCount && (
|
||
<>
|
||
<span>•</span>
|
||
<span>{task.wordCount.toLocaleString()} 字</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4 text-slate-500 font-mono text-xs">
|
||
{formatTime(task.createdAt)}
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
{renderAgentTags(task)}
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
{renderResultSummary(task)}
|
||
</td>
|
||
<td className="px-6 py-4 text-right">
|
||
{renderActions(task)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|