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:
284
frontend-v2/src/modules/rvw/pages/Dashboard.tsx
Normal file
284
frontend-v2/src/modules/rvw/pages/Dashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user