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:
2025-11-16 15:43:39 +08:00
parent 11325f88a7
commit 0fe6821a89
52 changed files with 7324 additions and 109 deletions

View File

@@ -0,0 +1,192 @@
import { Card, Collapse, Space, Typography, Tag, Alert, Empty, List, Divider } from 'antd';
import {
CheckCircleOutlined,
WarningOutlined,
CloseCircleOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import type { EditorialReview as EditorialReviewType, EditorialItem } from '../../api/reviewApi';
import ScoreCard from './ScoreCard';
const { Title, Text, Paragraph } = Typography;
const { Panel } = Collapse;
interface EditorialReviewProps {
data: EditorialReviewType;
}
/**
* 稿约规范性评估详情组件
* 展示11个评估标准的详细结果
*/
const EditorialReview = ({ data }: EditorialReviewProps) => {
if (!data) {
return <Empty description="暂无评估数据" />;
}
// 获取状态图标和颜色
const getStatusDisplay = (status: 'pass' | 'warning' | 'fail') => {
const config = {
pass: {
icon: <CheckCircleOutlined />,
color: 'success' as const,
text: '通过',
},
warning: {
icon: <WarningOutlined />,
color: 'warning' as const,
text: '警告',
},
fail: {
icon: <CloseCircleOutlined />,
color: 'error' as const,
text: '不通过',
},
};
return config[status];
};
// 统计数据
const stats = {
total: data.items.length,
pass: data.items.filter((item) => item.status === 'pass').length,
warning: data.items.filter((item) => item.status === 'warning').length,
fail: data.items.filter((item) => item.status === 'fail').length,
};
return (
<Card title={
<Space>
<FileTextOutlined />
<span>稿</span>
</Space>
}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 总体评分 */}
<ScoreCard
title="总体评分"
score={data.overall_score}
description="稿约规范性综合评分"
size="large"
/>
{/* 总结 */}
<Alert
message="评估总结"
description={data.summary}
type="info"
showIcon
/>
{/* 统计信息 */}
<Card size="small">
<Space size="large">
<div>
<Text type="secondary"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8 }}>{stats.total}</Text>
</div>
<Divider type="vertical" />
<div>
<Text type="success"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8, color: '#52c41a' }}>{stats.pass}</Text>
</div>
<Divider type="vertical" />
<div>
<Text type="warning"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8, color: '#faad14' }}>{stats.warning}</Text>
</div>
<Divider type="vertical" />
<div>
<Text type="danger"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8, color: '#f5222d' }}>{stats.fail}</Text>
</div>
</Space>
</Card>
{/* 详细评估结果 */}
<Card title="详细评估11项标准" size="small">
<Collapse defaultActiveKey={data.items.map((_, index) => index.toString())}>
{data.items.map((item: EditorialItem, index: number) => {
const statusDisplay = getStatusDisplay(item.status);
return (
<Panel
header={
<Space>
<Tag icon={statusDisplay.icon} color={statusDisplay.color}>
{statusDisplay.text}
</Tag>
<Text strong>{item.criterion}</Text>
<Text type="secondary">{item.score}</Text>
</Space>
}
key={index}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{/* 评分 */}
<div>
<Text strong></Text>
<Text style={{ fontSize: 18, marginLeft: 8, color: '#1890ff' }}>
{item.score} / 100
</Text>
</div>
{/* 问题列表 */}
{item.issues && item.issues.length > 0 && (
<div>
<Text strong style={{ color: '#f5222d' }}></Text>
<List
size="small"
bordered
dataSource={item.issues}
renderItem={(issue: string) => (
<List.Item>
<CloseCircleOutlined style={{ color: '#f5222d', marginRight: 8 }} />
{issue}
</List.Item>
)}
style={{ marginTop: 8 }}
/>
</div>
)}
{/* 改进建议 */}
{item.suggestions && item.suggestions.length > 0 && (
<div>
<Text strong style={{ color: '#52c41a' }}></Text>
<List
size="small"
bordered
dataSource={item.suggestions}
renderItem={(suggestion: string) => (
<List.Item>
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: 8 }} />
{suggestion}
</List.Item>
)}
style={{ marginTop: 8 }}
/>
</div>
)}
{/* 无问题提示 */}
{(!item.issues || item.issues.length === 0) && item.status === 'pass' && (
<Alert
message="该项检查通过,无需修改"
type="success"
showIcon
/>
)}
</Space>
</Panel>
);
})}
</Collapse>
</Card>
</Space>
</Card>
);
};
export default EditorialReview;

View File

