whistle.mock-plugins 1.0.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 (68) hide show
  1. package/API.md +247 -0
  2. package/README.md +91 -0
  3. package/app/cgi-bin/features.js +285 -0
  4. package/app/cgi-bin/index.js +31 -0
  5. package/app/cgi-bin/interfaces.js +409 -0
  6. package/app/cgi-bin/logs.js +151 -0
  7. package/app/cgi-bin/test-interface.js +152 -0
  8. package/app/public/index.html +1 -0
  9. package/app/public/js/index.js +2 -0
  10. package/app/public/js/index.js.LICENSE.txt +150 -0
  11. package/app/util/index.js +24 -0
  12. package/index.html +52 -0
  13. package/initialize.js +32 -0
  14. package/lib/dataManager.js +500 -0
  15. package/lib/index.js +3 -0
  16. package/lib/models.js +64 -0
  17. package/lib/routes/features.js +213 -0
  18. package/lib/routes/interfaces.js +304 -0
  19. package/lib/routes/proxy.js +239 -0
  20. package/lib/ruleManager.js +551 -0
  21. package/lib/rules-server.js +267 -0
  22. package/lib/server.js +805 -0
  23. package/lib/storage.js +16 -0
  24. package/lib/ui-server.js +68 -0
  25. package/package.json +51 -0
  26. package/public/bundle.js +2 -0
  27. package/public/bundle.js.LICENSE.txt +38 -0
  28. package/public/index.html +1 -0
  29. package/public/plugin.mock-plugin.html +14 -0
  30. package/rules.txt +22 -0
  31. package/test/rulesServer.test.js +120 -0
  32. package/ui/src/App.css +173 -0
  33. package/ui/src/App.js +24 -0
  34. package/ui/src/components/AppLayout.js +149 -0
  35. package/ui/src/components/FeatureCard.js +53 -0
  36. package/ui/src/components/Layout.js +94 -0
  37. package/ui/src/components/MainNav.js +42 -0
  38. package/ui/src/components/PageContainer.js +16 -0
  39. package/ui/src/index.html +11 -0
  40. package/ui/src/index.js +43 -0
  41. package/ui/src/pages/FileProxy.js +235 -0
  42. package/ui/src/pages/FilesPage.js +292 -0
  43. package/ui/src/pages/Home.js +128 -0
  44. package/ui/src/pages/HomePage.js +195 -0
  45. package/ui/src/pages/InterfaceManagement.js +829 -0
  46. package/ui/src/pages/InterfacePage.js +342 -0
  47. package/ui/src/pages/MockData.js +472 -0
  48. package/ui/src/pages/Settings.js +192 -0
  49. package/ui/src/pages/SettingsPage.js +165 -0
  50. package/ui/src/pages/UrlRedirect.js +225 -0
  51. package/ui/src/pages/Welcome.js +155 -0
  52. package/ui/src/services/api.js +186 -0
  53. package/ui/src/styles/app-layout.css +337 -0
  54. package/ui/src/styles/feature-card.css +172 -0
  55. package/ui/src/styles/file-proxy.css +301 -0
  56. package/ui/src/styles/global.css +224 -0
  57. package/ui/src/styles/home.css +123 -0
  58. package/ui/src/styles/index.css +419 -0
  59. package/ui/src/styles/interface-management.css +694 -0
  60. package/ui/src/styles/layout.css +159 -0
  61. package/ui/src/styles/logs-page.css +427 -0
  62. package/ui/src/styles/main-nav.css +52 -0
  63. package/ui/src/styles/mock-data.css +850 -0
  64. package/ui/src/styles/page-container.css +11 -0
  65. package/ui/src/styles/settings.css +208 -0
  66. package/ui/src/styles/url-redirect.css +279 -0
  67. package/ui/src/styles/welcome.css +282 -0
  68. package/webpack.config.js +49 -0
