whistle.mock-plugins
Version:
Whistle 插件,用于快速创建 API 模拟数据
289 lines (271 loc) • 9.94 kB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
import AppLayout from '../components/AppLayout';
import {
Card, Row, Col, Statistic, Table, Tag, Spin, Empty, Badge,
DatePicker, Input, Space
} from 'antd';
import {
ApiOutlined, ThunderboltOutlined,
BarChartOutlined, RiseOutlined, SearchOutlined
} from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
const Stats = () => {
const [loading, setLoading] = useState(true);
const [overview, setOverview] = useState({});
const [topData, setTopData] = useState({ topInterfaces: [], topFeatures: [] });
const [timeline, setTimeline] = useState({});
const [recentLogs, setRecentLogs] = useState([]);
const [recentLoading, setRecentLoading] = useState(false);
const [filterDate, setFilterDate] = useState(null);
const [filterUrl, setFilterUrl] = useState('');
const fetchOverview = useCallback(async () => {
const [oRes, tRes, tlRes] = await Promise.all([
axios.get('/cgi-bin/stats?type=overview'),
axios.get('/cgi-bin/stats?type=top'),
axios.get('/cgi-bin/stats?type=timeline'),
]);
setOverview(oRes.data.data || {});
setTopData(tRes.data.data || { topInterfaces: [], topFeatures: [] });
setTimeline(tlRes.data.data?.timeline || {});
}, []);
const fetchRecent = useCallback(async (date, url) => {
setRecentLoading(true);
try {
const params = new URLSearchParams({ type: 'recent' });
if (date) params.append('date', date);
if (url) params.append('url', url);
const res = await axios.get(`/cgi-bin/stats?${params.toString()}`);
setRecentLogs(res.data.data?.logs || []);
} catch (e) {
console.error('加载最近记录失败:', e);
} finally {
setRecentLoading(false);
}
}, []);
const fetchAll = async () => {
setLoading(true);
try {
await fetchOverview();
const dateStr = filterDate ? dayjs(filterDate).format('YYYY-MM-DD') : '';
await fetchRecent(dateStr, filterUrl);
} catch (e) {
console.error('加载统计数据失败:', e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAll();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleDateChange = (date) => {
setFilterDate(date);
const dateStr = date ? dayjs(date).format('YYYY-MM-DD') : '';
fetchRecent(dateStr, filterUrl);
};
const handleUrlChange = (e) => {
const value = e.target.value;
setFilterUrl(value);
const dateStr = filterDate ? dayjs(filterDate).format('YYYY-MM-DD') : '';
fetchRecent(dateStr, value);
};
const hitColumns = [
{ title: '接口名称', dataIndex: 'name', key: 'name' },
{ title: '命中次数', dataIndex: 'count', key: 'count', width: 120 },
{
title: '占比',
key: 'percent',
width: 120,
render: (_, record) => {
const total = overview.totalHits || 1;
const pct = ((record.count / total) * 100).toFixed(1);
return <Badge count={`${pct}%`} style={{ backgroundColor: '#52c41a' }} />;
},
},
];
const featureColumns = [
{ title: '模块名称', dataIndex: 'name', key: 'name' },
{ title: '命中次数', dataIndex: 'count', key: 'count', width: 120 },
];
const recentColumns = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: 180,
render: (t) => new Date(t).toLocaleString(),
},
{ title: '方法', dataIndex: 'method', key: 'method', width: 80 },
{ title: 'URL', dataIndex: 'url', key: 'url', ellipsis: true },
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
render: (s) => <Tag color={s >= 200 && s < 300 ? 'success' : 'default'}>{s}</Tag>,
},
{ title: '模块', dataIndex: 'featureName', key: 'featureName', width: 140 },
{ title: '接口', dataIndex: 'interfaceName', key: 'interfaceName', width: 160 },
];
const timelineEntries = Object.entries(timeline).sort();
const maxTimeline = Math.max(...Object.values(timeline), 1);
return (
<AppLayout>
<div className="page-container">
<div className="page-title-bar">
<div>
<h1 className="page-title">📊 请求统计</h1>
<div className="page-description">
查看 Mock 接口的命中情况、流量趋势和未覆盖接口
</div>
</div>
</div>
{loading ? (
<Spin size="large" style={{ display: 'block', margin: '80px auto' }} />
) : (
<>
{/* 概览卡片 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="今日命中"
value={overview.todayHits || 0}
prefix={<ThunderboltOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="7日总请求"
value={overview.totalRequests || 0}
prefix={<BarChartOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="命中率"
value={overview.hitRate || 0}
suffix="%"
prefix={<RiseOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="活跃接口 / 总数"
value={`${overview.activeInterfaces || 0} / ${overview.totalInterfaces || 0}`}
prefix={<ApiOutlined />}
/>
</Card>
</Col>
</Row>
{/* 趋势图(简单柱状图) */}
<Card title="📈 近7天命中趋势" style={{ marginBottom: 24 }}>
{timelineEntries.length === 0 ? (
<Empty description="暂无数据" />
) : (
<div style={{ display: 'flex', alignItems: 'flex-end', height: 160, gap: 12, padding: '0 8px' }}>
{timelineEntries.map(([date, count]) => (
<div key={date} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{count}</div>
<div
style={{
width: '100%',
height: `${(count / maxTimeline) * 120}px`,
background: '#1890ff',
borderRadius: '4px 4px 0 0',
minHeight: count > 0 ? 4 : 0,
transition: 'height 0.3s',
}}
/>
<div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>{date.slice(5)}</div>
</div>
))}
</div>
)}
</Card>
{/* Top 命中接口 & 功能模块 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={12}>
<Card title="🏆 Top 10 命中接口">
{topData.topInterfaces.length === 0 ? (
<Empty description="暂无命中记录" />
) : (
<Table
dataSource={topData.topInterfaces}
columns={hitColumns}
rowKey="id"
pagination={false}
size="small"
/>
)}
</Card>
</Col>
<Col span={12}>
<Card title="📦 Top 10 功能模块">
{topData.topFeatures.length === 0 ? (
<Empty description="暂无命中记录" />
) : (
<Table
dataSource={topData.topFeatures}
columns={featureColumns}
rowKey="id"
pagination={false}
size="small"
/>
)}
</Card>
</Col>
</Row>
{/* 最近命中 */}
<Card
title="🕐 最近命中记录"
extra={
<Space>
<DatePicker
placeholder="筛选日期"
value={filterDate}
onChange={handleDateChange}
allowClear
style={{ width: 140 }}
/>
<Input
placeholder="筛选 URL"
value={filterUrl}
onChange={handleUrlChange}
allowClear
prefix={<SearchOutlined />}
style={{ width: 200 }}
/>
</Space>
}
>
{recentLoading ? (
<Spin size="small" style={{ display: 'block', margin: '40px auto' }} />
) : recentLogs.length === 0 ? (
<Empty description="暂无记录" />
) : (
<Table
dataSource={recentLogs}
columns={recentColumns}
rowKey="id"
pagination={false}
size="small"
/>
)}
</Card>
</>
)}
</div>
</AppLayout>
);
};
export default Stats;