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
286 lines
8.9 KiB
TypeScript
286 lines
8.9 KiB
TypeScript
/**
|
||
* 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.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>
|
||
);
|
||
}
|
||
|
||
|