Features: - Backend statistics API (cloud-native Prisma aggregation) - Results page with hybrid solution (AI consensus + human final decision) - Excel export (frontend generation, zero disk write, cloud-native) - PRISMA-style exclusion reason analysis with bar chart - Batch selection and export (3 export methods) - Fixed logic contradiction (inclusion does not show exclusion reason) - Optimized table width (870px, no horizontal scroll) Components: - Backend: screeningController.ts - add getProjectStatistics API - Frontend: ScreeningResults.tsx - complete results page (hybrid solution) - Frontend: excelExport.ts - Excel export utility (40 columns full info) - Frontend: ScreeningWorkbench.tsx - add navigation button - Utils: get-test-projects.mjs - quick test tool Architecture: - Cloud-native: backend aggregation reduces network transfer - Cloud-native: frontend Excel generation (zero file persistence) - Reuse platform: global prisma instance, logger - Performance: statistics API < 500ms, Excel export < 3s (1000 records) Documentation: - Update module status guide (add Week 4 features) - Update task breakdown (mark Week 4 completed) - Update API design spec (add statistics API) - Update database design (add field usage notes) - Create Week 4 development plan - Create Week 4 completion report - Create technical debt list Test: - End-to-end flow test passed - All features verified - Performance test passed - Cloud-native compliance verified Ref: Week 4 Development Plan Scope: ASL Module MVP - Title Abstract Screening Results Cloud-Native: Backend aggregation + Frontend Excel generation
132 lines
2.5 KiB
TypeScript
132 lines
2.5 KiB
TypeScript
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|