import { useState } from 'react'; import { Card, Upload, Button, Select, Steps, Progress, message, Typography, Space, Alert, Spin, Empty, Tag, Divider, Tabs, Dropdown, } from 'antd'; import type { MenuProps } from 'antd'; import './ReviewPage.css'; import { UploadOutlined, FileTextOutlined, CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined, RocketOutlined, DownloadOutlined, PrinterOutlined, CopyOutlined, } from '@ant-design/icons'; import type { UploadProps } from 'antd'; import { uploadManuscript, pollTaskUntilComplete, getTaskReport, getStatusText, getStatusColor, formatFileSize, formatDuration, type ReviewTask, type ReviewReport, type ReviewTaskStatus, } from '../api/reviewApi'; import ScoreCard from '../components/review/ScoreCard'; import EditorialReview from '../components/review/EditorialReview'; import MethodologyReview from '../components/review/MethodologyReview'; const { Title, Paragraph, Text } = Typography; const { Dragger } = Upload; /** * 稿件审查页面 */ const ReviewPage = () => { // ==================== State ==================== const [modelType, setModelType] = useState<'deepseek-v3' | 'qwen3-72b' | 'qwen-long'>('deepseek-v3'); const [selectedFile, setSelectedFile] = useState(null); const [uploading, setUploading] = useState(false); const [currentTask, setCurrentTask] = useState(null); const [report, setReport] = useState(null); const [error, setError] = useState(null); // ==================== 上传配置 ==================== const uploadProps: UploadProps = { name: 'file', multiple: false, maxCount: 1, accept: '.doc,.docx', beforeUpload: (file) => { // 验证文件类型 const isDoc = file.name.endsWith('.doc') || file.name.endsWith('.docx'); if (!isDoc) { message.error('只支持Word文档(.doc或.docx)!'); return Upload.LIST_IGNORE; } // 验证文件大小(5MB) const isLt5M = file.size / 1024 / 1024 < 5; if (!isLt5M) { message.error('文件大小不能超过5MB!'); return Upload.LIST_IGNORE; } setSelectedFile(file); return false; // 阻止自动上传 }, onRemove: () => { setSelectedFile(null); }, }; // ==================== 开始审查 ==================== const handleStartReview = async () => { if (!selectedFile) { message.warning('请先选择稿件文件!'); return; } setUploading(true); setError(null); setReport(null); try { // 1. 上传稿件 message.loading({ content: '正在上传稿件...', key: 'upload' }); const result = await uploadManuscript({ file: selectedFile, modelType, }); message.success({ content: '上传成功!开始审查...', key: 'upload', duration: 2 }); // 2. 轮询任务状态 const task = await pollTaskUntilComplete( result.taskId, (updatedTask) => { setCurrentTask(updatedTask); console.log('任务状态:', updatedTask.status); }, 60 // 最多5分钟 ); // 3. 任务完成,获取报告 if (task.status === 'completed') { const fullReport = await getTaskReport(task.id); setReport(fullReport); message.success('审查完成!'); } else if (task.status === 'failed') { setError(task.errorMessage || '审查失败'); message.error('审查失败:' + (task.errorMessage || '未知错误')); } } catch (err: any) { console.error('审查失败:', err); setError(err.message || '审查过程出错'); message.error('审查失败:' + (err.message || '未知错误')); } finally { setUploading(false); } }; // ==================== 重新审查 ==================== const handleReset = () => { setSelectedFile(null); setCurrentTask(null); setReport(null); setError(null); }; // ==================== 导出报告 ==================== const handleExportPDF = async () => { if (!report) return; try { message.loading({ content: '正在生成PDF...', key: 'pdf', duration: 0 }); // 动态导入依赖 const html2canvas = (await import('html2canvas')).default; const jsPDF = (await import('jspdf')).default; // 获取报告内容的DOM元素 const reportElement = document.getElementById('report-content'); if (!reportElement) { message.error('无法找到报告内容'); return; } // 1. 确保所有Tabs内容都显示 const tabPanes = reportElement.querySelectorAll('.ant-tabs-tabpane'); const originalTabDisplay: string[] = []; tabPanes.forEach((pane, index) => { const htmlPane = pane as HTMLElement; originalTabDisplay[index] = htmlPane.style.display; htmlPane.style.display = 'block'; // 强制显示所有Tab内容 }); // 2. 隐藏不需要导出的元素 const elementsToHide = reportElement.querySelectorAll('.no-print, .ant-btn, .ant-dropdown, .ant-tabs-nav'); elementsToHide.forEach((el) => { (el as HTMLElement).style.display = 'none'; }); // 3. 等待DOM更新和渲染 await new Promise(resolve => setTimeout(resolve, 300)); // 4. 将HTML转换为Canvas const canvas = await html2canvas(reportElement, { scale: 2, // 提高清晰度 useCORS: true, logging: false, backgroundColor: '#ffffff', windowWidth: reportElement.scrollWidth, windowHeight: reportElement.scrollHeight, }); // 5. 恢复原始状态 elementsToHide.forEach((el) => { (el as HTMLElement).style.display = ''; }); tabPanes.forEach((pane, index) => { (pane as HTMLElement).style.display = originalTabDisplay[index] || ''; }); // 6. 创建PDF const imgWidth = 210; // A4纸宽度(mm) const imgHeight = (canvas.height * imgWidth) / canvas.width; const pageHeight = 297; // A4纸高度(mm) const pdf = new jsPDF('p', 'mm', 'a4'); const imgData = canvas.toDataURL('image/png'); let heightLeft = imgHeight; let position = 0; // 添加第一页 pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; // 如果内容超过一页,添加更多页 while (heightLeft > 0) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; } // 7. 下载PDF const fileName = `稿件审查报告-${report.fileName.replace('.docx', '').replace('.doc', '')}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.pdf`; pdf.save(fileName); message.success({ content: 'PDF已生成并下载!', key: 'pdf', duration: 2 }); } catch (error) { console.error('PDF生成失败:', error); message.error({ content: 'PDF生成失败,请重试', key: 'pdf', duration: 3 }); } }; const handleCopyReport = () => { if (!report) return; // 构建文本格式的报告 let reportText = `稿件审查报告\n`; reportText += `${'='.repeat(60)}\n\n`; reportText += `文件名: ${report.fileName}\n`; reportText += `字数: ${report.wordCount} 字符\n`; reportText += `使用模型: ${report.modelUsed}\n`; reportText += `评估时间: ${report.completedAt ? new Date(report.completedAt).toLocaleString('zh-CN') : 'N/A'}\n`; reportText += `耗时: ${report.durationSeconds ? formatDuration(report.durationSeconds) : 'N/A'}\n\n`; reportText += `总体评分: ${report.overallScore?.toFixed(1) || 'N/A'} / 100\n`; reportText += `${'='.repeat(60)}\n\n`; // 稿约规范性评估 if (report.editorialReview) { reportText += `一、稿约规范性评估\n`; reportText += `-`.repeat(60) + '\n\n'; reportText += `总分: ${report.editorialReview.overall_score} / 100\n\n`; reportText += `总结: ${report.editorialReview.summary}\n\n`; report.editorialReview.items.forEach((item, index) => { reportText += `${index + 1}. ${item.criterion}\n`; reportText += ` 状态: ${item.status === 'pass' ? '✓ 通过' : item.status === 'warning' ? '⚠ 警告' : '✗ 不通过'}\n`; reportText += ` 评分: ${item.score} / 100\n`; if (item.issues && item.issues.length > 0) { reportText += ` 问题:\n`; item.issues.forEach(issue => { reportText += ` - ${issue}\n`; }); } if (item.suggestions && item.suggestions.length > 0) { reportText += ` 建议:\n`; item.suggestions.forEach(suggestion => { reportText += ` - ${suggestion}\n`; }); } reportText += '\n'; }); } // 方法学评估 if (report.methodologyReview) { reportText += `\n二、方法学评估\n`; reportText += `-`.repeat(60) + '\n\n'; reportText += `总分: ${report.methodologyReview.overall_score} / 100\n\n`; reportText += `总结: ${report.methodologyReview.summary}\n\n`; report.methodologyReview.parts.forEach((part, partIndex) => { reportText += `${partIndex + 1}. ${part.part}\n`; reportText += ` 评分: ${part.score} / 100\n`; if (part.issues && part.issues.length > 0) { reportText += ` 发现的问题:\n`; part.issues.forEach((issue, issueIndex) => { reportText += ` ${issueIndex + 1}) ${issue.type} [${issue.severity === 'major' ? '严重' : '轻微'}]\n`; reportText += ` 描述: ${issue.description}\n`; reportText += ` 位置: ${issue.location}\n`; reportText += ` 建议: ${issue.suggestion}\n`; }); } else { reportText += ` ✓ 未发现问题\n`; } reportText += '\n'; }); } reportText += `${'='.repeat(60)}\n`; reportText += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n`; // 复制到剪贴板 navigator.clipboard.writeText(reportText).then(() => { message.success('报告已复制到剪贴板!可粘贴到Word或其他文档中'); }).catch(() => { message.error('复制失败,请手动选择文本复制'); }); }; // ==================== 获取当前步骤 ==================== const getCurrentStep = (status: ReviewTaskStatus): number => { const stepMap: Record = { pending: 0, extracting: 1, reviewing_editorial: 2, reviewing_methodology: 3, completed: 4, failed: 4, }; return stepMap[status] || 0; }; // ==================== 渲染 ==================== return (
{/* 标题 */}
<FileTextOutlined /> 稿件审查 智能评估稿件的规范性和方法学质量,提供详细的改进建议
{/* 主内容 */} {!currentTask && !report && !error && ( <> {/* 上传区域 */}

点击或拖拽Word文档到此区域

仅支持 .doc 或 .docx 格式,文件大小不超过5MB

{selectedFile && ( 文件名:{selectedFile.name} 大小:{formatFileSize(selectedFile.size)}
} type="success" showIcon /> )}
{/* 模型选择 */}