@@ -0,0 +1,829 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useParams, useHistory } from 'react-router-dom';
3
+ import AppLayout from '../components/AppLayout';
4
+ import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs } from 'antd';
5
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined } from '@ant-design/icons';
6
+ import '../styles/interface-management.css';
7
+ import axios from 'axios';
8
+
9
+ const { Option } = Select;
10
+ const { TextArea } = Input;
11
+ const { TabPane } = Tabs;
12
+
13
+ const InterfaceManagement = () => {
14
+ const { featureId } = useParams();
15
+ const history = useHistory();
16
+ const [features, setFeatures] = useState([]);
17
+ const [interfaces, setInterfaces] = useState([]);
18
+ const [loading, setLoading] = useState(false);
19
+ const [featuresLoading, setFeaturesLoading] = useState(false);
20
+ const [interfacesLoading, setInterfacesLoading] = useState(false);
21
+ const [modalVisible, setModalVisible] = useState(false);
22
+ const [editingInterface, setEditingInterface] = useState(null);
23
+ const [testModalVisible, setTestModalVisible] = useState(false);
24
+ const [testUrl, setTestUrl] = useState('');
25
+ const [testResult, setTestResult] = useState(null);
26
+ const [testLoading, setTestLoading] = useState(false);
27
+ const [selectedFeatureId, setSelectedFeatureId] = useState(null);
28
+ const [form] = Form.useForm();
29
+ const [testForm] = Form.useForm();
30
+ const [previewContent, setPreviewContent] = useState(null);
31
+ const [previewVisible, setPreviewVisible] = useState(false);
32
+
33
+ const contentTypes = [
34
+ { value: 'application/json; charset=utf-8', label: 'JSON' },
35
+ { value: 'text/plain; charset=utf-8', label: '纯文本' },
36
+ { value: 'text/html; charset=utf-8', label: 'HTML' },
37
+ { value: 'application/xml; charset=utf-8', label: 'XML' },
38
+ { value: 'application/javascript; charset=utf-8', label: 'JavaScript' },
39
+ ];
40
+
41
+ const statusCodes = [
42
+ { value: '200', label: '200 OK' },
43
+ { value: '201', label: '201 Created' },
44
+ { value: '204', label: '204 No Content' },
45
+ { value: '400', label: '400 Bad Request' },
46
+ { value: '401', label: '401 Unauthorized' },
47
+ { value: '403', label: '403 Forbidden' },
48
+ { value: '404', label: '404 Not Found' },
49
+ { value: '500', label: '500 Internal Server Error' },
50
+ ];
51
+
52
+ const httpMethods = [
53
+ { value: 'ALL', label: '所有方法' },
54
+ { value: 'GET', label: 'GET' },
55
+ { value: 'POST', label: 'POST' },
56
+ { value: 'PUT', label: 'PUT' },
57
+ { value: 'DELETE', label: 'DELETE' },
58
+ { value: 'PATCH', label: 'PATCH' },
59
+ { value: 'HEAD', label: 'HEAD' },
60
+ { value: 'OPTIONS', label: 'OPTIONS' },
61
+ ];
62
+
63
+ useEffect(() => {
64
+ fetchFeatures();
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ if (selectedFeatureId) {
69
+ fetchInterfaces();
70
+ }
71
+ }, [selectedFeatureId]);
72
+
73
+ // 获取当前选中的功能模块
74
+ const selectedFeature = features.find(f => f.id === selectedFeatureId);
75
+
76
+ const fetchFeatures = async () => {
77
+ try {
78
+ setFeaturesLoading(true);
79
+ const response = await axios.get('/cgi-bin/features');
80
+ if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
81
+ setFeatures(response.data.data);
82
+ // 如果URL中有featureId参数,使用它
83
+ const initialFeatureId = featureId || (response.data.data[0]?.id);
84
+ if (initialFeatureId) {
85
+ setSelectedFeatureId(initialFeatureId);
86
+ }
87
+ } else {
88
+ setFeatures([]);
89
+ message.warning('获取功能模块数据格式不正确');
90
+ }
91
+ } catch (error) {
92
+ console.error('获取功能模块失败:', error);
93
+ message.error(error.response?.data?.message || '获取功能模块失败');
94
+ setFeatures([]);
95
+ } finally {
96
+ setFeaturesLoading(false);
97
+ }
98
+ };
99
+
100
+ const fetchInterfaces = async () => {
101
+ if (!selectedFeatureId) {
102
+ return;
103
+ }
104
+ try {
105
+ setInterfacesLoading(true);
106
+ const response = await axios.get(`/cgi-bin/interfaces?featureId=${selectedFeatureId}`);
107
+ if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
108
+ setInterfaces(response.data.data);
109
+ } else {
110
+ setInterfaces([]);
111
+ message.warning('获取接口配置数据格式不正确');
112
+ }
113
+ } catch (error) {
114
+ console.error('获取接口配置失败:', error);
115
+ message.error(error.response?.data?.message || '获取接口配置失败');
116
+ setInterfaces([]);
117
+ } finally {
118
+ setInterfacesLoading(false);
119
+ }
120
+ };
121
+
122
+ const handleAddInterface = () => {
123
+ if (!selectedFeatureId) {
124
+ message.warning('请先选择一个功能模块');
125
+ return;
126
+ }
127
+ form.resetFields();
128
+ setEditingInterface(null);
129
+ setModalVisible(true);
130
+ };
131
+
132
+ const handleEditInterface = (record) => {
133
+ setEditingInterface(record);
134
+ form.setFieldsValue({
135
+ name: record.name,
136
+ pattern: record.urlPattern,
137
+ statusCode: record.httpStatus?.toString() || '200',
138
+ contentType: record.contentType || 'application/json; charset=utf-8',
139
+ responseBody: record.responseContent || '',
140
+ httpMethod: record.httpMethod || 'ALL',
141
+ });
142
+ setModalVisible(true);
143
+ };
144
+
145
+ const handleDeleteInterface = async (id) => {
146
+ try {
147
+ const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
148
+ if (response.data && response.data.code === 0) {
149
+ message.success('接口删除成功');
150
+ fetchInterfaces();
151
+ } else {
152
+ throw new Error(response.data?.message || '接口删除失败');
153
+ }
154
+ } catch (error) {
155
+ console.error('接口删除失败:', error);
156
+ message.error(error.response?.data?.message || error.message || '接口删除失败');
157
+ }
158
+ };
159
+
160
+ const handleToggleActive = async (id, currentActive) => {
161
+ try {
162
+ const response = await axios.patch(`/cgi-bin/interfaces?id=${id}`, {
163
+ active: !currentActive
164
+ });
165
+ if (response.data && response.data.code === 0) {
166
+ message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
167
+ fetchInterfaces();
168
+ } else {
169
+ throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
170
+ }
171
+ } catch (error) {
172
+ console.error(`接口${!currentActive ? '启用' : '禁用'}失败:`, error);
173
+ message.error(error.response?.data?.message || error.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
174
+ }
175
+ };
176
+
177
+ // 清理JSON响应内容中不需要的空白和格式化
178
+ const cleanJsonResponse = (jsonStr) => {
179
+ try {
180
+ const parsed = JSON.parse(jsonStr);
181
+ return JSON.stringify(parsed);
182
+ } catch (e) {
183
+ return jsonStr;
184
+ }
185
+ };
186
+
187
+ const handleSubmit = async () => {
188
+ try {
189
+ const values = await form.validateFields();
190
+
191
+ // 验证 URL 匹配规则格式
192
+ if (!isValidPattern(values.pattern)) {
193
+ message.error('URL匹配规则格式不正确');
194
+ return;
195
+ }
196
+
197
+ // 验证响应内容格式
198
+ if (values.contentType.includes('json')) {
199
+ try {
200
+ // 清理并验证JSON
201
+ const cleanedJson = cleanJsonResponse(values.responseBody);
202
+ JSON.parse(cleanedJson);
203
+ values.responseBody = cleanedJson;
204
+ } catch (e) {
205
+ message.error('JSON响应内容格式不正确');
206
+ return;
207
+ }
208
+ }
209
+
210
+ const interfaceData = {
211
+ name: values.name,
212
+ featureId: selectedFeatureId,
213
+ urlPattern: values.pattern,
214
+ proxyType: 'response',
215
+ responseContent: values.responseBody,
216
+ httpStatus: parseInt(values.statusCode, 10), // 转换为数字
217
+ contentType: values.contentType,
218
+ responseDelay: 0,
219
+ httpMethod: values.httpMethod,
220
+ active: true
221
+ };
222
+
223
+ if (editingInterface) {
224
+ const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
225
+ if (response.data && response.data.code === 0) {
226
+ message.success('接口更新成功');
227
+ } else {
228
+ throw new Error(response.data?.message || '接口更新失败');
229
+ }
230
+ } else {
231
+ const response = await axios.post('/cgi-bin/interfaces', interfaceData);
232
+ if (response.data && response.data.code === 0) {
233
+ message.success('接口添加成功');
234
+ } else {
235
+ throw new Error(response.data?.message || '接口添加失败');
236
+ }
237
+ }
238
+
239
+ setModalVisible(false);
240
+ fetchInterfaces();
241
+ } catch (error) {
242
+ if (error.errorFields) {
243
+ return; // 表单验证错误
244
+ }
245
+ console.error('操作失败:', error);
246
+ message.error(error.response?.data?.message || error.message || '操作失败,请检查数据格式是否正确');
247
+ }
248
+ };
249
+
250
+ // 验证 URL 匹配规则格式
251
+ const isValidPattern = (pattern) => {
252
+ if (!pattern) return false;
253
+
254
+ // 检查是否是有效的正则表达式
255
+ if (pattern.startsWith('/') && pattern.endsWith('/')) {
256
+ try {
257
+ new RegExp(pattern.slice(1, -1));
258
+ return true;
259
+ } catch (e) {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ // 检查通配符格式
265
+ if (pattern.includes('*')) {
266
+ return /^[a-zA-Z0-9\-_/.*]+$/.test(pattern);
267
+ }
268
+
269
+ // 检查普通路径格式
270
+ return /^[a-zA-Z0-9\-_/]+$/.test(pattern);
271
+ };
272
+
273
+ const handleCancel = () => {
274
+ setModalVisible(false);
275
+ };
276
+
277
+ const handleTestInterface = (record) => {
278
+ setEditingInterface(record);
279
+ setTestUrl('');
280
+ setTestResult(null);
281
+ setTestModalVisible(true);
282
+ testForm.resetFields();
283
+ };
284
+
285
+ const handleTestSubmit = async () => {
286
+ try {
287
+ const values = await testForm.validateFields();
288
+ setTestLoading(true);
289
+
290
+ try {
291
+ // 验证测试 URL 是否匹配当前接口的匹配规则
292
+ if (!isUrlMatchPattern(values.testUrl, editingInterface.urlPattern)) {
293
+ setTestResult({
294
+ success: false,
295
+ error: '测试URL与接口匹配规则不匹配'
296
+ });
297
+ message.error('测试URL与接口匹配规则不匹配');
298
+ return;
299
+ }
300
+
301
+ // 调用实际测试接口
302
+ const response = await axios.post(`/cgi-bin/test-interface`, {
303
+ url: values.testUrl,
304
+ interfaceId: editingInterface.id
305
+ });
306
+
307
+ if (response.data && response.data.code === 0 && response.data.data) {
308
+ setTestResult({
309
+ success: true,
310
+ statusCode: response.data.data.statusCode,
311
+ contentType: response.data.data.contentType,
312
+ responseBody: response.data.data.responseBody,
313
+ matchedRule: editingInterface.urlPattern,
314
+ httpMethod: editingInterface.httpMethod,
315
+ requestUrl: values.testUrl,
316
+ mockInfo: response.data.data.mockInfo
317
+ });
318
+ message.success('测试成功');
319
+ } else {
320
+ throw new Error(response.data?.message || '测试响应格式不正确');
321
+ }
322
+ } catch (error) {
323
+ setTestResult({
324
+ success: false,
325
+ error: error.response?.data?.message || error.message || '测试失败'
326
+ });
327
+ message.error(error.response?.data?.message || error.message || '测试失败');
328
+ }
329
+
330
+ setTestLoading(false);
331
+ } catch (error) {
332
+ if (error.errorFields) {
333
+ return; // 表单验证错误
334
+ }
335
+ setTestLoading(false);
336
+ }
337
+ };
338
+
339
+ // 检查 URL 是否匹配模式
340
+ const isUrlMatchPattern = (url, pattern) => {
341
+ if (!url || !pattern) return false;
342
+
343
+ try {
344
+ // 如果是正则表达式
345
+ if (pattern.startsWith('/') && pattern.endsWith('/')) {
346
+ const regex = new RegExp(pattern.slice(1, -1));
347
+ return regex.test(url);
348
+ }
349
+
350
+ // 如果是通配符模式
351
+ if (pattern.includes('*')) {
352
+ const regexPattern = pattern
353
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
354
+ .replace(/\*/g, '.*');
355
+ const regex = new RegExp(`^${regexPattern}$`);
356
+ return regex.test(url);
357
+ }
358
+
359
+ // 精确匹配
360
+ return url === pattern;
361
+ } catch (e) {
362
+ console.error('URL匹配检查失败:', e);
363
+ return false;
364
+ }
365
+ };
366
+
367
+ const handleSelectFeature = (featureId) => {
368
+ setSelectedFeatureId(featureId);
369
+ };
370
+
371
+ const formatResponseContent = (content, contentType) => {
372
+ if (contentType && contentType.includes('json')) {
373
+ try {
374
+ const parsed = JSON.parse(content);
375
+ return JSON.stringify(parsed, null, 2);
376
+ } catch (e) {
377
+ return content;
378
+ }
379
+ }
380
+ return content;
381
+ };
382
+
383
+ const formatJsonContent = () => {
384
+ const responseBody = form.getFieldValue('responseBody');
385
+ if (responseBody) {
386
+ try {
387
+ const formattedJson = JSON.stringify(JSON.parse(responseBody), null, 2);
388
+ form.setFieldsValue({ responseBody: formattedJson });
389
+ message.success('JSON格式化成功');
390
+ } catch (error) {
391
+ message.error('JSON格式不正确,无法格式化');
392
+ }
393
+ }
394
+ };
395
+
396
+ const handlePreview = () => {
397
+ const responseBody = form.getFieldValue('responseBody');
398
+ if (responseBody) {
399
+ try {
400
+ // 如果是JSON,格式化显示
401
+ const contentType = form.getFieldValue('contentType');
402
+ if (contentType && contentType.includes('json')) {
403
+ setPreviewContent(JSON.stringify(JSON.parse(responseBody), null, 2));
404
+ } else {
405
+ setPreviewContent(responseBody);
406
+ }
407
+ setPreviewVisible(true);
408
+ } catch (error) {
409
+ message.error('内容格式不正确,无法预览');
410
+ }
411
+ } else {
412
+ message.warning('响应内容为空,无法预览');
413
+ }
414
+ };
415
+
416
+ const filteredInterfaces = interfaces.filter(item =>
417
+ !selectedFeatureId || item.featureId === selectedFeatureId
418
+ );
419
+
420
+ const columns = [
421
+ {
422
+ title: '状态',
423
+ dataIndex: 'active',
424
+ key: 'active',
425
+ width: 80,
426
+ render: (active, record) => (
427
+ <Switch
428
+ checked={active}
429
+ onChange={() => handleToggleActive(record.id, active)}
430
+ />
431
+ ),
432
+ },
433
+ {
434
+ title: '名称',
435
+ dataIndex: 'name',
436
+ key: 'name',
437
+ ellipsis: true,
438
+ },
439
+ {
440
+ title: 'URL匹配规则',
441
+ dataIndex: 'urlPattern',
442
+ key: 'urlPattern',
443
+ ellipsis: true,
444
+ },
445
+ {
446
+ title: '状态码',
447
+ dataIndex: 'httpStatus',
448
+ key: 'httpStatus',
449
+ width: 100,
450
+ },
451
+ {
452
+ title: '内容类型',
453
+ dataIndex: 'contentType',
454
+ key: 'contentType',
455
+ width: 120,
456
+ render: (text) => {
457
+ const found = contentTypes.find(item => item.value === text);
458
+ return found ? found.label : text;
459
+ }
460
+ },
461
+ {
462
+ title: '请求方法',
463
+ dataIndex: 'httpMethod',
464
+ key: 'httpMethod',
465
+ width: 120,
466
+ render: (text) => {
467
+ const found = httpMethods.find(item => item.value === text);
468
+ return found ? found.label : text;
469
+ }
470
+ },
471
+ {
472
+ title: '操作',
473
+ key: 'action',
474
+ width: 180,
475
+ render: (_, record) => (
476
+ <div className="action-buttons">
477
+ <Button
478
+ type="text"
479
+ icon={<PlayCircleOutlined />}
480
+ onClick={() => handleTestInterface(record)}
481
+ />
482
+ <Button
483
+ type="text"
484
+ icon={<EditOutlined />}
485
+ onClick={() => handleEditInterface(record)}
486
+ />
487
+ <Popconfirm
488
+ title="确定要删除此接口吗?"
489
+ onConfirm={() => handleDeleteInterface(record.id)}
490
+ okText="确定"
491
+ cancelText="取消"
492
+ >
493
+ <Button type="text" danger icon={<DeleteOutlined />} />
494
+ </Popconfirm>
495
+ </div>
496
+ ),
497
+ },
498
+ ];
499
+
500
+ return (
501
+ <AppLayout>
502
+ <div className="interface-management-container">
503
+ <div className="interface-management-header">
504
+ <div className="feature-selector">
505
+ <span>功能模块:</span>
506
+ <Select
507
+ value={selectedFeatureId}
508
+ onChange={handleSelectFeature}
509
+ style={{ width: 200 }}
510
+ placeholder="选择功能模块"
511
+ loading={featuresLoading}
512
+ >
513
+ {(features || []).map(feature => (
514
+ <Option key={feature.id} value={feature.id}>
515
+ {feature.name}
516
+ {feature.active === false &&
517
+ <span style={{ color: '#ff4d4f', marginLeft: 8 }}>(已禁用)</span>
518
+ }
519
+ </Option>
520
+ ))}
521
+ </Select>
522
+ </div>
523
+ <Button
524
+ type="primary"
525
+ icon={<PlusOutlined />}
526
+ onClick={handleAddInterface}
527
+ disabled={!selectedFeatureId || selectedFeature?.active === false}
528
+ >
529
+ 添加接口
530
+ </Button>
531
+ </div>
532
+ {!features.length && (
533
+ <Alert
534
+ message="未找到功能模块"
535
+ description="请先在模拟数据页面创建功能模块,然后再添加接口"
536
+ type="warning"
537
+ showIcon
538
+ style={{ marginBottom: 16 }}
539
+ />
540
+ )}
541
+
542
+ {selectedFeature?.active === false && (
543
+ <Alert
544
+ message="功能模块已禁用"
545
+ description="当前功能模块已被禁用,所有关联接口不会生效。您可以在模拟数据页面启用此功能模块。"
546
+ type="warning"
547
+ showIcon
548
+ style={{ marginBottom: 16 }}
549
+ />
550
+ )}
551
+
552
+ <div className="interface-list-container">
553
+ <Table
554
+ columns={columns}
555
+ dataSource={filteredInterfaces}
556
+ rowKey="id"
557
+ loading={interfacesLoading}
558
+ pagination={{ pageSize: 10 }}
559
+ locale={{ emptyText: '暂无接口配置' }}
560
+ />
561
+ </div>
562
+
563
+ <Modal
564
+ title={editingInterface ? '编辑接口' : '添加接口'}
565
+ open={modalVisible}
566
+ onOk={handleSubmit}
567
+ onCancel={handleCancel}
568
+ width={800}
569
+ destroyOnClose
570
+ okText={editingInterface ? '保存' : '创建'}
571
+ cancelText="取消"
572
+ bodyStyle={{ maxHeight: '70vh', overflow: 'auto', padding: '24px' }}
573
+ >
574
+ <Form
575
+ form={form}
576
+ layout="vertical"
577
+ initialValues={{
578
+ name: '',
579
+ pattern: '',
580
+ statusCode: '200',
581
+ contentType: 'application/json; charset=utf-8',
582
+ responseBody: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
583
+ httpMethod: 'ALL'
584
+ }}
585
+ >
586
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
587
+ <div style={{ flex: 1 }}>
588
+ <Form.Item
589
+ name="name"
590
+ label="接口名称"
591
+ rules={[{ required: true, message: '请输入接口名称' }]}
592
+ >
593
+ <Input placeholder="请输入接口名称" />
594
+ </Form.Item>
595
+ </div>
596
+ <div style={{ flex: 1 }}>
597
+ <Form.Item
598
+ name="pattern"
599
+ label="URL匹配规则"
600
+ rules={[{ required: true, message: '请输入URL匹配规则' }]}
601
+ tooltip="支持多种匹配方式:精确匹配 - /api/users,通配符 - /api/users/*,正则表达式 - /api\/users\/\d+/"
602
+ >
603
+ <Input placeholder="例如:/api/users,/api/users/*,/api\/users\/\d+/" />
604
+ </Form.Item>
605
+ </div>
606
+ </div>
607
+
608
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
609
+ <div style={{ flex: 1 }}>
610
+ <Form.Item
611
+ name="statusCode"
612
+ label="状态码"
613
+ rules={[{ required: true, message: '请选择状态码' }]}
614
+ >
615
+ <Select>
616
+ {statusCodes.map(item => (
617
+ <Option key={item.value} value={item.value}>{item.label}</Option>
618
+ ))}
619
+ </Select>
620
+ </Form.Item>
621
+ </div>
622
+ <div style={{ flex: 1 }}>
623
+ <Form.Item
624
+ name="contentType"
625
+ label="内容类型"
626
+ rules={[{ required: true, message: '请选择内容类型' }]}
627
+ >
628
+ <Select>
629
+ {contentTypes.map(item => (
630
+ <Option key={item.value} value={item.value}>{item.label}</Option>
631
+ ))}
632
+ </Select>
633
+ </Form.Item>
634
+ </div>
635
+ <div style={{ flex: 1 }}>
636
+ <Form.Item
637
+ name="httpMethod"
638
+ label="请求方法"
639
+ rules={[{ required: true, message: '请选择请求方法' }]}
640
+ >
641
+ <Select>
642
+ {httpMethods.map(item => (
643
+ <Option key={item.value} value={item.value}>{item.label}</Option>
644
+ ))}
645
+ </Select>
646
+ </Form.Item>
647
+ </div>
648
+ </div>
649
+
650
+ <Form.Item
651
+ name="responseBody"
652
+ labelCol={{
653
+ span: 8, /* 宽度比例 */
654
+ }}
655
+ label={
656
+ <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
657
+ <span>响应内容</span>
658
+ <Space>
659
+ <Button
660
+ type="link"
661
+ icon={<EyeOutlined />}
662
+ onClick={handlePreview}
663
+ style={{ padding: 0 }}
664
+ >
665
+ 预览
666
+ </Button>
667
+ <Button
668
+ type="link"
669
+ icon={<FormatPainterOutlined />}
670
+ onClick={formatJsonContent}
671
+ style={{ padding: 0 }}
672
+ >
673
+ 格式化JSON
674
+ </Button>
675
+ </Space>
676
+ </div>
677
+ }
678
+ rules={[{ required: true, message: '请输入响应内容' }]}
679
+ >
680
+ <TextArea rows={12} placeholder="请输入响应内容" style={{ fontFamily: 'monospace' }} />
681
+ </Form.Item>
682
+ </Form>
683
+ </Modal>
684
+
685
+ <Modal
686
+ title="测试接口"
687
+ open={testModalVisible}
688
+ onCancel={() => setTestModalVisible(false)}
689
+ footer={[
690
+ <Button key="back" onClick={() => setTestModalVisible(false)}>
691
+ 关闭
692
+ </Button>,
693
+ <Button key="submit" type="primary" loading={testLoading} onClick={handleTestSubmit}>
694
+ 测试
695
+ </Button>
696
+ ]}
697
+ width={800}
698
+ >
699
+ {editingInterface && (
700
+ <div className="test-interface-container">
701
+ <Alert
702
+ message="URL匹配规则说明"
703
+ description={
704
+ <div>
705
+ <p>当前接口匹配规则:<code>{editingInterface.urlPattern}</code></p>
706
+ <p>在Whistle规则中,您需要配置完整URL或域名指向whistle.mock-plugin://,然后插件会根据接口的匹配规则处理请求。</p>
707
+ <p>插件支持以下匹配方式:</p>
708
+ <ul>
709
+ <li>精确匹配:完全匹配URL路径部分</li>
710
+ <li>通配符匹配:使用*代表任意字符(例如:/api/users/*)</li>
711
+ <li>正则表达式:使用/pattern/格式(例如:/\/api\/users\/\d+/)</li>
712
+ <li>部分匹配:URL包含指定路径即匹配</li>
713
+ </ul>
714
+ <p>请在下方输入完整测试URL进行测试</p>
715
+ </div>
716
+ }
717
+ type="info"
718
+ showIcon
719
+ style={{ marginBottom: 16 }}
720
+ />
721
+
722
+ <Form
723
+ form={testForm}
724
+ layout="vertical"
725
+ >
726
+ <Form.Item
727
+ name="testUrl"
728
+ label="测试URL"
729
+ rules={[{ required: true, message: '请输入测试URL' }]}
730
+ >
731
+ <Input
732
+ placeholder="输入完整URL,例如:https://example.com/api/users"
733
+ onChange={(e) => setTestUrl(e.target.value)}
734
+ />
735
+ </Form.Item>
736
+ </Form>
737
+
738
+ {testResult && (
739
+ <div className="test-result">
740
+ <div className="result-header">
741
+ <h3>测试结果</h3>
742
+ {testResult.success ? (
743
+ <span className="status success">成功</span>
744
+ ) : (
745
+ <span className="status error">失败</span>
746
+ )}
747
+ </div>
748
+
749
+ {testResult.success ? (
750
+ <div className="result-content">
751
+ <div className="result-item">
752
+ <span className="label">请求URL:</span>
753
+ <span className="value">{testResult.requestUrl}</span>
754
+ </div>
755
+ <div className="result-item">
756
+ <span className="label">匹配规则:</span>
757
+ <span className="value">{testResult.matchedRule}</span>
758
+ </div>
759
+ <div className="result-item">
760
+ <span className="label">请求方法:</span>
761
+ <span className="value">{testResult.httpMethod || 'ALL'}</span>
762
+ </div>
763
+ <div className="result-item">
764
+ <span className="label">状态码:</span>
765
+ <span className="value">{testResult.statusCode}</span>
766
+ </div>
767
+ <div className="result-item">
768
+ <span className="label">内容类型:</span>
769
+ <span className="value">{testResult.contentType}</span>
770
+ </div>
771
+ {testResult.mockInfo && (
772
+ <>
773
+ <div className="result-item">
774
+ <span className="label">模拟延迟:</span>
775
+ <span className="value">{testResult.mockInfo.delay}ms</span>
776
+ </div>
777
+ <div className="result-item">
778
+ <span className="label">响应时间:</span>
779
+ <span className="value">{new Date(testResult.mockInfo.timestamp).toLocaleString()}</span>
780
+ </div>
781
+ </>
782
+ )}
783
+ <div className="result-body">
784
+ <div className="label">响应内容:</div>
785
+ <pre>{formatResponseContent(testResult.responseBody, testResult.contentType)}</pre>
786
+ </div>
787
+ </div>
788
+ ) : (
789
+ <div className="result-error">
790
+ <ExclamationCircleOutlined /> {testResult.error}
791
+ </div>
792
+ )}
793
+ </div>
794
+ )}
795
+ </div>
796
+ )}
797
+ </Modal>
798
+
799
+ {/* 预览内容弹窗 */}
800
+ <Modal
801
+ title="预览响应内容"
802
+ open={previewVisible}
803
+ onCancel={() => setPreviewVisible(false)}
804
+ footer={[
805
+ <Button key="close" onClick={() => setPreviewVisible(false)}>
806
+ 关闭
807
+ </Button>
808
+ ]}
809
+ width={600}
810
+ >
811
+ <div style={{
812
+ maxHeight: '60vh',
813
+ overflow: 'auto',
814
+ backgroundColor: '#f5f5f5',
815
+ padding: '16px',
816
+ borderRadius: '4px',
817
+ fontFamily: 'monospace'
818
+ }}>
819
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
820
+ {previewContent}
821
+ </pre>
822
+ </div>
823
+ </Modal>
824
+ </div>
825
+ </AppLayout>
826
+ );
827
+ };
828
+
829
+ export default InterfaceManagement;