import React, { useMemo } from 'react';
|
import {
|
Box,
|
Typography,
|
Paper,
|
Table,
|
TableBody,
|
TableCell,
|
TableContainer,
|
TableHead,
|
TableRow,
|
Chip,
|
Stack,
|
} from '@mui/material';
|
|
/**
|
* 汇总报告组件
|
* 参考JMeter的SummaryReport,显示统计信息
|
*/
|
const SummaryReport = ({ results = [] }) => {
|
const statistics = useMemo(() => {
|
if (results.length === 0) {
|
return {
|
total: 0,
|
success: 0,
|
failed: 0,
|
successRate: 0,
|
avgResponseTime: 0,
|
minResponseTime: 0,
|
maxResponseTime: 0,
|
totalTime: 0,
|
};
|
}
|
|
const successfulResults = results.filter(r => r.success);
|
const failedResults = results.filter(r => !r.success);
|
|
const responseTimes = results
|
.map(r => {
|
if (r.startTime && r.endTime) {
|
return new Date(r.endTime) - new Date(r.startTime);
|
}
|
return 0;
|
})
|
.filter(t => t > 0);
|
|
const totalTime = results.length > 0
|
? new Date(results[results.length - 1].endTime || Date.now()) - new Date(results[0].startTime)
|
: 0;
|
|
const avgResponseTime = responseTimes.length > 0
|
? responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length
|
: 0;
|
|
const minResponseTime = responseTimes.length > 0
|
? Math.min(...responseTimes)
|
: 0;
|
|
const maxResponseTime = responseTimes.length > 0
|
? Math.max(...responseTimes)
|
: 0;
|
|
return {
|
total: results.length,
|
success: successfulResults.length,
|
failed: failedResults.length,
|
successRate: results.length > 0 ? (successfulResults.length / results.length * 100) : 0,
|
avgResponseTime: Math.round(avgResponseTime),
|
minResponseTime: Math.round(minResponseTime),
|
maxResponseTime: Math.round(maxResponseTime),
|
totalTime: Math.round(totalTime),
|
throughput: totalTime > 0 ? (results.length / (totalTime / 1000)).toFixed(2) : 0, // 每秒请求数
|
};
|
}, [results]);
|
|
const formatTime = (ms) => {
|
if (ms === 0) return 'N/A';
|
return `${ms}ms`;
|
};
|
|
return (
|
<Box>
|
<Typography variant="h6" gutterBottom>
|
汇总报告
|
</Typography>
|
|
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
|
<Stack direction="row" spacing={2} flexWrap="wrap">
|
<Box>
|
<Typography variant="caption" color="text.secondary">样本数</Typography>
|
<Typography variant="h6">{statistics.total}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">成功</Typography>
|
<Typography variant="h6" color="success.main">{statistics.success}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">失败</Typography>
|
<Typography variant="h6" color="error.main">{statistics.failed}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">成功率</Typography>
|
<Typography variant="h6">
|
{statistics.successRate.toFixed(2)}%
|
</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">平均响应时间</Typography>
|
<Typography variant="h6">{formatTime(statistics.avgResponseTime)}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">最小响应时间</Typography>
|
<Typography variant="h6">{formatTime(statistics.minResponseTime)}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">最大响应时间</Typography>
|
<Typography variant="h6">{formatTime(statistics.maxResponseTime)}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">总耗时</Typography>
|
<Typography variant="h6">{formatTime(statistics.totalTime)}</Typography>
|
</Box>
|
<Box>
|
<Typography variant="caption" color="text.secondary">吞吐量</Typography>
|
<Typography variant="h6">{statistics.throughput} req/s</Typography>
|
</Box>
|
</Stack>
|
</Paper>
|
|
<TableContainer component={Paper} variant="outlined">
|
<Table size="small">
|
<TableHead>
|
<TableRow>
|
<TableCell>步骤名称</TableCell>
|
<TableCell align="right">样本数</TableCell>
|
<TableCell align="right">成功</TableCell>
|
<TableCell align="right">失败</TableCell>
|
<TableCell align="right">成功率</TableCell>
|
<TableCell align="right">平均响应时间</TableCell>
|
<TableCell align="right">最小响应时间</TableCell>
|
<TableCell align="right">最大响应时间</TableCell>
|
</TableRow>
|
</TableHead>
|
<TableBody>
|
{(() => {
|
// 按步骤类型分组统计
|
const groupedResults = {};
|
results.forEach(result => {
|
const key = result.nodeName || result.nodeType || 'unknown';
|
if (!groupedResults[key]) {
|
groupedResults[key] = [];
|
}
|
groupedResults[key].push(result);
|
});
|
|
return Object.keys(groupedResults).map(key => {
|
const groupResults = groupedResults[key];
|
const successCount = groupResults.filter(r => r.success).length;
|
const responseTimes = groupResults
|
.map(r => {
|
if (r.startTime && r.endTime) {
|
return new Date(r.endTime) - new Date(r.startTime);
|
}
|
return 0;
|
})
|
.filter(t => t > 0);
|
|
const avgTime = responseTimes.length > 0
|
? Math.round(responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length)
|
: 0;
|
const minTime = responseTimes.length > 0 ? Math.min(...responseTimes) : 0;
|
const maxTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
|
|
return (
|
<TableRow key={key}>
|
<TableCell>{key}</TableCell>
|
<TableCell align="right">{groupResults.length}</TableCell>
|
<TableCell align="right">
|
<Chip
|
label={successCount}
|
size="small"
|
color="success"
|
variant="outlined"
|
/>
|
</TableCell>
|
<TableCell align="right">
|
<Chip
|
label={groupResults.length - successCount}
|
size="small"
|
color="error"
|
variant="outlined"
|
/>
|
</TableCell>
|
<TableCell align="right">
|
{groupResults.length > 0
|
? ((successCount / groupResults.length) * 100).toFixed(2) + '%'
|
: '0%'}
|
</TableCell>
|
<TableCell align="right">{formatTime(avgTime)}</TableCell>
|
<TableCell align="right">{formatTime(minTime)}</TableCell>
|
<TableCell align="right">{formatTime(maxTime)}</TableCell>
|
</TableRow>
|
);
|
});
|
})()}
|
</TableBody>
|
</Table>
|
</TableContainer>
|
</Box>
|
);
|
};
|
|
export default SummaryReport;
|