@@ -0,0 +1,207 @@
import { Card, Collapse, Space, Typography, Tag, Alert, Empty, List, Divider } from 'antd';
import {
ExperimentOutlined,
WarningOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import type { MethodologyReview as MethodologyReviewType, MethodologyPart, MethodologyIssue } from '../../api/reviewApi';
import ScoreCard from './ScoreCard';
const { Title, Text, Paragraph } = Typography;
const { Panel } = Collapse;
interface MethodologyReviewProps {
data: MethodologyReviewType;
}
/**
* 方法学评估详情组件
* 展示3个部分的方法学评估结果
*/
const MethodologyReview = ({ data }: MethodologyReviewProps) => {
if (!data) {
return <Empty description="暂无评估数据" />;
}
// 获取严重程度显示
const getSeverityDisplay = (severity: 'major' | 'minor') => {
const config = {
major: {
color: 'error' as const,
text: '严重',
},
minor: {
color: 'warning' as const,
text: '轻微',
},
};
return config[severity];
};
// 统计数据
const stats = {
totalParts: data.parts.length,
totalIssues: data.parts.reduce((sum, part) => sum + part.issues.length, 0),
majorIssues: data.parts.reduce((sum, part) => {
return sum + part.issues.filter((issue) => issue.severity === 'major').length;
}, 0),
minorIssues: data.parts.reduce((sum, part) => {
return sum + part.issues.filter((issue) => issue.severity === 'minor').length;
}, 0),
};
return (
<Card title={
<Space>
<ExperimentOutlined />
<span></span>
</Space>
}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 总体评分 */}
<ScoreCard
title="总体评分"
score={data.overall_score}
description="方法学综合评分"
size="large"
/>
{/* 总结 */}
<Alert
message="评估总结"
description={data.summary}
type="info"
showIcon
/>
{/* 统计信息 */}
<Card size="small">
<Space size="large">
<div>
<Text type="secondary"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8 }}>{stats.totalParts}</Text>
</div>
<Divider type="vertical" />
<div>
<Text type="secondary"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8 }}>{stats.totalIssues}</Text>
</div>
<Divider type="vertical" />
<div>
<Text type="danger"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8, color: '#f5222d' }}>{stats.majorIssues}</Text>
</div>
<Divider type="vertical" />
<div>
<Text type="warning"></Text>
<Text strong style={{ fontSize: 16, marginLeft: 8, color: '#faad14' }}>{stats.minorIssues}</Text>
</div>
</Space>
</Card>
{/* 详细评估结果3个部分 */}
<Card title="详细评估3个部分" size="small">
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{data.parts.map((part: MethodologyPart, partIndex: number) => (
<Card
key={partIndex}
title={
<Space>
<Text strong style={{ fontSize: 16 }}>{part.part}</Text>
<Tag color="blue">{part.score}</Tag>
<Tag color={part.issues.length === 0 ? 'success' : 'warning'}>
{part.issues.length}
</Tag>
</Space>
}
size="small"
>
{part.issues.length === 0 ? (
<Alert
message="该部分检查通过,未发现问题"
type="success"
showIcon
/>
) : (
<Collapse defaultActiveKey={part.issues.map((_, issueIndex) => issueIndex.toString())}>
{part.issues.map((issue: MethodologyIssue, issueIndex: number) => {
const severityDisplay = getSeverityDisplay(issue.severity);
return (
<Panel
header={
<Space>
<Tag color={severityDisplay.color}>
{severityDisplay.text}
</Tag>
<Text strong>{issue.type}</Text>
</Space>
}
key={issueIndex}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{/* 问题描述 */}
<div>
<Text strong style={{ color: '#f5222d' }}>
<WarningOutlined style={{ marginRight: 8 }} />
</Text>
<Paragraph style={{ marginTop: 8, marginBottom: 0, paddingLeft: 24 }}>
{issue.description}
</Paragraph>
</div>
{/* 位置 */}
<div>
<Text strong style={{ color: '#1890ff' }}>
<InfoCircleOutlined style={{ marginRight: 8 }} />
</Text>
<Paragraph style={{ marginTop: 8, marginBottom: 0, paddingLeft: 24 }}>
{issue.location}
</Paragraph>
</div>
{/* 改进建议 */}
<div>
<Text strong style={{ color: '#52c41a' }}>
</Text>
<Alert
message={issue.suggestion}
type="success"
showIcon
style={{ marginTop: 8 }}
/>
</div>
</Space>
</Panel>
);
})}
</Collapse>
)}
</Card>
))}
</Space>
</Card>
{/* 提示信息 */}
<Alert
message="方法学评估说明"
description={
<Space direction="vertical">
<Text> <strong></strong></Text>
<Text> <strong></strong></Text>
<Text> <strong></strong>使</Text>
</Space>
}
type="info"
showIcon
/>
</Space>
</Card>
);
};
export default MethodologyReview;

View File

@@ -0,0 +1,122 @@
import { Card, Progress, Space, Typography, Tag } from 'antd';
import { TrophyOutlined, CheckCircleOutlined, WarningOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { getScoreLevel } from '../../api/reviewApi';
const { Title, Text } = Typography;
interface ScoreCardProps {
title: string;
score: number;
maxScore?: number;
description?: string;
showProgress?: boolean;
size?: 'small' | 'default' | 'large';
}
/**
* 评分卡片组件
* 用于展示单项评分
*/
const ScoreCard = ({
title,
score,
maxScore = 100,
description,
showProgress = true,
size = 'default',
}: ScoreCardProps) => {
const { level, text, color } = getScoreLevel(score);
// 获取等级图标
const getLevelIcon = () => {
switch (level) {
case 'excellent':
return <TrophyOutlined style={{ color }} />;
case 'good':
return <CheckCircleOutlined style={{ color }} />;
case 'fair':
return <WarningOutlined style={{ color }} />;
case 'poor':
return <CloseCircleOutlined style={{ color }} />;
}
};
// 根据size调整字体大小
const scoreFontSize = size === 'large' ? 56 : size === 'small' ? 32 : 44;
const titleSize = size === 'large' ? 3 : size === 'small' ? 5 : 4;
return (
<Card hoverable>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{/* 标题 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={titleSize as 3 | 4 | 5} style={{ margin: 0 }}>
{title}
</Title>
<Tag icon={getLevelIcon()} color={color} style={{ margin: 0 }}>
{text}
</Tag>
</div>
{/* 分数 */}
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: scoreFontSize, fontWeight: 'bold', color }}>
{score.toFixed(1)}
</div>
<Text type="secondary" style={{ fontSize: size === 'small' ? 12 : 14 }}>
{maxScore}
</Text>
</div>
{/* 进度条 */}
{showProgress && (
<Progress
percent={Math.round((score / maxScore) * 100)}
strokeColor={color}
status="active"
/>
)}
{/* 描述 */}
{description && (
<Text type="secondary" style={{ fontSize: 12 }}>
{description}
</Text>
)}
</Space>
</Card>
);
};
export default ScoreCard;