whistle.mock-plugins 1.2.2-beta.8 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
- package/TEST_GUIDE.md +156 -0
- package/VERSION.md +57 -3
- package/app/cgi-bin/proxy-fetch.js +117 -0
- package/app/cgi-bin/settings.js +37 -0
- package/app/cgi-bin/stats.js +149 -0
- package/app/public/index.html +1 -1
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +25 -2
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +122 -63
- package/lib/server.js +4 -0
- package/lib/ui-server.js +20 -4
- package/package.json +2 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +317 -80
- package/ui/src/components/FeatureCard.js +7 -2
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
- package/ui/src/components/VersionModal.js +4 -41
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
- package/ui/src/components/interface-management/PreviewModal.js +4 -6
- package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +124 -165
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +12 -2
- package/ui/src/pages/InterfaceManagementV2.js +565 -141
- package/ui/src/pages/MockDataV2.js +21 -13
- package/ui/src/pages/Settings.js +275 -8
- package/ui/src/pages/Sniffer.js +521 -0
- package/ui/src/pages/Stats.js +288 -0
- package/ui/src/styles/cyber-theme.css +1234 -0
- package/ui/src/styles/earth-theme.css +618 -0
- package/ui/src/styles/interface-management.css +37 -1
- package/ui/src/styles/settings.css +45 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import AppLayout from '../components/AppLayout';
|
|
3
|
+
import {
|
|
4
|
+
Card, Row, Col, Statistic, Table, Tag, Spin, Empty, Badge,
|
|
5
|
+
DatePicker, Input, Space
|
|
6
|
+
} from 'antd';
|
|
7
|
+
import {
|
|
8
|
+
ApiOutlined, ThunderboltOutlined,
|
|
9
|
+
BarChartOutlined, RiseOutlined, SearchOutlined
|
|
10
|
+
} from '@ant-design/icons';
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import dayjs from 'dayjs';
|
|
13
|
+
|
|
14
|
+
const Stats = () => {
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [overview, setOverview] = useState({});
|
|
17
|
+
const [topData, setTopData] = useState({ topInterfaces: [], topFeatures: [] });
|
|
18
|
+
const [timeline, setTimeline] = useState({});
|
|
19
|
+
const [recentLogs, setRecentLogs] = useState([]);
|
|
20
|
+
const [recentLoading, setRecentLoading] = useState(false);
|
|
21
|
+
const [filterDate, setFilterDate] = useState(null);
|
|
22
|
+
const [filterUrl, setFilterUrl] = useState('');
|
|
23
|
+
|
|
24
|
+
const fetchOverview = useCallback(async () => {
|
|
25
|
+
const [oRes, tRes, tlRes] = await Promise.all([
|
|
26
|
+
axios.get('/cgi-bin/stats?type=overview'),
|
|
27
|
+
axios.get('/cgi-bin/stats?type=top'),
|
|
28
|
+
axios.get('/cgi-bin/stats?type=timeline'),
|
|
29
|
+
]);
|
|
30
|
+
setOverview(oRes.data.data || {});
|
|
31
|
+
setTopData(tRes.data.data || { topInterfaces: [], topFeatures: [] });
|
|
32
|
+
setTimeline(tlRes.data.data?.timeline || {});
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const fetchRecent = useCallback(async (date, url) => {
|
|
36
|
+
setRecentLoading(true);
|
|
37
|
+
try {
|
|
38
|
+
const params = new URLSearchParams({ type: 'recent' });
|
|
39
|
+
if (date) params.append('date', date);
|
|
40
|
+
if (url) params.append('url', url);
|
|
41
|
+
const res = await axios.get(`/cgi-bin/stats?${params.toString()}`);
|
|
42
|
+
setRecentLogs(res.data.data?.logs || []);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('加载最近记录失败:', e);
|
|
45
|
+
} finally {
|
|
46
|
+
setRecentLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const fetchAll = async () => {
|
|
51
|
+
setLoading(true);
|
|
52
|
+
try {
|
|
53
|
+
await fetchOverview();
|
|
54
|
+
const dateStr = filterDate ? dayjs(filterDate).format('YYYY-MM-DD') : '';
|
|
55
|
+
await fetchRecent(dateStr, filterUrl);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error('加载统计数据失败:', e);
|
|
58
|
+
} finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
fetchAll();
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const handleDateChange = (date) => {
|
|
69
|
+
setFilterDate(date);
|
|
70
|
+
const dateStr = date ? dayjs(date).format('YYYY-MM-DD') : '';
|
|
71
|
+
fetchRecent(dateStr, filterUrl);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleUrlChange = (e) => {
|
|
75
|
+
const value = e.target.value;
|
|
76
|
+
setFilterUrl(value);
|
|
77
|
+
const dateStr = filterDate ? dayjs(filterDate).format('YYYY-MM-DD') : '';
|
|
78
|
+
fetchRecent(dateStr, value);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const hitColumns = [
|
|
82
|
+
{ title: '接口名称', dataIndex: 'name', key: 'name' },
|
|
83
|
+
{ title: '命中次数', dataIndex: 'count', key: 'count', width: 120 },
|
|
84
|
+
{
|
|
85
|
+
title: '占比',
|
|
86
|
+
key: 'percent',
|
|
87
|
+
width: 120,
|
|
88
|
+
render: (_, record) => {
|
|
89
|
+
const total = overview.totalHits || 1;
|
|
90
|
+
const pct = ((record.count / total) * 100).toFixed(1);
|
|
91
|
+
return <Badge count={`${pct}%`} style={{ backgroundColor: '#52c41a' }} />;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const featureColumns = [
|
|
97
|
+
{ title: '模块名称', dataIndex: 'name', key: 'name' },
|
|
98
|
+
{ title: '命中次数', dataIndex: 'count', key: 'count', width: 120 },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const recentColumns = [
|
|
102
|
+
{
|
|
103
|
+
title: '时间',
|
|
104
|
+
dataIndex: 'timestamp',
|
|
105
|
+
key: 'timestamp',
|
|
106
|
+
width: 180,
|
|
107
|
+
render: (t) => new Date(t).toLocaleString(),
|
|
108
|
+
},
|
|
109
|
+
{ title: '方法', dataIndex: 'method', key: 'method', width: 80 },
|
|
110
|
+
{ title: 'URL', dataIndex: 'url', key: 'url', ellipsis: true },
|
|
111
|
+
{
|
|
112
|
+
title: '状态',
|
|
113
|
+
dataIndex: 'status',
|
|
114
|
+
key: 'status',
|
|
115
|
+
width: 80,
|
|
116
|
+
render: (s) => <Tag color={s >= 200 && s < 300 ? 'success' : 'default'}>{s}</Tag>,
|
|
117
|
+
},
|
|
118
|
+
{ title: '模块', dataIndex: 'featureName', key: 'featureName', width: 140 },
|
|
119
|
+
{ title: '接口', dataIndex: 'interfaceName', key: 'interfaceName', width: 160 },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const timelineEntries = Object.entries(timeline).sort();
|
|
123
|
+
const maxTimeline = Math.max(...Object.values(timeline), 1);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<AppLayout>
|
|
127
|
+
<div className="page-container">
|
|
128
|
+
<div className="page-title-bar">
|
|
129
|
+
<div>
|
|
130
|
+
<h1 className="page-title">📊 请求统计</h1>
|
|
131
|
+
<div className="page-description">
|
|
132
|
+
查看 Mock 接口的命中情况、流量趋势和未覆盖接口
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{loading ? (
|
|
138
|
+
<Spin size="large" style={{ display: 'block', margin: '80px auto' }} />
|
|
139
|
+
) : (
|
|
140
|
+
<>
|
|
141
|
+
{/* 概览卡片 */}
|
|
142
|
+
<Row gutter={16} style={{ marginBottom: 24 }}>
|
|
143
|
+
<Col span={6}>
|
|
144
|
+
<Card>
|
|
145
|
+
<Statistic
|
|
146
|
+
title="今日命中"
|
|
147
|
+
value={overview.todayHits || 0}
|
|
148
|
+
prefix={<ThunderboltOutlined />}
|
|
149
|
+
valueStyle={{ color: '#52c41a' }}
|
|
150
|
+
/>
|
|
151
|
+
</Card>
|
|
152
|
+
</Col>
|
|
153
|
+
<Col span={6}>
|
|
154
|
+
<Card>
|
|
155
|
+
<Statistic
|
|
156
|
+
title="7日总请求"
|
|
157
|
+
value={overview.totalRequests || 0}
|
|
158
|
+
prefix={<BarChartOutlined />}
|
|
159
|
+
/>
|
|
160
|
+
</Card>
|
|
161
|
+
</Col>
|
|
162
|
+
<Col span={6}>
|
|
163
|
+
<Card>
|
|
164
|
+
<Statistic
|
|
165
|
+
title="命中率"
|
|
166
|
+
value={overview.hitRate || 0}
|
|
167
|
+
suffix="%"
|
|
168
|
+
prefix={<RiseOutlined />}
|
|
169
|
+
valueStyle={{ color: '#1890ff' }}
|
|
170
|
+
/>
|
|
171
|
+
</Card>
|
|
172
|
+
</Col>
|
|
173
|
+
<Col span={6}>
|
|
174
|
+
<Card>
|
|
175
|
+
<Statistic
|
|
176
|
+
title="活跃接口 / 总数"
|
|
177
|
+
value={`${overview.activeInterfaces || 0} / ${overview.totalInterfaces || 0}`}
|
|
178
|
+
prefix={<ApiOutlined />}
|
|
179
|
+
/>
|
|
180
|
+
</Card>
|
|
181
|
+
</Col>
|
|
182
|
+
</Row>
|
|
183
|
+
|
|
184
|
+
{/* 趋势图(简单柱状图) */}
|
|
185
|
+
<Card title="📈 近7天命中趋势" style={{ marginBottom: 24 }}>
|
|
186
|
+
{timelineEntries.length === 0 ? (
|
|
187
|
+
<Empty description="暂无数据" />
|
|
188
|
+
) : (
|
|
189
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', height: 160, gap: 12, padding: '0 8px' }}>
|
|
190
|
+
{timelineEntries.map(([date, count]) => (
|
|
191
|
+
<div key={date} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
192
|
+
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{count}</div>
|
|
193
|
+
<div
|
|
194
|
+
style={{
|
|
195
|
+
width: '100%',
|
|
196
|
+
height: `${(count / maxTimeline) * 120}px`,
|
|
197
|
+
background: '#1890ff',
|
|
198
|
+
borderRadius: '4px 4px 0 0',
|
|
199
|
+
minHeight: count > 0 ? 4 : 0,
|
|
200
|
+
transition: 'height 0.3s',
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
<div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>{date.slice(5)}</div>
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</Card>
|
|
209
|
+
|
|
210
|
+
{/* Top 命中接口 & 功能模块 */}
|
|
211
|
+
<Row gutter={16} style={{ marginBottom: 24 }}>
|
|
212
|
+
<Col span={12}>
|
|
213
|
+
<Card title="🏆 Top 10 命中接口">
|
|
214
|
+
{topData.topInterfaces.length === 0 ? (
|
|
215
|
+
<Empty description="暂无命中记录" />
|
|
216
|
+
) : (
|
|
217
|
+
<Table
|
|
218
|
+
dataSource={topData.topInterfaces}
|
|
219
|
+
columns={hitColumns}
|
|
220
|
+
rowKey="id"
|
|
221
|
+
pagination={false}
|
|
222
|
+
size="small"
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</Card>
|
|
226
|
+
</Col>
|
|
227
|
+
<Col span={12}>
|
|
228
|
+
<Card title="📦 Top 10 功能模块">
|
|
229
|
+
{topData.topFeatures.length === 0 ? (
|
|
230
|
+
<Empty description="暂无命中记录" />
|
|
231
|
+
) : (
|
|
232
|
+
<Table
|
|
233
|
+
dataSource={topData.topFeatures}
|
|
234
|
+
columns={featureColumns}
|
|
235
|
+
rowKey="id"
|
|
236
|
+
pagination={false}
|
|
237
|
+
size="small"
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</Card>
|
|
241
|
+
</Col>
|
|
242
|
+
</Row>
|
|
243
|
+
|
|
244
|
+
{/* 最近命中 */}
|
|
245
|
+
<Card
|
|
246
|
+
title="🕐 最近命中记录"
|
|
247
|
+
extra={
|
|
248
|
+
<Space>
|
|
249
|
+
<DatePicker
|
|
250
|
+
placeholder="筛选日期"
|
|
251
|
+
value={filterDate}
|
|
252
|
+
onChange={handleDateChange}
|
|
253
|
+
allowClear
|
|
254
|
+
style={{ width: 140 }}
|
|
255
|
+
/>
|
|
256
|
+
<Input
|
|
257
|
+
placeholder="筛选 URL"
|
|
258
|
+
value={filterUrl}
|
|
259
|
+
onChange={handleUrlChange}
|
|
260
|
+
allowClear
|
|
261
|
+
prefix={<SearchOutlined />}
|
|
262
|
+
style={{ width: 200 }}
|
|
263
|
+
/>
|
|
264
|
+
</Space>
|
|
265
|
+
}
|
|
266
|
+
>
|
|
267
|
+
{recentLoading ? (
|
|
268
|
+
<Spin size="small" style={{ display: 'block', margin: '40px auto' }} />
|
|
269
|
+
) : recentLogs.length === 0 ? (
|
|
270
|
+
<Empty description="暂无记录" />
|
|
271
|
+
) : (
|
|
272
|
+
<Table
|
|
273
|
+
dataSource={recentLogs}
|
|
274
|
+
columns={recentColumns}
|
|
275
|
+
rowKey="id"
|
|
276
|
+
pagination={false}
|
|
277
|
+
size="small"
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
</Card>
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</AppLayout>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export default Stats;
|