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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import React, { useState, useEffect } from 'react';
|
|
6
|
+
import { useRef } from 'react';
|
|
6
7
|
import { useHistory } from 'react-router-dom';
|
|
7
8
|
import AppLayout from '../components/AppLayout';
|
|
8
9
|
import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag } from 'antd';
|
|
@@ -57,6 +58,24 @@ const MockData = () => {
|
|
|
57
58
|
fetchFeatures();
|
|
58
59
|
}, []);
|
|
59
60
|
|
|
61
|
+
// 键盘快捷键监听
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const handleSave = () => {
|
|
64
|
+
if (showModal) {
|
|
65
|
+
handleSubmit();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const handleNew = () => {
|
|
69
|
+
openModal();
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener('shortcut-save', handleSave);
|
|
72
|
+
window.addEventListener('shortcut-new-interface', handleNew);
|
|
73
|
+
return () => {
|
|
74
|
+
window.removeEventListener('shortcut-save', handleSave);
|
|
75
|
+
window.removeEventListener('shortcut-new-interface', handleNew);
|
|
76
|
+
};
|
|
77
|
+
}, [showModal]);
|
|
78
|
+
|
|
60
79
|
// 获取所有功能模块
|
|
61
80
|
const fetchFeatures = async () => {
|
|
62
81
|
try {
|
|
@@ -221,7 +240,7 @@ const MockData = () => {
|
|
|
221
240
|
};
|
|
222
241
|
|
|
223
242
|
const viewInterfaces = (feature) => {
|
|
224
|
-
|
|
243
|
+
localStorage.setItem('selectedFeatureId', feature.id);
|
|
225
244
|
history.push(`/interface/${feature.id}`);
|
|
226
245
|
};
|
|
227
246
|
|
|
@@ -566,7 +585,7 @@ const MockData = () => {
|
|
|
566
585
|
width: 100,
|
|
567
586
|
align: 'center',
|
|
568
587
|
render: (count) => (
|
|
569
|
-
<Badge count={count || 0} showZero style={{ backgroundColor: '#
|
|
588
|
+
<Badge count={count || 0} showZero style={{ backgroundColor: '#1677ff' }} />
|
|
570
589
|
),
|
|
571
590
|
},
|
|
572
591
|
{
|
|
@@ -661,17 +680,6 @@ const MockData = () => {
|
|
|
661
680
|
<div className="title-section">
|
|
662
681
|
<div className="title-with-badge">
|
|
663
682
|
<h1 className="page-title">功能模块管理</h1>
|
|
664
|
-
<Badge
|
|
665
|
-
count="V2"
|
|
666
|
-
style={{
|
|
667
|
-
backgroundColor: '#52c41a',
|
|
668
|
-
fontSize: '12px',
|
|
669
|
-
height: '20px',
|
|
670
|
-
lineHeight: '20px',
|
|
671
|
-
borderRadius: '10px',
|
|
672
|
-
marginLeft: '12px'
|
|
673
|
-
}}
|
|
674
|
-
/>
|
|
675
683
|
</div>
|
|
676
684
|
<div className="page-description">
|
|
677
685
|
创建和管理功能模块,为每个功能配置独立的接口
|
package/ui/src/pages/Settings.js
CHANGED
|
@@ -1,11 +1,146 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import AppLayout from '../components/AppLayout';
|
|
3
|
-
import { Card, Typography,
|
|
3
|
+
import { Card, Typography, Switch, Space, Modal, Button, message, Radio } from 'antd';
|
|
4
|
+
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
5
|
+
import axios from 'axios';
|
|
4
6
|
import '../styles/settings.css';
|
|
5
7
|
|
|
6
|
-
const {
|
|
8
|
+
const { Text } = Typography;
|
|
9
|
+
|
|
10
|
+
const syncSettingsToBackend = (patch) => {
|
|
11
|
+
axios.put('/cgi-bin/settings', patch).catch(() => {});
|
|
12
|
+
};
|
|
7
13
|
|
|
8
14
|
const Settings = () => {
|
|
15
|
+
const [displayMode, setDisplayMode] = useState('modal');
|
|
16
|
+
const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
|
|
17
|
+
const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
|
|
18
|
+
const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
|
|
19
|
+
const [currentTheme, setCurrentTheme] = useState('light');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const savedMode = localStorage.getItem('dataManagementDisplayMode');
|
|
23
|
+
if (savedMode) setDisplayMode(savedMode);
|
|
24
|
+
|
|
25
|
+
const savedInterfaceFormMode = localStorage.getItem('interfaceFormDisplayMode');
|
|
26
|
+
if (savedInterfaceFormMode) setInterfaceFormDisplayMode(savedInterfaceFormMode);
|
|
27
|
+
|
|
28
|
+
// 优先从后端读取入参匹配开关,保持前后端一致
|
|
29
|
+
axios.get('/cgi-bin/settings').then(({ data }) => {
|
|
30
|
+
const ipm = data.interfaceParamMatcherEnabled !== false;
|
|
31
|
+
const rpm = data.responseParamMatcherEnabled !== false;
|
|
32
|
+
setInterfaceParamMatcherEnabled(ipm);
|
|
33
|
+
setResponseParamMatcherEnabled(rpm);
|
|
34
|
+
localStorage.setItem('interfaceParamMatcherEnabled', String(ipm));
|
|
35
|
+
localStorage.setItem('responseParamMatcherEnabled', String(rpm));
|
|
36
|
+
window.dispatchEvent(new Event('storage'));
|
|
37
|
+
}).catch(() => {
|
|
38
|
+
const savedInterfaceParamMatcher = localStorage.getItem('interfaceParamMatcherEnabled');
|
|
39
|
+
if (savedInterfaceParamMatcher !== null) setInterfaceParamMatcherEnabled(savedInterfaceParamMatcher !== 'false');
|
|
40
|
+
const savedResponseParamMatcher = localStorage.getItem('responseParamMatcherEnabled');
|
|
41
|
+
if (savedResponseParamMatcher !== null) setResponseParamMatcherEnabled(savedResponseParamMatcher !== 'false');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 读取主题设置(兼容旧版 cyberTheme)
|
|
45
|
+
const savedTheme = localStorage.getItem('theme');
|
|
46
|
+
if (savedTheme) {
|
|
47
|
+
setCurrentTheme(savedTheme);
|
|
48
|
+
} else {
|
|
49
|
+
const oldCyber = localStorage.getItem('cyberTheme');
|
|
50
|
+
const themeValue = oldCyber === 'false' ? 'light' : oldCyber === 'true' ? 'cyber' : 'light';
|
|
51
|
+
setCurrentTheme(themeValue);
|
|
52
|
+
localStorage.setItem('theme', themeValue);
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const handleDisplayModeChange = (checked) => {
|
|
57
|
+
const newMode = checked ? 'drawer' : 'modal';
|
|
58
|
+
setDisplayMode(newMode);
|
|
59
|
+
localStorage.setItem('dataManagementDisplayMode', newMode);
|
|
60
|
+
window.dispatchEvent(new Event('storage'));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleInterfaceFormDisplayModeChange = (checked) => {
|
|
64
|
+
const newMode = checked ? 'drawer' : 'modal';
|
|
65
|
+
setInterfaceFormDisplayMode(newMode);
|
|
66
|
+
localStorage.setItem('interfaceFormDisplayMode', newMode);
|
|
67
|
+
window.dispatchEvent(new Event('storage'));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleInterfaceParamMatcherChange = (checked) => {
|
|
71
|
+
setInterfaceParamMatcherEnabled(checked);
|
|
72
|
+
localStorage.setItem('interfaceParamMatcherEnabled', String(checked));
|
|
73
|
+
syncSettingsToBackend({ interfaceParamMatcherEnabled: checked });
|
|
74
|
+
window.dispatchEvent(new Event('storage'));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleResponseParamMatcherChange = (checked) => {
|
|
78
|
+
setResponseParamMatcherEnabled(checked);
|
|
79
|
+
localStorage.setItem('responseParamMatcherEnabled', String(checked));
|
|
80
|
+
syncSettingsToBackend({ responseParamMatcherEnabled: checked });
|
|
81
|
+
window.dispatchEvent(new Event('storage'));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleThemeChange = (e) => {
|
|
85
|
+
const newTheme = e.target.value;
|
|
86
|
+
setCurrentTheme(newTheme);
|
|
87
|
+
localStorage.setItem('theme', newTheme);
|
|
88
|
+
// 兼容旧版存储
|
|
89
|
+
localStorage.setItem('cyberTheme', String(newTheme === 'cyber'));
|
|
90
|
+
window.dispatchEvent(new Event('storage'));
|
|
91
|
+
const themeNames = { light: '经典亮色', cyber: '赛博朋克', earth: '大地色系' };
|
|
92
|
+
message.success(`${themeNames[newTheme]}主题已开启`);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const showMatcherRuleModal = (type) => {
|
|
96
|
+
if (type === 'interface') {
|
|
97
|
+
Modal.info({
|
|
98
|
+
title: '接口入参匹配规则说明',
|
|
99
|
+
width: 560,
|
|
100
|
+
content: (
|
|
101
|
+
<div>
|
|
102
|
+
<p>用于区分同一 URL 下的不同接口定义(接口级匹配)。</p>
|
|
103
|
+
<p>执行顺序与优先级:</p>
|
|
104
|
+
<ul>
|
|
105
|
+
<li>先进行 URL 匹配,命中多个接口时,按接口匹配条件顺序查找第一条命中规则。</li>
|
|
106
|
+
<li>若命中规则且配置了目标响应,则直接返回该响应(最高优先级)。</li>
|
|
107
|
+
<li>若命中规则但未配置目标响应,则进入“响应入参匹配”阶段继续判断。</li>
|
|
108
|
+
<li>若本开关关闭,则跳过接口级匹配,直接走“响应入参匹配”(若开启)。</li>
|
|
109
|
+
</ul>
|
|
110
|
+
<p>兜底逻辑:</p>
|
|
111
|
+
<ul>
|
|
112
|
+
<li>若最终没有任何入参规则命中,系统返回默认响应。</li>
|
|
113
|
+
<li>支持嵌套参数路径,例如:data.user.id。</li>
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Modal.info({
|
|
122
|
+
title: '响应入参匹配规则说明',
|
|
123
|
+
width: 560,
|
|
124
|
+
content: (
|
|
125
|
+
<div>
|
|
126
|
+
<p>用于在同一接口内,根据请求参数选择具体响应(响应级匹配)。</p>
|
|
127
|
+
<p>执行顺序与优先级:</p>
|
|
128
|
+
<ul>
|
|
129
|
+
<li>仅在“接口入参匹配”未直接确定目标响应时才会执行。</li>
|
|
130
|
+
<li>按响应配置顺序匹配,命中第一条规则即返回对应响应。</li>
|
|
131
|
+
<li>支持精确匹配、包含匹配、正则匹配。</li>
|
|
132
|
+
<li>若本开关关闭,则跳过响应级匹配,直接使用默认响应。</li>
|
|
133
|
+
</ul>
|
|
134
|
+
<p>兜底逻辑:</p>
|
|
135
|
+
<ul>
|
|
136
|
+
<li>开启但未命中任一规则时,使用默认响应。</li>
|
|
137
|
+
<li>与接口级都关闭时,请求将直接返回默认响应。</li>
|
|
138
|
+
</ul>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
9
144
|
return (
|
|
10
145
|
<AppLayout>
|
|
11
146
|
<div className="page-container">
|
|
@@ -19,14 +154,146 @@ const Settings = () => {
|
|
|
19
154
|
</div>
|
|
20
155
|
|
|
21
156
|
<Card title="界面设置" className="settings-card">
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
157
|
+
<div className="settings-item">
|
|
158
|
+
<div className="settings-item-header">
|
|
159
|
+
<div>
|
|
160
|
+
<div className="settings-item-title">数据管理交互</div>
|
|
161
|
+
<div className="settings-item-description">
|
|
162
|
+
选择数据管理的显示方式
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<Space>
|
|
166
|
+
<Text type="secondary">
|
|
167
|
+
{displayMode === 'drawer' ? '抽屉模式' : '弹窗模式'}
|
|
168
|
+
</Text>
|
|
169
|
+
<Switch
|
|
170
|
+
checked={displayMode === 'drawer'}
|
|
171
|
+
onChange={handleDisplayModeChange}
|
|
172
|
+
/>
|
|
173
|
+
</Space>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="settings-item">
|
|
178
|
+
<div className="settings-item-header">
|
|
179
|
+
<div>
|
|
180
|
+
<div className="settings-item-title">接口表单交互</div>
|
|
181
|
+
<div className="settings-item-description">
|
|
182
|
+
选择添加/编辑接口的显示方式
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<Space>
|
|
186
|
+
<Text type="secondary">
|
|
187
|
+
{interfaceFormDisplayMode === 'drawer' ? '抽屉模式' : '弹窗模式'}
|
|
188
|
+
</Text>
|
|
189
|
+
<Switch
|
|
190
|
+
checked={interfaceFormDisplayMode === 'drawer'}
|
|
191
|
+
onChange={handleInterfaceFormDisplayModeChange}
|
|
192
|
+
/>
|
|
193
|
+
</Space>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="settings-item">
|
|
198
|
+
<div className="settings-item-header">
|
|
199
|
+
<div>
|
|
200
|
+
<div className="settings-item-title">
|
|
201
|
+
接口入参匹配
|
|
202
|
+
<Button
|
|
203
|
+
type="text"
|
|
204
|
+
size="small"
|
|
205
|
+
className="settings-help-icon"
|
|
206
|
+
icon={<QuestionCircleOutlined />}
|
|
207
|
+
onClick={() => showMatcherRuleModal('interface')}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="settings-item-description">
|
|
211
|
+
是否在接口编辑表单中显示「接口匹配条件」配置区域
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<Space>
|
|
215
|
+
<Text type="secondary">
|
|
216
|
+
{interfaceParamMatcherEnabled ? '已开启' : '已关闭'}
|
|
217
|
+
</Text>
|
|
218
|
+
<Switch
|
|
219
|
+
checked={interfaceParamMatcherEnabled}
|
|
220
|
+
onChange={handleInterfaceParamMatcherChange}
|
|
221
|
+
/>
|
|
222
|
+
</Space>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="settings-item">
|
|
227
|
+
<div className="settings-item-header">
|
|
228
|
+
<div>
|
|
229
|
+
<div className="settings-item-title">
|
|
230
|
+
响应入参匹配
|
|
231
|
+
<Button
|
|
232
|
+
type="text"
|
|
233
|
+
size="small"
|
|
234
|
+
className="settings-help-icon"
|
|
235
|
+
icon={<QuestionCircleOutlined />}
|
|
236
|
+
onClick={() => showMatcherRuleModal('response')}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
<div className="settings-item-description">
|
|
240
|
+
是否在响应内容编辑器中显示「入参匹配规则」配置区域
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
<Space>
|
|
244
|
+
<Text type="secondary">
|
|
245
|
+
{responseParamMatcherEnabled ? '已开启' : '已关闭'}
|
|
246
|
+
</Text>
|
|
247
|
+
<Switch
|
|
248
|
+
checked={responseParamMatcherEnabled}
|
|
249
|
+
onChange={handleResponseParamMatcherChange}
|
|
250
|
+
/>
|
|
251
|
+
</Space>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</Card>
|
|
255
|
+
|
|
256
|
+
<Card title="主题设置" className="settings-card" style={{ marginTop: 24 }}>
|
|
257
|
+
<div className="settings-item">
|
|
258
|
+
<div className="settings-item-header">
|
|
259
|
+
<div>
|
|
260
|
+
<div className="settings-item-title">界面主题</div>
|
|
261
|
+
<div className="settings-item-description">
|
|
262
|
+
选择插件的界面主题风格
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<Radio.Group
|
|
267
|
+
value={currentTheme}
|
|
268
|
+
onChange={handleThemeChange}
|
|
269
|
+
style={{ marginTop: 12 }}
|
|
270
|
+
>
|
|
271
|
+
<Space direction="vertical">
|
|
272
|
+
<Radio value="light">
|
|
273
|
+
<Space>
|
|
274
|
+
<span style={{ display: 'inline-block', width: 16, height: 16, borderRadius: 4, background: '#1890ff', verticalAlign: 'middle', marginRight: 4 }} />
|
|
275
|
+
经典亮色 — Ant Design 默认亮色风格
|
|
276
|
+
</Space>
|
|
277
|
+
</Radio>
|
|
278
|
+
<Radio value="cyber">
|
|
279
|
+
<Space>
|
|
280
|
+
<span style={{ display: 'inline-block', width: 16, height: 16, borderRadius: 4, background: 'linear-gradient(135deg, #00f0ff, #00ff41)', verticalAlign: 'middle', marginRight: 4 }} />
|
|
281
|
+
赛博朋克 — 深色霓虹科技风格
|
|
282
|
+
</Space>
|
|
283
|
+
</Radio>
|
|
284
|
+
<Radio value="earth">
|
|
285
|
+
<Space>
|
|
286
|
+
<span style={{ display: 'inline-block', width: 16, height: 16, borderRadius: 4, background: 'linear-gradient(135deg, #b87333, #d4a017)', verticalAlign: 'middle', marginRight: 4 }} />
|
|
287
|
+
大地色系 — 暖色调自然舒适风格
|
|
288
|
+
</Space>
|
|
289
|
+
</Radio>
|
|
290
|
+
</Space>
|
|
291
|
+
</Radio.Group>
|
|
292
|
+
</div>
|
|
26
293
|
</Card>
|
|
27
294
|
</div>
|
|
28
295
|
</AppLayout>
|
|
29
296
|
);
|
|
30
297
|
};
|
|
31
298
|
|
|
32
|
-
export default Settings;
|
|
299
|
+
export default Settings;
|