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:
2026-01-07 22:39:08 +08:00
parent 06028c6952
commit 179afa2c6b
226 changed files with 5860 additions and 21 deletions

View File

@@ -0,0 +1,236 @@
/**
* RVW审稿系统 - 主Dashboard页面
*/
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd';
import {
Sidebar,
Header,
FilterChips,
TaskTable,
BatchToolbar,
AgentModal,
ReportDetail,
} from './components';
import * as api from './api';
import type { ReviewTask, ReviewReport, TaskFilters, AgentType } from './types';
import './styles.css';
export default function Dashboard() {
// ==================== State ====================
const [currentView, setCurrentView] = useState<'dashboard' | 'archive'>('dashboard');
const [tasks, setTasks] = useState<ReviewTask[]>([]);
const [loading, setLoading] = useState(true);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [filters, setFilters] = useState<TaskFilters>({ status: 'all', timeRange: 'all' });
const [agentModalVisible, setAgentModalVisible] = useState(false);
const [pendingTaskForRun, setPendingTaskForRun] = useState<ReviewTask | null>(null);
// 报告详情
const [reportDetail, setReportDetail] = useState<ReviewReport | null>(null);
// ==================== 数据加载 ====================
const loadTasks = useCallback(async () => {
try {
setLoading(true);
const data = await api.getTasks(filters.status !== 'all' ? filters.status : undefined);
// 时间筛选
let filtered = data;
if (filters.timeRange === 'today') {
const today = new Date().toDateString();
filtered = data.filter(t => new Date(t.createdAt).toDateString() === today);
} else if (filters.timeRange === 'week') {
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
filtered = data.filter(t => new Date(t.createdAt).getTime() > weekAgo);
}
setTasks(filtered);
} catch (error) {
console.error('加载任务失败:', error);
message.error('加载任务列表失败');
} finally {
setLoading(false);
}
}, [filters]);
useEffect(() => {
loadTasks();
}, [loadTasks]);
// 轮询更新进行中的任务
useEffect(() => {
const processingTasks = tasks.filter(t =>
t.status === 'extracting' || t.status === 'reviewing'
);
if (processingTasks.length === 0) return;
const interval = setInterval(async () => {
for (const task of processingTasks) {
try {
const updated = await api.getTask(task.id);
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t));
} catch (error) {
console.error('更新任务状态失败:', error);
}
}
}, 3000);
return () => clearInterval(interval);
}, [tasks]);
// ==================== 统计数据 ====================
const counts = {
all: tasks.length,
pending: tasks.filter(t => t.status === 'pending').length,
completed: tasks.filter(t => t.status === 'completed').length,
};
// ==================== 事件处理 ====================
const handleUpload = async (files: FileList) => {
const uploadPromises = Array.from(files).map(async (file) => {
try {
message.loading({ content: `正在上传 ${file.name}...`, key: file.name });
await api.uploadManuscript(file);
message.success({ content: `${file.name} 上传成功`, key: file.name, duration: 2 });
} catch (error: any) {
message.error({ content: `${file.name} 上传失败: ${error.message}`, key: file.name, duration: 3 });
}
});
await Promise.all(uploadPromises);
loadTasks();
};
const handleRunTask = (task: ReviewTask) => {
setPendingTaskForRun(task);
setAgentModalVisible(true);
};
const handleRunBatch = () => {
setPendingTaskForRun(null); // 批量模式
setAgentModalVisible(true);
};
const handleConfirmRun = async (agents: AgentType[]) => {
try {
if (pendingTaskForRun) {
// 单个任务
message.loading({ content: '正在启动审查...', key: 'run' });
await api.runTask(pendingTaskForRun.id, agents);
message.success({ content: '审查已启动', key: 'run', duration: 2 });
} else {
// 批量任务
const pendingIds = selectedIds.filter(id => {
const task = tasks.find(t => t.id === id);
return task && task.status === 'pending';
});
if (pendingIds.length === 0) {
message.warning('没有待处理的任务');
return;
}
message.loading({ content: `正在启动 ${pendingIds.length} 个任务...`, key: 'run' });
await api.batchRunTasks(pendingIds, agents);
message.success({ content: `${pendingIds.length} 个任务已启动`, key: 'run', duration: 2 });
setSelectedIds([]);
}
loadTasks();
} catch (error: any) {
message.error({ content: error.message || '启动失败', key: 'run', duration: 3 });
}
};
const handleViewReport = async (task: ReviewTask) => {
try {
message.loading({ content: '加载报告中...', key: 'report' });
const report = await api.getTaskReport(task.id);
setReportDetail(report);
message.destroy('report');
} catch (error: any) {
message.error({ content: '加载报告失败', key: 'report', duration: 3 });
}
};
const handleBackToList = () => {
setReportDetail(null);
};
// ==================== 渲染 ====================
// 报告详情视图
if (reportDetail) {
return (
<div className="h-full flex overflow-hidden bg-slate-50">
<Sidebar
currentView={currentView}
onViewChange={setCurrentView}
/>
<ReportDetail report={reportDetail} onBack={handleBackToList} />
</div>
);
}
// 主仪表盘视图
return (
<div className="h-full flex overflow-hidden bg-slate-50">
<Sidebar
currentView={currentView}
onViewChange={setCurrentView}
/>
<main className="flex-1 flex flex-col min-w-0 bg-white">
<div className="flex-1 flex flex-col h-full relative fade-in">
{/* 顶部操作区 */}
<header className="bg-white px-8 pt-6 pb-4 border-b border-gray-100 flex-shrink-0 z-10">
<Header onUpload={handleUpload} />
<FilterChips
filters={filters}
counts={counts}
onFilterChange={setFilters}
/>
</header>
{/* 列表区域 */}
<div className="flex-1 overflow-auto bg-slate-50/50 p-6">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600" />
</div>
) : (
<TaskTable
tasks={tasks}
selectedIds={selectedIds}
onSelectChange={setSelectedIds}
onViewReport={handleViewReport}
onRunTask={handleRunTask}
/>
)}
</div>
</div>
</main>
{/* 批量操作工具栏 */}
<BatchToolbar
selectedCount={selectedIds.length}
onRunBatch={handleRunBatch}
onClearSelection={() => setSelectedIds([])}
/>
{/* 智能体选择弹窗 */}
<AgentModal
visible={agentModalVisible}
taskCount={pendingTaskForRun ? 1 : selectedIds.length}
onClose={() => {
setAgentModalVisible(false);
setPendingTaskForRun(null);
}}
onConfirm={handleConfirmRun}
/>
</div>
);
}