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.
Files changed (38) hide show
  1. package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
  2. package/TEST_GUIDE.md +156 -0
  3. package/VERSION.md +57 -3
  4. package/app/cgi-bin/proxy-fetch.js +117 -0
  5. package/app/cgi-bin/settings.js +37 -0
  6. package/app/cgi-bin/stats.js +149 -0
  7. package/app/public/index.html +1 -1
  8. package/app/public/js/index.js +1 -1
  9. package/app/public/js/index.js.LICENSE.txt +25 -2
  10. package/lib/models.js +9 -0
  11. package/lib/routes/interfaces.js +6 -3
  12. package/lib/routes/settings.js +51 -0
  13. package/lib/ruleManager.js +122 -63
  14. package/lib/server.js +4 -0
  15. package/lib/ui-server.js +20 -4
  16. package/package.json +2 -1
  17. package/ui/src/App.css +1 -1
  18. package/ui/src/components/AppLayout.js +317 -80
  19. package/ui/src/components/FeatureCard.js +7 -2
  20. package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
  21. package/ui/src/components/VersionModal.js +4 -41
  22. package/ui/src/components/interface-management/DataManagementDrawer.js +115 -0
  23. package/ui/src/components/interface-management/PreviewModal.js +4 -6
  24. package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
  25. package/ui/src/components/interface-management/ResponseContentEditor.js +124 -165
  26. package/ui/src/components/interface-management/index.js +1 -0
  27. package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
  28. package/ui/src/index.html +5 -2
  29. package/ui/src/index.js +12 -2
  30. package/ui/src/pages/InterfaceManagementV2.js +565 -141
  31. package/ui/src/pages/MockDataV2.js +21 -13
  32. package/ui/src/pages/Settings.js +275 -8
  33. package/ui/src/pages/Sniffer.js +521 -0
  34. package/ui/src/pages/Stats.js +288 -0
  35. package/ui/src/styles/cyber-theme.css +1234 -0
  36. package/ui/src/styles/earth-theme.css +618 -0
  37. package/ui/src/styles/interface-management.css +37 -1
  38. 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;