feat(rvw): Complete Phase 4-5 - Bug fixes and Word export

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
This commit is contained in:
2026-01-10 22:52:15 +08:00
parent 179afa2c6b
commit 440f75255e
237 changed files with 3942 additions and 657 deletions

View File

@@ -0,0 +1,284 @@
/**
* RVW审稿系统 - 主Dashboard页面
*/
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd';
import {
Sidebar,
Header,
FilterChips,
TaskTable,
BatchToolbar,
AgentModal,
ReportDetail,
TaskDetail,
} from '../components';
import * as api from '../api';
import type { ReviewTask, ReviewReport, TaskFilters, AgentType } from '../types';
import '../styles/index.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 [viewingTask, setViewingTask] = useState<ReviewTask | null>(null);
const [currentJobId, setCurrentJobId] = useState<string | 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[]) => {
// 🔥 保存到局部变量避免onClose后丢失
const taskToRun = pendingTaskForRun;
// 立即关闭弹窗
setAgentModalVisible(false);
setPendingTaskForRun(null);
try {
if (taskToRun) {
// 单个任务 - 启动后跳转到详情页显示进度
message.loading({ content: '正在启动审查...', key: 'run' });
const { jobId } = await api.runTask(taskToRun.id, agents);
message.success({ content: '审查已启动', key: 'run', duration: 2 });
// 更新任务状态后跳转到详情页传递jobId
const updatedTask = await api.getTask(taskToRun.id);
setCurrentJobId(jobId);
setViewingTask(updatedTask);
return;
} 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) => {
// 直接使用TaskDetail视图支持进度和报告
setViewingTask(task);
};
const handleDeleteTask = async (task: ReviewTask) => {
if (!window.confirm(`确定要删除 "${task.fileName}" 吗?`)) {
return;
}
try {
message.loading({ content: '正在删除...', key: 'delete' });
await api.deleteTask(task.id);
message.success({ content: '删除成功', key: 'delete', duration: 2 });
loadTasks();
} catch (error: any) {
message.error({ content: error.message || '删除失败', key: 'delete', duration: 3 });
}
};
const handleBackToList = () => {
setReportDetail(null);
};
// 返回列表并刷新
const handleBackFromDetail = () => {
setViewingTask(null);
setCurrentJobId(null);
loadTasks();
};
// ==================== 渲染 ====================
// 任务详情视图(支持进度显示)
if (viewingTask) {
return (
<div className="h-full flex overflow-hidden bg-slate-50">
<Sidebar
currentView={currentView}
onViewChange={setCurrentView}
/>
<TaskDetail task={viewingTask} jobId={currentJobId} onBack={handleBackFromDetail} />
</div>
);
}
// 报告详情视图(旧版,保留兼容)
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}
onDeleteTask={handleDeleteTask}
/>
)}
</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>
);
}