385 lines
12 KiB
TypeScript
385 lines
12 KiB
TypeScript
// src/components/PerformancePanel.tsx
|
|
// 性能监控可视化面板 - 仅开发环境显示
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
VStack,
|
|
HStack,
|
|
Text,
|
|
Badge,
|
|
Button,
|
|
IconButton,
|
|
Stat,
|
|
StatLabel,
|
|
StatNumber,
|
|
StatHelpText,
|
|
Accordion,
|
|
AccordionItem,
|
|
AccordionButton,
|
|
AccordionPanel,
|
|
AccordionIcon,
|
|
useDisclosure,
|
|
Drawer,
|
|
DrawerBody,
|
|
DrawerHeader,
|
|
DrawerOverlay,
|
|
DrawerContent,
|
|
DrawerCloseButton,
|
|
} from '@chakra-ui/react';
|
|
import { MdSpeed, MdClose, MdRefresh, MdFileDownload } from 'react-icons/md';
|
|
import { performanceMonitor } from '@/utils/performanceMonitor';
|
|
|
|
/**
|
|
* 性能评分颜色映射
|
|
*/
|
|
const getScoreColor = (score: string): string => {
|
|
switch (score) {
|
|
case 'excellent':
|
|
return 'green';
|
|
case 'good':
|
|
return 'blue';
|
|
case 'needs improvement':
|
|
return 'yellow';
|
|
case 'poor':
|
|
return 'red';
|
|
default:
|
|
return 'gray';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 格式化毫秒数
|
|
*/
|
|
const formatMs = (ms: number | undefined): string => {
|
|
if (ms === undefined) return 'N/A';
|
|
return `${ms.toFixed(0)}ms`;
|
|
};
|
|
|
|
/**
|
|
* 性能面板组件
|
|
*/
|
|
export const PerformancePanel: React.FC = () => {
|
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
const [report, setReport] = useState<any>(null);
|
|
|
|
// 刷新性能数据
|
|
const refreshData = () => {
|
|
const newReport = performanceMonitor.getReport();
|
|
setReport(newReport);
|
|
};
|
|
|
|
// 导出 JSON
|
|
const exportJSON = () => {
|
|
const json = performanceMonitor.exportJSON();
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `performance-report-${Date.now()}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
// 初始加载
|
|
useEffect(() => {
|
|
refreshData();
|
|
}, []);
|
|
|
|
// 仅在开发环境显示
|
|
if (process.env.NODE_ENV !== 'development') {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* 浮动按钮 */}
|
|
<IconButton
|
|
aria-label="Open performance panel"
|
|
icon={<MdSpeed />}
|
|
position="fixed"
|
|
bottom="100px"
|
|
right="20px"
|
|
colorScheme="blue"
|
|
size="lg"
|
|
borderRadius="full"
|
|
boxShadow="lg"
|
|
zIndex={9999}
|
|
onClick={onOpen}
|
|
/>
|
|
|
|
{/* 抽屉面板 */}
|
|
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="lg">
|
|
<DrawerOverlay />
|
|
<DrawerContent>
|
|
<DrawerCloseButton />
|
|
<DrawerHeader borderBottomWidth="1px">
|
|
<HStack>
|
|
<MdSpeed size={24} />
|
|
<Text>性能监控面板</Text>
|
|
</HStack>
|
|
</DrawerHeader>
|
|
|
|
<DrawerBody>
|
|
{report ? (
|
|
<VStack spacing={4} align="stretch" py={4}>
|
|
{/* 操作按钮 */}
|
|
<HStack spacing={2}>
|
|
<Button
|
|
leftIcon={<MdRefresh />}
|
|
size="sm"
|
|
colorScheme="blue"
|
|
onClick={refreshData}
|
|
>
|
|
刷新数据
|
|
</Button>
|
|
<Button
|
|
leftIcon={<MdFileDownload />}
|
|
size="sm"
|
|
colorScheme="green"
|
|
onClick={exportJSON}
|
|
>
|
|
导出 JSON
|
|
</Button>
|
|
</HStack>
|
|
|
|
{/* 总览 */}
|
|
<Box p={4} bg="gray.50" borderRadius="md">
|
|
<VStack align="stretch" spacing={2}>
|
|
<HStack justify="space-between">
|
|
<Text fontWeight="bold">性能评分</Text>
|
|
<Badge
|
|
colorScheme={getScoreColor(report.summary.performanceScore)}
|
|
fontSize="md"
|
|
px={3}
|
|
py={1}
|
|
borderRadius="full"
|
|
>
|
|
{report.summary.performanceScore.toUpperCase()}
|
|
</Badge>
|
|
</HStack>
|
|
<HStack justify="space-between">
|
|
<Text fontSize="sm" color="gray.600">
|
|
性能标记: {report.summary.totalMarks}
|
|
</Text>
|
|
<Text fontSize="sm" color="gray.600">
|
|
性能测量: {report.summary.totalMeasures}
|
|
</Text>
|
|
</HStack>
|
|
</VStack>
|
|
</Box>
|
|
|
|
{/* 网络指标 */}
|
|
<Box>
|
|
<Text fontWeight="bold" mb={2}>
|
|
网络指标
|
|
</Text>
|
|
<VStack align="stretch" spacing={2}>
|
|
<MetricStat
|
|
label="DNS 查询"
|
|
value={formatMs(report.metrics.dns)}
|
|
threshold={100}
|
|
actualValue={report.metrics.dns}
|
|
/>
|
|
<MetricStat
|
|
label="TCP 连接"
|
|
value={formatMs(report.metrics.tcp)}
|
|
threshold={100}
|
|
actualValue={report.metrics.tcp}
|
|
/>
|
|
<MetricStat
|
|
label="TTFB"
|
|
value={formatMs(report.metrics.ttfb)}
|
|
threshold={500}
|
|
actualValue={report.metrics.ttfb}
|
|
/>
|
|
</VStack>
|
|
</Box>
|
|
|
|
{/* 渲染指标 */}
|
|
<Box>
|
|
<Text fontWeight="bold" mb={2}>
|
|
渲染指标
|
|
</Text>
|
|
<VStack align="stretch" spacing={2}>
|
|
<MetricStat
|
|
label="FP (首次绘制)"
|
|
value={formatMs(report.metrics.fp)}
|
|
threshold={1000}
|
|
actualValue={report.metrics.fp}
|
|
/>
|
|
<MetricStat
|
|
label="FCP (首次内容绘制)"
|
|
value={formatMs(report.metrics.fcp)}
|
|
threshold={1800}
|
|
actualValue={report.metrics.fcp}
|
|
/>
|
|
<MetricStat
|
|
label="LCP (最大内容绘制)"
|
|
value={formatMs(report.metrics.lcp)}
|
|
threshold={2500}
|
|
actualValue={report.metrics.lcp}
|
|
/>
|
|
</VStack>
|
|
</Box>
|
|
|
|
{/* React 指标 */}
|
|
<Box>
|
|
<Text fontWeight="bold" mb={2}>
|
|
React 指标
|
|
</Text>
|
|
<VStack align="stretch" spacing={2}>
|
|
<MetricStat
|
|
label="React 初始化"
|
|
value={formatMs(report.metrics.reactInit)}
|
|
threshold={1000}
|
|
actualValue={report.metrics.reactInit}
|
|
/>
|
|
<MetricStat
|
|
label="认证检查"
|
|
value={formatMs(report.metrics.authCheck)}
|
|
threshold={300}
|
|
actualValue={report.metrics.authCheck}
|
|
/>
|
|
<MetricStat
|
|
label="首页渲染"
|
|
value={formatMs(report.metrics.homepageRender)}
|
|
threshold={500}
|
|
actualValue={report.metrics.homepageRender}
|
|
/>
|
|
</VStack>
|
|
</Box>
|
|
|
|
{/* 总白屏时间 */}
|
|
<Box p={4} bg="blue.50" borderRadius="md" borderWidth="2px" borderColor="blue.200">
|
|
<Stat>
|
|
<StatLabel>总白屏时间</StatLabel>
|
|
<StatNumber fontSize="3xl">
|
|
{formatMs(report.metrics.totalWhiteScreen)}
|
|
</StatNumber>
|
|
<StatHelpText>
|
|
{report.metrics.totalWhiteScreen && report.metrics.totalWhiteScreen < 1500
|
|
? '✅ 优秀'
|
|
: report.metrics.totalWhiteScreen && report.metrics.totalWhiteScreen < 2000
|
|
? '⚠️ 良好'
|
|
: '❌ 需要优化'}
|
|
</StatHelpText>
|
|
</Stat>
|
|
</Box>
|
|
|
|
{/* 优化建议 */}
|
|
<Accordion allowToggle>
|
|
<AccordionItem>
|
|
<h2>
|
|
<AccordionButton>
|
|
<Box flex="1" textAlign="left" fontWeight="bold">
|
|
优化建议 ({report.recommendations.length})
|
|
</Box>
|
|
<AccordionIcon />
|
|
</AccordionButton>
|
|
</h2>
|
|
<AccordionPanel pb={4}>
|
|
<VStack align="stretch" spacing={2}>
|
|
{report.recommendations.map((rec: string, index: number) => (
|
|
<Text key={index} fontSize="sm">
|
|
{rec}
|
|
</Text>
|
|
))}
|
|
</VStack>
|
|
</AccordionPanel>
|
|
</AccordionItem>
|
|
|
|
{/* 性能标记 */}
|
|
<AccordionItem>
|
|
<h2>
|
|
<AccordionButton>
|
|
<Box flex="1" textAlign="left" fontWeight="bold">
|
|
性能标记 ({report.marks.length})
|
|
</Box>
|
|
<AccordionIcon />
|
|
</AccordionButton>
|
|
</h2>
|
|
<AccordionPanel pb={4}>
|
|
<VStack align="stretch" spacing={1}>
|
|
{report.marks.map((mark: any, index: number) => (
|
|
<HStack key={index} justify="space-between" fontSize="sm">
|
|
<Text>{mark.name}</Text>
|
|
<Text color="gray.600">{mark.time.toFixed(2)}ms</Text>
|
|
</HStack>
|
|
))}
|
|
</VStack>
|
|
</AccordionPanel>
|
|
</AccordionItem>
|
|
|
|
{/* 性能测量 */}
|
|
<AccordionItem>
|
|
<h2>
|
|
<AccordionButton>
|
|
<Box flex="1" textAlign="left" fontWeight="bold">
|
|
性能测量 ({report.measures.length})
|
|
</Box>
|
|
<AccordionIcon />
|
|
</AccordionButton>
|
|
</h2>
|
|
<AccordionPanel pb={4}>
|
|
<VStack align="stretch" spacing={2}>
|
|
{report.measures.map((measure: any, index: number) => (
|
|
<Box key={index} p={2} bg="gray.50" borderRadius="md">
|
|
<HStack justify="space-between">
|
|
<Text fontWeight="semibold" fontSize="sm">
|
|
{measure.name}
|
|
</Text>
|
|
<Badge>{measure.duration.toFixed(2)}ms</Badge>
|
|
</HStack>
|
|
<Text fontSize="xs" color="gray.600">
|
|
{measure.startMark} → {measure.endMark}
|
|
</Text>
|
|
</Box>
|
|
))}
|
|
</VStack>
|
|
</AccordionPanel>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</VStack>
|
|
) : (
|
|
<Text>加载中...</Text>
|
|
)}
|
|
</DrawerBody>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
</>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 指标统计组件
|
|
*/
|
|
interface MetricStatProps {
|
|
label: string;
|
|
value: string;
|
|
threshold: number;
|
|
actualValue?: number;
|
|
}
|
|
|
|
const MetricStat: React.FC<MetricStatProps> = ({ label, value, threshold, actualValue }) => {
|
|
const isGood = actualValue !== undefined && actualValue < threshold;
|
|
|
|
return (
|
|
<HStack justify="space-between" p={2} bg="gray.50" borderRadius="md">
|
|
<Text fontSize="sm">{label}</Text>
|
|
<HStack>
|
|
<Text fontSize="sm" fontWeight="bold">
|
|
{value}
|
|
</Text>
|
|
{actualValue !== undefined && (
|
|
<Text fontSize="xs">{isGood ? '✅' : '⚠️'}</Text>
|
|
)}
|
|
</HStack>
|
|
</HStack>
|
|
);
|
|
};
|
|
|
|
export default PerformancePanel;
|