feat(frontend): add batch processing and review features
- Add batch processing API and mode - Add deep read mode for full-text analysis - Add document selector and switcher components - Add review page with editorial and methodology assessment - Add capacity indicator and usage info modal - Add custom hooks for batch tasks and chat modes - Update layouts and routing - Add TypeScript types for chat features
This commit is contained in:
624
frontend/src/pages/ReviewPage.tsx
Normal file
624
frontend/src/pages/ReviewPage.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
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<File | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [currentTask, setCurrentTask] = useState<ReviewTask | null>(null);
|
||||
const [report, setReport] = useState<ReviewReport | null>(null);
|
||||
const [error, setError] = useState<string | null>(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<ReviewTaskStatus, number> = {
|
||||
pending: 0,
|
||||
extracting: 1,
|
||||
reviewing_editorial: 2,
|
||||
reviewing_methodology: 3,
|
||||
completed: 4,
|
||||
failed: 4,
|
||||
};
|
||||
return stepMap[status] || 0;
|
||||
};
|
||||
|
||||
// ==================== 渲染 ====================
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
padding: '24px',
|
||||
}}>
|
||||
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
|
||||
{/* 标题 */}
|
||||
<Card style={{ marginBottom: 24, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<div style={{ color: 'white', padding: '20px 0' }}>
|
||||
<Title level={2} style={{ color: 'white', marginBottom: 16 }}>
|
||||
<FileTextOutlined /> 稿件审查
|
||||
</Title>
|
||||
<Paragraph style={{ color: 'white', fontSize: 16, marginBottom: 0 }}>
|
||||
智能评估稿件的规范性和方法学质量,提供详细的改进建议
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 主内容 */}
|
||||
{!currentTask && !report && !error && (
|
||||
<>
|
||||
{/* 上传区域 */}
|
||||
<Card title="1. 上传稿件" style={{ marginBottom: 24 }}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Dragger {...uploadProps} style={{ padding: '20px' }}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<FileTextOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖拽Word文档到此区域</p>
|
||||
<p className="ant-upload-hint">
|
||||
仅支持 .doc 或 .docx 格式,文件大小不超过5MB
|
||||
</p>
|
||||
</Dragger>
|
||||
|
||||
{selectedFile && (
|
||||
<Alert
|
||||
message="已选择文件"
|
||||
description={
|
||||
<Space direction="vertical">
|
||||
<Text>文件名:{selectedFile.name}</Text>
|
||||
<Text>大小:{formatFileSize(selectedFile.size)}</Text>
|
||||
</Space>
|
||||
}
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 模型选择 */}
|
||||
<Card title="2. 选择评估模型" style={{ marginBottom: 24 }}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Select
|
||||
value={modelType}
|
||||
onChange={setModelType}
|
||||
style={{ width: '100%' }}
|
||||
size="large"
|
||||
options={[
|
||||
{
|
||||
value: 'deepseek-v3',
|
||||
label: (
|
||||
<Space>
|
||||
<RocketOutlined />
|
||||
<span>DeepSeek-V3(推荐)- 高质量、高速度</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'qwen3-72b',
|
||||
label: (
|
||||
<Space>
|
||||
<span>Qwen3-72B - 阿里云千问大模型</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'qwen-long',
|
||||
label: (
|
||||
<Space>
|
||||
<span>Qwen-Long - 超长上下文(1M tokens)</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
message="模型说明"
|
||||
description={
|
||||
<div>
|
||||
<p><strong>DeepSeek-V3</strong>:性能强大,速度快,推荐使用</p>
|
||||
<p><strong>Qwen3-72B</strong>:阿里云千问大模型,表现稳定</p>
|
||||
<p><strong>Qwen-Long</strong>:支持超长文本,适合特别长的稿件</p>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 开始按钮 */}
|
||||
<Card>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<RocketOutlined />}
|
||||
onClick={handleStartReview}
|
||||
disabled={!selectedFile || uploading}
|
||||
loading={uploading}
|
||||
block
|
||||
>
|
||||
{uploading ? '审查中...' : '开始审查'}
|
||||
</Button>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 进度展示 */}
|
||||
{currentTask && !report && !error && (
|
||||
<Card>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Spin size="large" indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
|
||||
<Title level={4} style={{ marginTop: 16 }}>
|
||||
正在审查中...
|
||||
</Title>
|
||||
<Tag color={getStatusColor(currentTask.status)} style={{ fontSize: 14, padding: '4px 12px' }}>
|
||||
{getStatusText(currentTask.status)}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<Steps
|
||||
current={getCurrentStep(currentTask.status)}
|
||||
items={[
|
||||
{ title: '上传完成', icon: <CheckCircleOutlined /> },
|
||||
{ title: '提取文本', icon: getCurrentStep(currentTask.status) >= 1 ? <CheckCircleOutlined /> : <LoadingOutlined /> },
|
||||
{ title: '稿约评估', icon: getCurrentStep(currentTask.status) >= 2 ? <CheckCircleOutlined /> : <LoadingOutlined /> },
|
||||
{ title: '方法学评估', icon: getCurrentStep(currentTask.status) >= 3 ? <CheckCircleOutlined /> : <LoadingOutlined /> },
|
||||
{ title: '生成报告', icon: getCurrentStep(currentTask.status) >= 4 ? <CheckCircleOutlined /> : <LoadingOutlined /> },
|
||||
]}
|
||||
/>
|
||||
|
||||
{currentTask.wordCount && (
|
||||
<Alert
|
||||
message="文档信息"
|
||||
description={`已提取 ${currentTask.wordCount} 个字符`}
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<Progress percent={getCurrentStep(currentTask.status) * 20} status="active" />
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 报告展示(完整版) */}
|
||||
{report && (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }} id="report-content">
|
||||
{/* 成功提示 */}
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<CheckCircleOutlined style={{ fontSize: 72, color: '#52c41a' }} />
|
||||
<Title level={3} style={{ marginTop: 16 }}>
|
||||
审查完成!
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
已完成稿约规范性评估和方法学评估
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 总体评分 */}
|
||||
<ScoreCard
|
||||
title="总体评分"
|
||||
score={report.overallScore || 0}
|
||||
description={`稿约规范性(40%)+ 方法学(60%)的综合评分`}
|
||||
size="large"
|
||||
/>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<Card title="审查信息">
|
||||
<Space direction="vertical">
|
||||
<Text><strong>文件名:</strong>{report.fileName}</Text>
|
||||
<Text><strong>字数:</strong>{report.wordCount} 字符</Text>
|
||||
<Text><strong>使用模型:</strong>{report.modelUsed}</Text>
|
||||
<Text><strong>耗时:</strong>{report.durationSeconds ? formatDuration(report.durationSeconds) : 'N/A'}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 详细评估报告(Tabs) */}
|
||||
<Tabs
|
||||
defaultActiveKey="editorial"
|
||||
size="large"
|
||||
items={[
|
||||
{
|
||||
key: 'editorial',
|
||||
label: `稿约规范性评估(${report.editorialReview?.overall_score || 'N/A'}分)`,
|
||||
children: report.editorialReview ? (
|
||||
<EditorialReview data={report.editorialReview} />
|
||||
) : (
|
||||
<Empty description="暂无稿约规范性评估数据" />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'methodology',
|
||||
label: `方法学评估(${report.methodologyReview?.overall_score || 'N/A'}分)`,
|
||||
children: report.methodologyReview ? (
|
||||
<MethodologyReview data={report.methodologyReview} />
|
||||
) : (
|
||||
<Empty description="暂无方法学评估数据" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Card className="no-print">
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<RocketOutlined />}
|
||||
onClick={handleReset}
|
||||
block
|
||||
>
|
||||
审查新稿件
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'pdf',
|
||||
label: '导出为PDF',
|
||||
icon: <PrinterOutlined />,
|
||||
onClick: handleExportPDF,
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: '复制报告内容',
|
||||
icon: <CopyOutlined />,
|
||||
onClick: handleCopyReport,
|
||||
},
|
||||
],
|
||||
}}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon={<DownloadOutlined />}
|
||||
block
|
||||
>
|
||||
导出报告
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{/* 错误展示 */}
|
||||
{error && (
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<CloseCircleOutlined style={{ fontSize: 72, color: '#f5222d' }} />
|
||||
<Title level={3} style={{ marginTop: 16, color: '#f5222d' }}>
|
||||
审查失败
|
||||
</Title>
|
||||
<Alert
|
||||
message="错误信息"
|
||||
description={error}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginTop: 24, textAlign: 'left' }}
|
||||
/>
|
||||
<Button type="primary" size="large" onClick={handleReset} style={{ marginTop: 24 }}>
|
||||
重新开始
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewPage;
|
||||
|
||||
Reference in New Issue
Block a user