whistle.mock-plugins 1.0.6 → 1.1.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.
@@ -0,0 +1,427 @@
1
+ import React, { useState } from 'react';
2
+ import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, AutoComplete } from 'antd';
3
+ import {
4
+ CodeOutlined,
5
+ EyeOutlined,
6
+ FormatPainterOutlined,
7
+ FileTextOutlined,
8
+ PlusCircleOutlined,
9
+ DeleteOutlined,
10
+ CheckCircleOutlined,
11
+ EditOutlined
12
+ } from '@ant-design/icons';
13
+ import { generateResponseId } from './utils';
14
+
15
+ const { TextArea } = Input;
16
+ const { Option } = Select;
17
+
18
+ const ResponseContentEditor = ({
19
+ form,
20
+ responses,
21
+ activeResponseId,
22
+ onPreview
23
+ }) => {
24
+ const { getFieldValue, setFieldsValue } = form;
25
+ const [isEditing, setIsEditing] = useState(false);
26
+ const [editingName, setEditingName] = useState('');
27
+
28
+ // 查找激活的响应
29
+ const activeResponseIndex = responses.findIndex(r => r.id === activeResponseId);
30
+ const activeResponse = activeResponseIndex >= 0 ? responses[activeResponseIndex] : null;
31
+
32
+ // 添加新响应
33
+ const handleAddResponse = () => {
34
+ const newResponseId = generateResponseId();
35
+ let currentResponses = getFieldValue('responses') || [];
36
+
37
+ // 确保数组格式
38
+ if (!Array.isArray(currentResponses)) {
39
+ try {
40
+ if (typeof currentResponses === 'string') {
41
+ currentResponses = JSON.parse(currentResponses);
42
+ }
43
+ } catch (e) {
44
+ currentResponses = [];
45
+ }
46
+ }
47
+
48
+ if (!Array.isArray(currentResponses)) {
49
+ currentResponses = [];
50
+ }
51
+
52
+ const newResponse = {
53
+ id: newResponseId,
54
+ name: `响应 ${currentResponses.length + 1}`,
55
+ description: '',
56
+ content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
57
+ };
58
+
59
+ const updatedResponses = [...currentResponses, newResponse];
60
+
61
+ // 更新表单字段
62
+ setFieldsValue({
63
+ responses: updatedResponses,
64
+ activeResponseId: newResponseId
65
+ });
66
+ };
67
+
68
+ // 删除响应
69
+ const handleDeleteResponse = (responseId) => {
70
+ let currentResponses = getFieldValue('responses') || [];
71
+
72
+ if (currentResponses.length <= 1) {
73
+ message.warning('至少需要保留一个响应');
74
+ return;
75
+ }
76
+
77
+ const updatedResponses = currentResponses.filter(resp => resp.id !== responseId);
78
+
79
+ // 如果删除的是当前选中的响应,切换到第一个响应
80
+ let newActiveId = activeResponseId;
81
+ if (responseId === activeResponseId) {
82
+ newActiveId = updatedResponses[0]?.id;
83
+ }
84
+
85
+ setFieldsValue({
86
+ responses: updatedResponses,
87
+ activeResponseId: newActiveId
88
+ });
89
+ };
90
+
91
+ // 切换响应
92
+ const handleResponseSelect = (responseId) => {
93
+ setFieldsValue({ activeResponseId: responseId });
94
+ setIsEditing(false);
95
+ };
96
+
97
+ // 开始编辑响应名称
98
+ const handleStartEdit = () => {
99
+ if (activeResponse) {
100
+ setEditingName(activeResponse.name || '');
101
+ setIsEditing(true);
102
+ }
103
+ };
104
+
105
+ // 完成编辑响应名称
106
+ const handleFinishEdit = () => {
107
+ if (activeResponseId && editingName.trim()) {
108
+ const currentResponses = getFieldValue('responses') || [];
109
+ const updatedResponses = currentResponses.map(resp =>
110
+ resp.id === activeResponseId ? { ...resp, name: editingName.trim() } : resp
111
+ );
112
+ setFieldsValue({ responses: updatedResponses });
113
+ }
114
+ setIsEditing(false);
115
+ setEditingName('');
116
+ };
117
+
118
+ // 取消编辑
119
+ const handleCancelEdit = () => {
120
+ setIsEditing(false);
121
+ setEditingName('');
122
+ };
123
+
124
+ // 更新响应内容
125
+ const updateActiveResponseContent = (content) => {
126
+ if (activeResponseIndex >= 0) {
127
+ const currentResponses = getFieldValue('responses') || [];
128
+ const newResponses = [...currentResponses];
129
+ newResponses[activeResponseIndex] = {
130
+ ...newResponses[activeResponseIndex],
131
+ content
132
+ };
133
+ setFieldsValue({ responses: newResponses });
134
+ }
135
+ };
136
+
137
+ // 格式化JSON内容
138
+ const formatJsonContent = () => {
139
+ if (!activeResponse || !activeResponse.content) {
140
+ message.warning('没有可格式化的JSON内容');
141
+ return;
142
+ }
143
+
144
+ try {
145
+ const parsedJson = JSON.parse(activeResponse.content);
146
+ const formattedJson = JSON.stringify(parsedJson, null, 2);
147
+
148
+ updateActiveResponseContent(formattedJson);
149
+ message.success('JSON已格式化');
150
+ } catch (error) {
151
+ message.error('无效的JSON格式,无法格式化');
152
+ }
153
+ };
154
+
155
+ // 预览响应内容
156
+ const handlePreview = () => {
157
+ if (!activeResponse) {
158
+ message.error('未找到有效的响应内容');
159
+ return;
160
+ }
161
+
162
+ if (onPreview) {
163
+ onPreview();
164
+ }
165
+ };
166
+
167
+ // 如果没有响应数据,显示创建按钮
168
+ if (!Array.isArray(responses) || responses.length === 0) {
169
+ return (
170
+ <Card
171
+ title={
172
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
173
+ <CodeOutlined />
174
+ <span>响应内容编辑</span>
175
+ </div>
176
+ }
177
+ style={{ marginBottom: 16 }}
178
+ bodyStyle={{ padding: '16px' }}
179
+ >
180
+ <div style={{
181
+ textAlign: 'center',
182
+ padding: '40px 20px',
183
+ background: '#fafafa',
184
+ borderRadius: '6px',
185
+ border: '1px dashed #d9d9d9'
186
+ }}>
187
+ <FileTextOutlined style={{ fontSize: '32px', color: '#d9d9d9', marginBottom: '12px' }} />
188
+ <div style={{ color: '#999', marginBottom: '16px' }}>暂无响应数据</div>
189
+ <Button
190
+ type="primary"
191
+ icon={<PlusCircleOutlined />}
192
+ onClick={handleAddResponse}
193
+ >
194
+ 创建第一个响应
195
+ </Button>
196
+ </div>
197
+ </Card>
198
+ );
199
+ }
200
+
201
+ return (
202
+ <Card
203
+ title={
204
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
205
+ <CodeOutlined />
206
+ <span>响应内容编辑</span>
207
+ <Badge
208
+ count={responses.length}
209
+ style={{ backgroundColor: '#52c41a' }}
210
+ />
211
+ </div>
212
+ }
213
+ extra={
214
+ <Space size="small">
215
+ <Button
216
+ type="default"
217
+ icon={<PlusCircleOutlined />}
218
+ onClick={handleAddResponse}
219
+ size="small"
220
+ style={{
221
+ display: 'flex',
222
+ alignItems: 'center',
223
+ gap: '4px',
224
+ borderColor: '#1890ff',
225
+ color: '#1890ff'
226
+ }}
227
+ >
228
+ 添加响应
229
+ </Button>
230
+ <Button
231
+ type="default"
232
+ icon={<EyeOutlined />}
233
+ onClick={handlePreview}
234
+ size="small"
235
+ style={{
236
+ display: 'flex',
237
+ alignItems: 'center',
238
+ gap: '4px',
239
+ borderColor: '#52c41a',
240
+ color: '#52c41a'
241
+ }}
242
+ >
243
+ 预览
244
+ </Button>
245
+ <Button
246
+ type="default"
247
+ icon={<FormatPainterOutlined />}
248
+ onClick={formatJsonContent}
249
+ size="small"
250
+ style={{
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ gap: '4px',
254
+ borderColor: '#fa8c16',
255
+ color: '#fa8c16'
256
+ }}
257
+ >
258
+ 格式化JSON
259
+ </Button>
260
+ </Space>
261
+ }
262
+ style={{ marginBottom: 16 }}
263
+ bodyStyle={{ padding: '16px' }}
264
+ >
265
+ {/* 响应选择和名称编辑合并区域 */}
266
+ <div style={{ marginBottom: '16px' }}>
267
+ <Row gutter={12} align="middle">
268
+ <Col span={20}>
269
+ <div style={{ marginBottom: '8px' }}>
270
+ <label style={{
271
+ fontSize: '12px',
272
+ color: '#666',
273
+ fontWeight: 'bold'
274
+ }}>
275
+ 选择响应 / 编辑名称
276
+ </label>
277
+ </div>
278
+
279
+ <div style={{ position: 'relative' }}>
280
+ {isEditing ? (
281
+ // 编辑模式:显示输入框
282
+ <Input.Group compact>
283
+ <Input
284
+ style={{ width: 'calc(100% - 80px)' }}
285
+ value={editingName}
286
+ onChange={(e) => setEditingName(e.target.value)}
287
+ onPressEnter={handleFinishEdit}
288
+ onBlur={handleFinishEdit}
289
+ placeholder="输入响应名称"
290
+ autoFocus
291
+ />
292
+ <Button
293
+ style={{ width: '40px' }}
294
+ type="primary"
295
+ icon={<CheckCircleOutlined />}
296
+ onClick={handleFinishEdit}
297
+ />
298
+ <Button
299
+ style={{ width: '40px' }}
300
+ icon={<DeleteOutlined />}
301
+ onClick={handleCancelEdit}
302
+ />
303
+ </Input.Group>
304
+ ) : (
305
+ // 选择模式:显示下拉选择器
306
+ <Input.Group compact>
307
+ <Select
308
+ style={{ width: 'calc(100% - 40px)' }}
309
+ placeholder="选择要编辑的响应"
310
+ value={activeResponseId}
311
+ onChange={handleResponseSelect}
312
+ optionLabelProp="label"
313
+ >
314
+ {responses.map((resp, index) => (
315
+ <Option
316
+ key={resp.id}
317
+ value={resp.id}
318
+ label={
319
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
320
+ <CheckCircleOutlined style={{ color: '#52c41a' }} />
321
+ <span>{resp.name || `响应 ${index + 1}`}</span>
322
+ </div>
323
+ }
324
+ >
325
+ <div style={{ padding: '4px 0' }}>
326
+ <div style={{
327
+ display: 'flex',
328
+ alignItems: 'center',
329
+ justifyContent: 'space-between',
330
+ marginBottom: '4px'
331
+ }}>
332
+ <span style={{ fontWeight: 'bold' }}>
333
+ {resp.name || `响应 ${index + 1}`}
334
+ </span>
335
+ {resp.id === activeResponseId && (
336
+ <Badge status="processing" text="当前选中" />
337
+ )}
338
+ </div>
339
+ </div>
340
+ </Option>
341
+ ))}
342
+ </Select>
343
+ <Tooltip title="编辑响应名称">
344
+ <Button
345
+ style={{ width: '40px' }}
346
+ icon={<EditOutlined />}
347
+ onClick={handleStartEdit}
348
+ disabled={!activeResponse}
349
+ />
350
+ </Tooltip>
351
+ </Input.Group>
352
+ )}
353
+ </div>
354
+ </Col>
355
+
356
+ <Col span={4}>
357
+ <div style={{ marginBottom: '8px' }}>
358
+ <label style={{
359
+ fontSize: '12px',
360
+ color: '#666',
361
+ fontWeight: 'bold'
362
+ }}>
363
+ 操作
364
+ </label>
365
+ </div>
366
+ <Tooltip title="删除当前响应">
367
+ <Button
368
+ type="text"
369
+ danger
370
+ icon={<DeleteOutlined />}
371
+ onClick={() => activeResponseId && handleDeleteResponse(activeResponseId)}
372
+ disabled={responses.length <= 1 || !activeResponse}
373
+ style={{ width: '100%' }}
374
+ >
375
+ 删除
376
+ </Button>
377
+ </Tooltip>
378
+ </Col>
379
+ </Row>
380
+ </div>
381
+
382
+ {/* 响应内容编辑区域 */}
383
+ {activeResponse && (
384
+ <>
385
+ <div style={{ marginBottom: '8px' }}>
386
+ <label style={{
387
+ fontSize: '12px',
388
+ color: '#666',
389
+ fontWeight: 'bold'
390
+ }}>
391
+ 响应内容
392
+ </label>
393
+ </div>
394
+
395
+ <div style={{ position: 'relative' }}>
396
+ <TextArea
397
+ rows={12}
398
+ placeholder="请输入响应内容..."
399
+ style={{
400
+ fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
401
+ fontSize: '13px',
402
+ lineHeight: '1.5'
403
+ }}
404
+ value={activeResponse.content || ''}
405
+ onChange={(e) => updateActiveResponseContent(e.target.value)}
406
+ />
407
+ <div style={{
408
+ position: 'absolute',
409
+ top: '8px',
410
+ right: '8px',
411
+ background: 'rgba(0, 0, 0, 0.1)',
412
+ borderRadius: '4px',
413
+ padding: '2px 6px',
414
+ fontSize: '11px',
415
+ color: '#666',
416
+ pointerEvents: 'none'
417
+ }}>
418
+ JSON
419
+ </div>
420
+ </div>
421
+ </>
422
+ )}
423
+ </Card>
424
+ );
425
+ };
426
+
427
+ export default ResponseContentEditor;
@@ -0,0 +1,36 @@
1
+ // 常量定义文件
2
+ export const contentTypes = [
3
+ { value: 'application/json; charset=utf-8', label: 'JSON' },
4
+ { value: 'text/plain; charset=utf-8', label: '纯文本' },
5
+ { value: 'text/html; charset=utf-8', label: 'HTML' },
6
+ { value: 'application/xml; charset=utf-8', label: 'XML' },
7
+ { value: 'application/javascript; charset=utf-8', label: 'JavaScript' },
8
+ ];
9
+
10
+ export const proxyTypes = [
11
+ { value: 'response', label: '模拟响应' },
12
+ { value: 'redirect', label: '重定向' },
13
+ { value: 'url_redirect', label: 'URL重定向' },
14
+ ];
15
+
16
+ export const statusCodes = [
17
+ { value: '200', label: '200 OK' },
18
+ { value: '201', label: '201 Created' },
19
+ { value: '204', label: '204 No Content' },
20
+ { value: '400', label: '400 Bad Request' },
21
+ { value: '401', label: '401 Unauthorized' },
22
+ { value: '403', label: '403 Forbidden' },
23
+ { value: '404', label: '404 Not Found' },
24
+ { value: '500', label: '500 Internal Server Error' },
25
+ ];
26
+
27
+ export const httpMethods = [
28
+ { value: 'ALL', label: '所有方法' },
29
+ { value: 'GET', label: 'GET' },
30
+ { value: 'POST', label: 'POST' },
31
+ { value: 'PUT', label: 'PUT' },
32
+ { value: 'DELETE', label: 'DELETE' },
33
+ { value: 'PATCH', label: 'PATCH' },
34
+ { value: 'HEAD', label: 'HEAD' },
35
+ { value: 'OPTIONS', label: 'OPTIONS' },
36
+ ];
@@ -0,0 +1,6 @@
1
+ // 统一导出接口管理相关的组件
2
+ export { default as ResponseContentEditor } from './ResponseContentEditor';
3
+ export { default as InterfaceTestModal } from './InterfaceTestModal';
4
+ export { default as PreviewModal } from './PreviewModal';
5
+ export * from './constants';
6
+ export * from './utils';
@@ -0,0 +1,103 @@
1
+ import axios from 'axios';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ // 清理JSON响应
5
+ export const cleanJsonResponse = (jsonStr) => {
6
+ try {
7
+ const parsed = JSON.parse(jsonStr);
8
+ return JSON.stringify(parsed);
9
+ } catch (e) {
10
+ return jsonStr;
11
+ }
12
+ };
13
+
14
+ // 验证URL是否合法
15
+ export const isValidUrl = (url) => {
16
+ if (!url) return false;
17
+
18
+ try {
19
+ new URL(url);
20
+ return true;
21
+ } catch (e) {
22
+ return false;
23
+ }
24
+ };
25
+
26
+ // 缓存刷新服务
27
+ export const flushCache = async () => {
28
+ try {
29
+ const response = await axios.get('/_flush_cache');
30
+ return response.data;
31
+ } catch (error) {
32
+ console.error('刷新缓存失败:', error);
33
+ throw error;
34
+ }
35
+ };
36
+
37
+ // 更新成功后刷新缓存
38
+ export const refreshCacheAfterUpdate = async () => {
39
+ try {
40
+ await flushCache();
41
+ // 这里不需要显示提示,因为主要操作会有自己的提示
42
+ } catch (error) {
43
+ // 如果刷新缓存失败,记录错误但不影响用户体验
44
+ console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
45
+ }
46
+ };
47
+
48
+ // 检查 URL 是否匹配模式
49
+ export const isUrlMatchPattern = (url, pattern, proxyType) => {
50
+ if (!url || !pattern) return false;
51
+
52
+ try {
53
+ // 对于url_redirect类型,需要完全匹配
54
+ if (proxyType === 'url_redirect') {
55
+ return url === pattern;
56
+ }
57
+
58
+ // 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
59
+ if (proxyType === 'redirect') {
60
+ return url.indexOf(pattern) === 0;
61
+ }
62
+
63
+ // 以下是默认的匹配逻辑(用于response类型等)
64
+ // 如果是正则表达式
65
+ if (pattern.startsWith('/') && pattern.endsWith('/')) {
66
+ const regex = new RegExp(pattern.slice(1, -1));
67
+ return regex.test(url);
68
+ }
69
+
70
+ // 如果是通配符模式
71
+ if (pattern.includes('*')) {
72
+ const regexPattern = pattern
73
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
74
+ .replace(/\*/g, '.*');
75
+ const regex = new RegExp(`^${regexPattern}$`);
76
+ return regex.test(url);
77
+ }
78
+
79
+ // 精确匹配
80
+ return url === pattern;
81
+ } catch (e) {
82
+ console.error('URL匹配检查失败:', e);
83
+ return false;
84
+ }
85
+ };
86
+
87
+ // 格式化响应内容
88
+ export const formatResponseContent = (content, contentType) => {
89
+ if (contentType && contentType.includes('json')) {
90
+ try {
91
+ const parsed = JSON.parse(content);
92
+ return JSON.stringify(parsed, null, 2);
93
+ } catch (e) {
94
+ return content;
95
+ }
96
+ }
97
+ return content;
98
+ };
99
+
100
+ // 生成唯一的响应ID
101
+ export const generateResponseId = () => {
102
+ return uuidv4();
103
+ };