feat(rvw): Complete RVW module development Phase 1-3
Summary: - Migrate backend to modules/rvw with v2 API routes (/api/v2/rvw) - Add new database fields: selectedAgents, editorialScore, methodologyStatus, picoExtract, isArchived - Create frontend module in frontend-v2/src/modules/rvw - Implement Dashboard with task list, filtering, batch operations - Implement ReportDetail with dual tabs (editorial/methodology) - Implement AgentModal for intelligent agent selection - Register RVW module in moduleRegistry.ts - Add navigation entry in TopNavigation - Update documentation for RVW module status (v3.0) - Update system status document (v2.9) Features: - User can select agents: editorial, methodology, or both - Support batch task execution - Task status filtering - Replace console.log with logger service - Maintain v1 API backward compatibility Tested: Frontend and backend verified locally Status: 85% complete (Phase 1-3 done)
This commit is contained in:
252
frontend/src/pages/rvw/components/TaskTable.tsx
Normal file
252
frontend/src/pages/rvw/components/TaskTable.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 任务表格组件
|
||||
*/
|
||||
import { FileText, FileType2, Loader2, Play, Eye } 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;
|
||||
}
|
||||
|
||||
export default function TaskTable({
|
||||
tasks,
|
||||
selectedIds,
|
||||
onSelectChange,
|
||||
onViewReport,
|
||||
onRunTask
|
||||
}: 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 === 'completed') {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.status === 'pending') {
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
开始
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.status === 'extracting' || task.status === 'reviewing') {
|
||||
return (
|
||||
<span className="text-xs text-slate-400 flex items-center gap-1">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
处理中
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user