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:
@@ -11,6 +11,7 @@ import {
|
||||
BatchToolbar,
|
||||
AgentModal,
|
||||
ReportDetail,
|
||||
TaskDetail,
|
||||
} from './components';
|
||||
import * as api from './api';
|
||||
import type { ReviewTask, ReviewReport, TaskFilters, AgentType } from './types';
|
||||
@@ -28,6 +29,10 @@ export default function Dashboard() {
|
||||
|
||||
// 报告详情
|
||||
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 () => {
|
||||
@@ -114,12 +119,25 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
const handleConfirmRun = async (agents: AgentType[]) => {
|
||||
// 🔥 保存到局部变量,避免onClose后丢失
|
||||
const taskToRun = pendingTaskForRun;
|
||||
|
||||
// 立即关闭弹窗
|
||||
setAgentModalVisible(false);
|
||||
setPendingTaskForRun(null);
|
||||
|
||||
try {
|
||||
if (pendingTaskForRun) {
|
||||
// 单个任务
|
||||
if (taskToRun) {
|
||||
// 单个任务 - 启动后跳转到详情页显示进度
|
||||
message.loading({ content: '正在启动审查...', key: 'run' });
|
||||
await api.runTask(pendingTaskForRun.id, agents);
|
||||
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 => {
|
||||
@@ -145,13 +163,22 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
const handleViewReport = async (task: ReviewTask) => {
|
||||
// 直接使用TaskDetail视图(支持进度和报告)
|
||||
setViewingTask(task);
|
||||
};
|
||||
|
||||
const handleDeleteTask = async (task: ReviewTask) => {
|
||||
if (!window.confirm(`确定要删除 "${task.fileName}" 吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
message.loading({ content: '加载报告中...', key: 'report' });
|
||||
const report = await api.getTaskReport(task.id);
|
||||
setReportDetail(report);
|
||||
message.destroy('report');
|
||||
message.loading({ content: '正在删除...', key: 'delete' });
|
||||
await api.deleteTask(task.id);
|
||||
message.success({ content: '删除成功', key: 'delete', duration: 2 });
|
||||
loadTasks();
|
||||
} catch (error: any) {
|
||||
message.error({ content: '加载报告失败', key: 'report', duration: 3 });
|
||||
message.error({ content: error.message || '删除失败', key: 'delete', duration: 3 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,9 +186,29 @@ export default function Dashboard() {
|
||||
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">
|
||||
@@ -207,6 +254,7 @@ export default function Dashboard() {
|
||||
onSelectChange={setSelectedIds}
|
||||
onViewReport={handleViewReport}
|
||||
onRunTask={handleRunTask}
|
||||
onDeleteTask={handleDeleteTask}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -234,3 +282,4 @@ export default function Dashboard() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -44,17 +44,18 @@ export async function getTaskReport(taskId: string): Promise<ReviewReport> {
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
// 运行审查任务
|
||||
export async function runTask(taskId: string, agents: AgentType[]): Promise<void> {
|
||||
const response = await axios.post<ApiResponse<void>>(`${API_BASE}/tasks/${taskId}/run`, { agents });
|
||||
// 运行审查任务(返回jobId供轮询)
|
||||
export async function runTask(taskId: string, agents: AgentType[]): Promise<{ taskId: string; jobId: string }> {
|
||||
const response = await axios.post<ApiResponse<{ taskId: string; jobId: string }>>(`${API_BASE}/tasks/${taskId}/run`, { agents });
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '运行失败');
|
||||
}
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
// 批量运行审查任务
|
||||
export async function batchRunTasks(taskIds: string[], agents: AgentType[]): Promise<void> {
|
||||
const response = await axios.post<ApiResponse<void>>(`${API_BASE}/tasks/batch-run`, { taskIds, agents });
|
||||
const response = await axios.post<ApiResponse<void>>(`${API_BASE}/tasks/batch/run`, { taskIds, agents });
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '批量运行失败');
|
||||
}
|
||||
@@ -127,3 +128,4 @@ export function formatTime(dateStr: string): string {
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
// 只调用onConfirm,让调用方控制关闭时机
|
||||
onConfirm(selectedAgents);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
@@ -121,3 +121,4 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -41,3 +41,5 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,3 +106,5 @@ export default function EditorialReport({ data }: EditorialReportProps) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -64,3 +64,5 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -54,3 +54,5 @@ export default function Header({ onUpload }: HeaderProps) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -108,3 +108,5 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,3 +36,5 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }:
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,3 +71,5 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }:
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
295
frontend/src/pages/rvw/components/TaskDetail.tsx
Normal file
295
frontend/src/pages/rvw/components/TaskDetail.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* 任务详情页组件
|
||||
* 支持显示审稿进度和结果
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, FileCheck, Clock, AlertCircle, CheckCircle, Loader2, FileText, Bot } from 'lucide-react';
|
||||
import type { ReviewTask, ReviewReport, TaskStatus } from '../types';
|
||||
import EditorialReport from './EditorialReport';
|
||||
import MethodologyReport from './MethodologyReport';
|
||||
import * as api from '../api';
|
||||
import { message } from 'antd';
|
||||
|
||||
interface TaskDetailProps {
|
||||
task: ReviewTask;
|
||||
jobId?: string | null; // pg-boss 任务ID(可选,用于更精确的状态轮询)
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
// 状态信息映射
|
||||
const STATUS_INFO: Record<TaskStatus, { label: string; color: string; icon: any }> = {
|
||||
pending: { label: '等待开始', color: 'text-slate-500', icon: Clock },
|
||||
extracting: { label: '正在提取文档', color: 'text-blue-500', icon: Loader2 },
|
||||
reviewing: { label: '正在初始化审查', color: 'text-indigo-500', icon: Loader2 },
|
||||
reviewing_editorial: { label: '正在审查稿约规范性', color: 'text-indigo-500', icon: Bot },
|
||||
reviewing_methodology: { label: '正在审查方法学', color: 'text-purple-500', icon: Bot },
|
||||
completed: { label: '审查完成', color: 'text-green-600', icon: CheckCircle },
|
||||
failed: { label: '审查失败', color: 'text-red-500', icon: AlertCircle },
|
||||
};
|
||||
|
||||
// 进度步骤
|
||||
const PROGRESS_STEPS = [
|
||||
{ key: 'upload', label: '上传文档', status: 'completed' as const },
|
||||
{ key: 'extract', label: '文本提取', status: 'completed' as const },
|
||||
{ key: 'editorial', label: '稿约规范性', status: 'pending' as const },
|
||||
{ key: 'methodology', label: '方法学评估', status: 'pending' as const },
|
||||
];
|
||||
|
||||
export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDetailProps) {
|
||||
const [task, setTask] = useState<ReviewTask>(initialTask);
|
||||
const [report, setReport] = useState<ReviewReport | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'editorial' | 'methodology'>('editorial');
|
||||
const [elapsedTime, setElapsedTime] = useState(0);
|
||||
|
||||
const isProcessing = ['extracting', 'reviewing', 'reviewing_editorial', 'reviewing_methodology'].includes(task.status);
|
||||
const isCompleted = task.status === 'completed';
|
||||
const isFailed = task.status === 'failed';
|
||||
|
||||
// 轮询任务状态
|
||||
useEffect(() => {
|
||||
if (!isProcessing) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const updated = await api.getTask(task.id);
|
||||
setTask(updated);
|
||||
|
||||
// 如果完成了,加载报告
|
||||
if (updated.status === 'completed') {
|
||||
const reportData = await api.getTaskReport(task.id);
|
||||
setReport(reportData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新任务状态失败:', error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [task.id, isProcessing]);
|
||||
|
||||
// 计时器
|
||||
useEffect(() => {
|
||||
if (!isProcessing) return;
|
||||
|
||||
const start = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
setElapsedTime(Math.floor((Date.now() - start) / 1000));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isProcessing]);
|
||||
|
||||
// 完成时加载报告
|
||||
useEffect(() => {
|
||||
if (isCompleted && !report) {
|
||||
api.getTaskReport(task.id).then(setReport).catch(() => {
|
||||
message.error('加载报告失败');
|
||||
});
|
||||
}
|
||||
}, [isCompleted, task.id, report]);
|
||||
|
||||
// 获取进度步骤状态
|
||||
const getStepStatus = (stepKey: string): 'completed' | 'active' | 'pending' => {
|
||||
if (task.status === 'pending') {
|
||||
return stepKey === 'upload' ? 'completed' : 'pending';
|
||||
}
|
||||
if (task.status === 'extracting') {
|
||||
if (stepKey === 'upload') return 'completed';
|
||||
if (stepKey === 'extract') return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
if (task.status === 'reviewing') {
|
||||
if (['upload', 'extract'].includes(stepKey)) return 'completed';
|
||||
if (stepKey === 'editorial') return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
if (task.status === 'reviewing_editorial') {
|
||||
if (['upload', 'extract'].includes(stepKey)) return 'completed';
|
||||
if (stepKey === 'editorial') return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
if (task.status === 'reviewing_methodology') {
|
||||
if (['upload', 'extract', 'editorial'].includes(stepKey)) return 'completed';
|
||||
if (stepKey === 'methodology') return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
if (task.status === 'completed') return 'completed';
|
||||
if (task.status === 'failed') {
|
||||
if (['upload', 'extract'].includes(stepKey)) return 'completed';
|
||||
return 'pending';
|
||||
}
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (seconds: number): string => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return mins > 0 ? `${mins}分${secs}秒` : `${secs}秒`;
|
||||
};
|
||||
|
||||
const statusInfo = STATUS_INFO[task.status];
|
||||
const StatusIcon = statusInfo.icon;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full bg-slate-50 relative fade-in">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="h-16 bg-white border-b border-gray-200 px-6 flex items-center justify-between sticky top-0 z-20 shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-slate-500 hover:text-slate-800 transition-colors px-2 py-1 rounded hover:bg-slate-100"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">返回列表</span>
|
||||
</button>
|
||||
<div className="h-6 w-px bg-slate-200" />
|
||||
<div>
|
||||
<h1 className="text-base font-bold text-slate-800 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-indigo-500" />
|
||||
{task.fileName}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isCompleted && (
|
||||
<button className="px-3 py-1.5 bg-indigo-600 text-white rounded text-sm font-medium hover:bg-indigo-700 transition shadow-sm flex items-center gap-2">
|
||||
<FileCheck className="w-4 h-4" />
|
||||
导出报告
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex-1 overflow-auto p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* 进度显示(审查中) */}
|
||||
{isProcessing && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 mb-8">
|
||||
{/* 状态头部 */}
|
||||
<div className="flex items-center justify-center gap-3 mb-8">
|
||||
<StatusIcon className={`w-6 h-6 ${statusInfo.color} ${isProcessing ? 'animate-spin' : ''}`} />
|
||||
<span className={`text-lg font-semibold ${statusInfo.color}`}>
|
||||
{statusInfo.label}
|
||||
</span>
|
||||
<span className="text-slate-400 text-sm">
|
||||
已用时 {formatTime(elapsedTime)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="flex items-center justify-between mb-6 px-8">
|
||||
{PROGRESS_STEPS.map((step, index) => {
|
||||
const stepStatus = getStepStatus(step.key);
|
||||
return (
|
||||
<div key={step.key} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`
|
||||
w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold
|
||||
${stepStatus === 'completed' ? 'bg-green-500 text-white' : ''}
|
||||
${stepStatus === 'active' ? 'bg-indigo-500 text-white animate-pulse' : ''}
|
||||
${stepStatus === 'pending' ? 'bg-slate-200 text-slate-400' : ''}
|
||||
`}>
|
||||
{stepStatus === 'completed' ? (
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
) : stepStatus === 'active' ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
index + 1
|
||||
)}
|
||||
</div>
|
||||
<span className={`text-xs mt-2 ${
|
||||
stepStatus === 'active' ? 'text-indigo-600 font-medium' : 'text-slate-500'
|
||||
}`}>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < PROGRESS_STEPS.length - 1 && (
|
||||
<div className={`w-20 h-1 mx-2 rounded ${
|
||||
getStepStatus(PROGRESS_STEPS[index + 1].key) === 'completed' || stepStatus === 'completed'
|
||||
? 'bg-green-200'
|
||||
: 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div className="text-center text-slate-500 text-sm bg-slate-50 rounded-lg py-4">
|
||||
<p>AI 正在分析您的稿件,这可能需要 1-3 分钟</p>
|
||||
<p className="text-slate-400 mt-1">请耐心等待,完成后将自动显示结果</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 失败状态 */}
|
||||
{isFailed && (
|
||||
<div className="bg-red-50 rounded-xl border border-red-200 p-8 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-red-400 mx-auto mb-4" />
|
||||
<h2 className="text-lg font-semibold text-red-700 mb-2">审查失败</h2>
|
||||
<p className="text-red-600 text-sm">{task.errorMessage || '未知错误,请重试'}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 完成状态 - 显示报告 */}
|
||||
{isCompleted && report && (
|
||||
<>
|
||||
{/* 分数卡片 */}
|
||||
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl shadow-lg p-6 mb-8 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-1">综合评分</h2>
|
||||
<p className="text-indigo-100 text-sm">
|
||||
审查用时 {report.durationSeconds ? formatTime(report.durationSeconds) : '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-5xl font-bold">{report.overallScore || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab切换 */}
|
||||
<div className="flex gap-1 bg-slate-200/50 p-1 rounded-lg mb-6 w-fit mx-auto">
|
||||
{report.editorialReview && (
|
||||
<button
|
||||
onClick={() => setActiveTab('editorial')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
activeTab === 'editorial'
|
||||
? 'font-bold bg-white text-indigo-600 shadow-sm'
|
||||
: 'font-medium text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
稿约规范性 ({report.editorialReview.overall_score}分)
|
||||
</button>
|
||||
)}
|
||||
{report.methodologyReview && (
|
||||
<button
|
||||
onClick={() => setActiveTab('methodology')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
activeTab === 'methodology'
|
||||
? 'font-bold bg-white text-indigo-600 shadow-sm'
|
||||
: 'font-medium text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
方法学评估 ({report.methodologyReview.overall_score}分)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 报告内容 */}
|
||||
{activeTab === 'editorial' && report.editorialReview && (
|
||||
<EditorialReport data={report.editorialReview} />
|
||||
)}
|
||||
{activeTab === 'methodology' && report.methodologyReview && (
|
||||
<MethodologyReport data={report.methodologyReview} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 任务表格组件
|
||||
*/
|
||||
import { FileText, FileType2, Loader2, Play, Eye } from 'lucide-react';
|
||||
import { FileText, FileType2, Loader2, Play, Eye, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import type { ReviewTask } from '../types';
|
||||
import { formatFileSize, formatTime } from '../api';
|
||||
|
||||
@@ -11,6 +11,7 @@ interface TaskTableProps {
|
||||
onSelectChange: (ids: string[]) => void;
|
||||
onViewReport: (task: ReviewTask) => void;
|
||||
onRunTask: (task: ReviewTask) => void;
|
||||
onDeleteTask: (task: ReviewTask) => void;
|
||||
}
|
||||
|
||||
export default function TaskTable({
|
||||
@@ -18,7 +19,8 @@ export default function TaskTable({
|
||||
selectedIds,
|
||||
onSelectChange,
|
||||
onViewReport,
|
||||
onRunTask
|
||||
onRunTask,
|
||||
onDeleteTask
|
||||
}: TaskTableProps) {
|
||||
const allSelected = tasks.length > 0 && selectedIds.length === tasks.length;
|
||||
|
||||
@@ -127,36 +129,89 @@ export default function TaskTable({
|
||||
|
||||
// 渲染操作按钮
|
||||
const renderActions = (task: ReviewTask) => {
|
||||
if (task.status === 'completed') {
|
||||
// 待审稿:[开始审稿] [删除]
|
||||
if (task.status === 'pending') {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => onRunTask(task)}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white 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>
|
||||
<button
|
||||
onClick={() => onDeleteTask(task)}
|
||||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-md transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 处理中:[查看进度]
|
||||
if (['extracting', 'reviewing', 'reviewing_editorial', 'reviewing_methodology'].includes(task.status)) {
|
||||
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"
|
||||
className="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"
|
||||
>
|
||||
<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>
|
||||
查看进度
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// 已完成:[查看报告] [重新审稿] [删除]
|
||||
if (task.status === 'completed') {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<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>
|
||||
<button
|
||||
onClick={() => onRunTask(task)}
|
||||
className="text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 p-1.5 rounded-md transition-colors"
|
||||
title="重新审稿"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteTask(task)}
|
||||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-md transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 失败:[重新审稿] [删除]
|
||||
if (task.status === 'failed') {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<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"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
重新审稿
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteTask(task)}
|
||||
className="text-slate-400 hover:text-red-500 hover:bg-red-50 p-1.5 rounded-md transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,3 +305,5 @@ export default function TaskTable({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,4 +11,6 @@ export { default as ScoreRing } from './ScoreRing';
|
||||
export { default as EditorialReport } from './EditorialReport';
|
||||
export { default as MethodologyReport } from './MethodologyReport';
|
||||
export { default as ReportDetail } from './ReportDetail';
|
||||
export { default as TaskDetail } from './TaskDetail';
|
||||
|
||||
|
||||
|
||||
@@ -5,3 +5,5 @@ export { default as RvwDashboard } from './Dashboard';
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -231,3 +231,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
// 任务状态
|
||||
export type TaskStatus =
|
||||
| 'pending' // 待处理
|
||||
| 'extracting' // 提取文本中
|
||||
| 'reviewing' // 审查中
|
||||
| 'completed' // 已完成
|
||||
| 'failed'; // 失败
|
||||
| 'pending' // 待处理
|
||||
| 'extracting' // 提取文本中
|
||||
| 'reviewing' // 审查中
|
||||
| 'reviewing_editorial' // 正在审查稿约规范性
|
||||
| 'reviewing_methodology' // 正在审查方法学
|
||||
| 'completed' // 已完成
|
||||
| 'failed'; // 失败
|
||||
|
||||
// 智能体类型
|
||||
export type AgentType = 'editorial' | 'methodology';
|
||||
@@ -90,3 +92,4 @@ export interface TaskFilters {
|
||||
timeRange: 'all' | 'today' | 'week';
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user