whistle.mock-plugins 1.0.6-beta.2 → 1.0.6-beta.21

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.
@@ -1,70 +1,57 @@
1
- import React, { useState, useEffect, useRef } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import { useParams, useHistory } from 'react-router-dom';
3
3
  import AppLayout from '../components/AppLayout';
4
- import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs, Card, Divider, Badge, Tooltip, Row, Col } from 'antd';
5
- import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined, CopyOutlined, CodeOutlined, CheckCircleOutlined, CloseCircleOutlined, MenuOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
4
+ import {
5
+ Table, Button, Modal, Form, Input, Select, message, Switch,
6
+ Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col
7
+ } from 'antd';
8
+ import {
9
+ PlusOutlined, EditOutlined, DeleteOutlined,
10
+ PlayCircleOutlined, FileTextOutlined, PlusCircleOutlined
11
+ } from '@ant-design/icons';
6
12
  import '../styles/interface-management.css';
7
13
  import axios from 'axios';
8
14
 
15
+ // 导入拆分后的组件
16
+ import {
17
+ ResponseContentEditor,
18
+ InterfaceTestModal,
19
+ PreviewModal,
20
+ contentTypes,
21
+ proxyTypes,
22
+ statusCodes,
23
+ httpMethods,
24
+ refreshCacheAfterUpdate,
25
+ formatResponseContent,
26
+ generateResponseId
27
+ } from '../components/interface-management';
28
+
9
29
  const { Option } = Select;
10
- const { TextArea } = Input;
11
- const { TabPane } = Tabs;
12
30
 
13
31
  const InterfaceManagement = () => {
14
32
  const { featureId } = useParams();
15
33
  const history = useHistory();
34
+
35
+ // 基础状态
16
36
  const [features, setFeatures] = useState([]);
17
37
  const [interfaces, setInterfaces] = useState([]);
18
38
  const [loading, setLoading] = useState(false);
19
39
  const [featuresLoading, setFeaturesLoading] = useState(false);
20
40
  const [interfacesLoading, setInterfacesLoading] = useState(false);
41
+ const [selectedFeatureId, setSelectedFeatureId] = useState(null);
42
+
43
+ // 模态框状态
21
44
  const [modalVisible, setModalVisible] = useState(false);
22
- const [editingInterface, setEditingInterface] = useState(null);
23
45
  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
46
  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 proxyTypes = [
42
- { value: 'response', label: '模拟响应' },
43
- { value: 'redirect', label: '重定向' },
44
- { value: 'url_redirect', label: 'URL重定向' },
45
- ];
46
-
47
- const statusCodes = [
48
- { value: '200', label: '200 OK' },
49
- { value: '201', label: '201 Created' },
50
- { value: '204', label: '204 No Content' },
51
- { value: '400', label: '400 Bad Request' },
52
- { value: '401', label: '401 Unauthorized' },
53
- { value: '403', label: '403 Forbidden' },
54
- { value: '404', label: '404 Not Found' },
55
- { value: '500', label: '500 Internal Server Error' },
56
- ];
57
-
58
- const httpMethods = [
59
- { value: 'ALL', label: '所有方法' },
60
- { value: 'GET', label: 'GET' },
61
- { value: 'POST', label: 'POST' },
62
- { value: 'PUT', label: 'PUT' },
63
- { value: 'DELETE', label: 'DELETE' },
64
- { value: 'PATCH', label: 'PATCH' },
65
- { value: 'HEAD', label: 'HEAD' },
66
- { value: 'OPTIONS', label: 'OPTIONS' },
67
- ];
47
+
48
+ // 编辑状态
49
+ const [editingInterface, setEditingInterface] = useState(null);
50
+ const [previewContent, setPreviewContent] = useState(null);
51
+ const [currentResponseId, setCurrentResponseId] = useState(null);
52
+
53
+ // 表单实例
54
+ const [form] = Form.useForm();
68
55
 
69
56
  useEffect(() => {
70
57
  fetchFeatures();
@@ -110,9 +97,38 @@ const InterfaceManagement = () => {
110
97
  try {
111
98
  setInterfacesLoading(true);
112
99
  const response = await axios.get(`/cgi-bin/interfaces?featureId=${selectedFeatureId}`);
100
+
101
+ console.log('获取接口列表响应:', response.data);
102
+
113
103
  if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
114
- setInterfaces(response.data.data);
104
+ // 确保每个接口的响应数据格式正确
105
+ const processedInterfaces = response.data.data.map(item => {
106
+ console.log(`处理接口 ${item.name}:`, {
107
+ hasResponses: !!item.responses,
108
+ responsesLength: item.responses ? item.responses.length : 0,
109
+ activeResponseId: item.activeResponseId
110
+ });
111
+
112
+ // 确保 responses 是数组
113
+ if (!Array.isArray(item.responses)) {
114
+ item.responses = [];
115
+ }
116
+
117
+ // 确保每个响应都有必要的字段
118
+ item.responses = item.responses.map((resp, index) => ({
119
+ id: resp.id || `resp-${index}-${Date.now()}`,
120
+ name: resp.name || `响应 ${index + 1}`,
121
+ description: resp.description || '',
122
+ content: resp.content || '{}'
123
+ }));
124
+
125
+ return item;
126
+ });
127
+
128
+ setInterfaces(processedInterfaces);
129
+ console.log('处理后的接口列表:', processedInterfaces);
115
130
  } else {
131
+ console.warn('接口数据格式不正确:', response.data);
116
132
  setInterfaces([]);
117
133
  message.warning('获取接口配置数据格式不正确');
118
134
  }
@@ -131,11 +147,34 @@ const InterfaceManagement = () => {
131
147
  return;
132
148
  }
133
149
  form.resetFields();
150
+
151
+ // 创建默认响应
152
+ const defaultResponseId = generateResponseId();
153
+ const defaultResponses = [{
154
+ id: defaultResponseId,
155
+ name: '默认响应',
156
+ description: '',
157
+ content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
158
+ }];
159
+
160
+ console.log('创建新接口,初始化响应数据:', defaultResponses);
161
+
162
+ form.setFieldsValue({
163
+ proxyType: 'response',
164
+ statusCode: '200',
165
+ contentType: 'application/json; charset=utf-8',
166
+ responses: defaultResponses,
167
+ activeResponseId: defaultResponseId,
168
+ httpMethod: 'ALL',
169
+ });
170
+
171
+ setCurrentResponseId(defaultResponseId);
134
172
  setEditingInterface(null);
135
173
  setModalVisible(true);
136
174
  };
137
175
 
138
176
  const handleEditInterface = (record) => {
177
+ console.log('开始编辑接口:', record.name, '原始数据:', record);
139
178
  setEditingInterface(record);
140
179
 
141
180
  // 将自定义请求头转换为数组格式,用于动态表单项
@@ -146,6 +185,42 @@ const InterfaceManagement = () => {
146
185
  headerValue: value
147
186
  }));
148
187
  }
188
+
189
+ // 处理多响应数据
190
+ let initialResponses = [];
191
+ let initialResponseId = null;
192
+
193
+ // 深拷贝记录中的响应数据,避免引用问题
194
+ if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
195
+ console.log('使用现有响应数据, 数量:', record.responses.length);
196
+
197
+ initialResponses = JSON.parse(JSON.stringify(record.responses));
198
+ initialResponseId = record.activeResponseId || record.responses[0].id;
199
+
200
+ // 确保每个响应都有名称
201
+ initialResponses = initialResponses.map((resp, index) => ({
202
+ id: resp.id || generateResponseId(),
203
+ name: resp.name || `响应 ${index + 1}`,
204
+ description: resp.description || '',
205
+ content: resp.content || '{}'
206
+ }));
207
+ } else if (record.responseContent) {
208
+ // 向后兼容:如果只有传统的 responseContent 字段,创建一个默认响应
209
+ console.log('创建默认响应,使用responseContent字段');
210
+ const defaultResponseId = generateResponseId();
211
+ initialResponses = [
212
+ {
213
+ id: defaultResponseId,
214
+ name: '默认响应',
215
+ description: '',
216
+ content: record.responseContent
217
+ }
218
+ ];
219
+ initialResponseId = defaultResponseId;
220
+ }
221
+
222
+ console.log('编辑接口,设置响应数据:', initialResponses);
223
+ setCurrentResponseId(initialResponseId);
149
224
 
150
225
  form.setFieldsValue({
151
226
  name: record.name,
@@ -153,7 +228,8 @@ const InterfaceManagement = () => {
153
228
  proxyType: record.proxyType || 'response',
154
229
  statusCode: record.httpStatus?.toString() || '200',
155
230
  contentType: record.contentType || 'application/json; charset=utf-8',
156
- responseBody: record.responseContent || '',
231
+ responses: initialResponses,
232
+ activeResponseId: initialResponseId,
157
233
  httpMethod: record.httpMethod || 'ALL',
158
234
  targetUrl: record.targetUrl || '',
159
235
  headerItems: headersArray // 使用数组存储表单项
@@ -196,63 +272,93 @@ const InterfaceManagement = () => {
196
272
  }
197
273
  };
198
274
 
199
- const cleanJsonResponse = (jsonStr) => {
200
- try {
201
- const parsed = JSON.parse(jsonStr);
202
- return JSON.stringify(parsed);
203
- } catch (e) {
204
- return jsonStr;
205
- }
206
- };
207
-
208
- // 验证URL是否合法
209
- const isValidUrl = (url) => {
210
- if (!url) return false;
211
-
212
- try {
213
- new URL(url);
214
- return true;
215
- } catch (e) {
216
- return false;
217
- }
218
- };
219
-
220
- // 缓存刷新服务
221
- const flushCache = async () => {
222
- try {
223
- const response = await axios.get('/_flush_cache');
224
- return response.data;
225
- } catch (error) {
226
- console.error('刷新缓存失败:', error);
227
- throw error;
228
- }
229
- };
230
-
231
- // 更新成功后刷新缓存
232
- const refreshCacheAfterUpdate = async () => {
233
- try {
234
- await flushCache();
235
- // 这里不需要显示提示,因为主要操作会有自己的提示
236
- } catch (error) {
237
- // 如果刷新缓存失败,记录错误但不影响用户体验
238
- console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
239
- }
240
- };
241
-
242
275
  const handleSubmit = async () => {
243
276
  try {
244
277
  const values = await form.validateFields();
278
+ console.log('=== 开始表单提交 ===');
279
+ console.log('表单提交,原始值:', JSON.stringify(values, null, 2));
245
280
 
246
- // 检查responseBody是否是有效的JSON
247
- if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
281
+ // 获取当前激活的响应ID和所有响应
282
+ const activeResponseId = values.activeResponseId;
283
+ let responses = values.responses || [];
284
+
285
+ console.log('提取的原始响应数据:', responses);
286
+ console.log('响应数据类型:', typeof responses);
287
+ console.log('是否为数组:', Array.isArray(responses));
288
+
289
+ // 确保responses是数组格式
290
+ if (!Array.isArray(responses)) {
291
+ console.log('响应数据不是数组,尝试解析...');
248
292
  try {
249
- JSON.parse(values.responseBody);
293
+ if (typeof responses === 'string') {
294
+ responses = JSON.parse(responses);
295
+ if (!Array.isArray(responses)) {
296
+ responses = [];
297
+ }
298
+ } else {
299
+ responses = [];
300
+ }
250
301
  } catch (e) {
251
- message.error('响应体不是有效的JSON格式');
252
- return;
302
+ console.error('解析响应数据失败:', e);
303
+ responses = [];
253
304
  }
254
305
  }
255
306
 
307
+ console.log('处理前的响应数据:', JSON.stringify(responses, null, 2));
308
+
309
+ // 确保每个响应都有名称、描述和内容
310
+ const cleanedResponses = responses.map((resp, index) => {
311
+ console.log(`处理响应 ${index}:`, resp);
312
+ const cleaned = {
313
+ id: resp.id || generateResponseId(),
314
+ name: resp.name || `响应 ${index + 1}`,
315
+ description: resp.description || '',
316
+ content: resp.content || '{}'
317
+ };
318
+ console.log(`处理后的响应 ${index}:`, cleaned);
319
+ return cleaned;
320
+ });
321
+
322
+ // 如果没有响应,创建一个默认响应
323
+ if (cleanedResponses.length === 0) {
324
+ console.log('没有响应数据,创建默认响应');
325
+ const defaultId = generateResponseId();
326
+ cleanedResponses.push({
327
+ id: defaultId,
328
+ name: '默认响应',
329
+ description: '',
330
+ content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
331
+ });
332
+ setCurrentResponseId(defaultId);
333
+ }
334
+
335
+ // 确保有一个激活的响应ID
336
+ const validActiveResponseId = activeResponseId && cleanedResponses.some(r => r.id === activeResponseId)
337
+ ? activeResponseId
338
+ : cleanedResponses[0].id;
339
+
340
+ // 获取激活的响应
341
+ const activeResponse = cleanedResponses.find(r => r.id === validActiveResponseId) || cleanedResponses[0];
342
+
343
+ console.log('=== 清理后的响应数据 ===');
344
+ console.log('清理后的响应数据:', JSON.stringify(cleanedResponses, null, 2));
345
+ console.log('激活的响应ID:', validActiveResponseId);
346
+ console.log('激活的响应:', activeResponse);
347
+
348
+ // 检查responseBody是否是有效的JSON
349
+ if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
350
+ cleanedResponses.forEach(response => {
351
+ try {
352
+ if (response.content) {
353
+ JSON.parse(response.content);
354
+ }
355
+ } catch (e) {
356
+ message.error(`响应 "${response.name}" 不是有效的JSON格式`);
357
+ throw new Error(`响应 "${response.name}" 不是有效的JSON格式`);
358
+ }
359
+ });
360
+ }
361
+
256
362
  // 处理自定义请求头,将表单项数组转换为对象格式
257
363
  let customHeaders = {};
258
364
 
@@ -271,7 +377,11 @@ const InterfaceManagement = () => {
271
377
  featureId: selectedFeatureId,
272
378
  urlPattern: values.pattern,
273
379
  proxyType: values.proxyType,
274
- responseContent: values.proxyType === 'response' ? values.responseBody : '',
380
+ // 同时保存所有响应和当前活跃的响应
381
+ responses: cleanedResponses,
382
+ activeResponseId: validActiveResponseId,
383
+ // 兼容性保留:将当前活跃响应的内容保存到 responseContent
384
+ responseContent: activeResponse ? activeResponse.content : '',
275
385
  targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
276
386
  customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
277
387
  httpStatus: parseInt(values.statusCode, 10), // 转换为数字
@@ -281,16 +391,24 @@ const InterfaceManagement = () => {
281
391
  active: true
282
392
  };
283
393
 
394
+ console.log('=== 最终提交的接口数据 ===');
395
+ console.log('提交的接口数据:', JSON.stringify(interfaceData, null, 2));
396
+
284
397
  let response;
285
398
  if (editingInterface) {
286
399
  // 更新现有接口
400
+ console.log('执行接口更新,ID:', editingInterface.id);
287
401
  response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
288
402
  } else {
289
403
  // 创建新接口
404
+ console.log('执行接口创建');
290
405
  response = await axios.post('/cgi-bin/interfaces', interfaceData);
291
406
  }
292
407
 
408
+ console.log('服务器响应:', response.data);
409
+
293
410
  if (response.data && response.data.code === 0) {
411
+ console.log('=== 接口操作成功 ===');
294
412
  message.success(editingInterface ? '接口更新成功' : '接口创建成功');
295
413
  setModalVisible(false);
296
414
  fetchInterfaces();
@@ -301,6 +419,11 @@ const InterfaceManagement = () => {
301
419
  throw new Error(response.data?.message || '操作失败');
302
420
  }
303
421
  } catch (error) {
422
+ if (error.message && error.message.includes('JSON格式')) {
423
+ // 已经显示了错误信息,不需要再显示
424
+ return;
425
+ }
426
+ console.error('=== 接口操作失败 ===');
304
427
  console.error('操作失败:', error);
305
428
  message.error(error.response?.data?.message || error.message || '操作失败');
306
429
  }
@@ -312,188 +435,41 @@ const InterfaceManagement = () => {
312
435
 
313
436
  const handleTestInterface = async (record) => {
314
437
  setEditingInterface(record);
315
- setTestUrl('');
316
- setTestResult(null);
317
- setTestLoading(false);
318
438
  setTestModalVisible(true);
319
- testForm.resetFields();
320
- };
321
-
322
- // 检查 URL 是否匹配模式
323
- const isUrlMatchPattern = (url, pattern, proxyType) => {
324
- if (!url || !pattern) return false;
325
-
326
- try {
327
- // 对于url_redirect类型,需要完全匹配
328
- if (proxyType === 'url_redirect') {
329
- return url === pattern;
330
- }
331
-
332
- // 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
333
- if (proxyType === 'redirect') {
334
- return url.indexOf(pattern) === 0;
335
- }
336
-
337
- // 以下是默认的匹配逻辑(用于response类型等)
338
- // 如果是正则表达式
339
- if (pattern.startsWith('/') && pattern.endsWith('/')) {
340
- const regex = new RegExp(pattern.slice(1, -1));
341
- return regex.test(url);
342
- }
343
-
344
- // 如果是通配符模式
345
- if (pattern.includes('*')) {
346
- const regexPattern = pattern
347
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
348
- .replace(/\*/g, '.*');
349
- const regex = new RegExp(`^${regexPattern}$`);
350
- return regex.test(url);
351
- }
352
-
353
- // 精确匹配
354
- return url === pattern;
355
- } catch (e) {
356
- console.error('URL匹配检查失败:', e);
357
- return false;
358
- }
359
- };
360
-
361
- const handleTestSubmit = async () => {
362
- try {
363
- const values = await testForm.validateFields();
364
- const { testUrl } = values;
365
-
366
- if (!testUrl) {
367
- message.error('请输入测试URL');
368
- return;
369
- }
370
-
371
- setTestLoading(true);
372
-
373
- try {
374
- // 测试前刷新缓存,确保使用最新的接口定义
375
- await refreshCacheAfterUpdate();
376
- } catch (error) {
377
- // 即使刷新缓存失败,也继续测试
378
- console.warn('刷新缓存失败,将使用现有的缓存数据:', error);
379
- }
380
-
381
- try {
382
- // 验证测试 URL 是否匹配当前接口的匹配规则
383
- if (!isUrlMatchPattern(testUrl, editingInterface.urlPattern, editingInterface.proxyType)) {
384
- let errorMessage = '测试URL与接口匹配规则不匹配';
385
-
386
- // 根据不同的代理类型提供更具体的错误信息
387
- if (editingInterface.proxyType === 'url_redirect') {
388
- errorMessage = `测试URL必须完全匹配 ${editingInterface.urlPattern}`;
389
- } else if (editingInterface.proxyType === 'redirect') {
390
- errorMessage = `测试URL必须以 ${editingInterface.urlPattern} 开头`;
391
- }
392
-
393
- setTestResult({
394
- success: false,
395
- error: errorMessage
396
- });
397
- message.error(errorMessage);
398
- setTestLoading(false);
399
- return;
400
- }
401
-
402
- // 调用实际测试接口
403
- const response = await axios.post(`/cgi-bin/test-interface`, {
404
- url: testUrl,
405
- interfaceId: editingInterface.id
406
- });
407
-
408
- if (response.data && response.data.code === 0) {
409
- // 设置测试结果
410
- setTestResult({
411
- success: true,
412
- ...response.data.data,
413
- matchedRule: editingInterface.urlPattern,
414
- httpMethod: editingInterface.httpMethod,
415
- requestUrl: testUrl,
416
- mockInfo: response.data.data.mockInfo
417
- });
418
- } else {
419
- throw new Error(response.data?.message || '测试响应格式不正确');
420
- }
421
- } catch (error) {
422
- setTestResult({
423
- success: false,
424
- error: error.response?.data?.message || error.message || '测试失败'
425
- });
426
- message.error(error.response?.data?.message || error.message || '测试失败');
427
- }
428
-
429
- setTestLoading(false);
430
- } catch (error) {
431
- if (error.errorFields) {
432
- return; // 表单验证错误
433
- }
434
- console.error('测试失败:', error);
435
- message.error(error.message || '测试失败');
436
- setTestLoading(false);
437
- }
438
439
  };
439
440
 
440
441
  const handleSelectFeature = (featureId) => {
441
442
  setSelectedFeatureId(featureId);
442
443
  };
443
444
 
444
- const formatResponseContent = (content, contentType) => {
445
- if (contentType && contentType.includes('json')) {
446
- try {
447
- const parsed = JSON.parse(content);
448
- return JSON.stringify(parsed, null, 2);
449
- } catch (e) {
450
- return content;
451
- }
452
- }
453
- return content;
454
- };
455
-
456
- const formatJsonContent = () => {
457
- const proxyType = form.getFieldValue('proxyType');
458
- if (proxyType !== 'response') {
445
+ // 预览响应内容
446
+ const handlePreview = () => {
447
+ // 获取当前选中的响应内容
448
+ const responses = form.getFieldValue('responses') || [];
449
+ const activeResponseId = form.getFieldValue('activeResponseId');
450
+ const activeResponse = responses.find(r => r.id === activeResponseId);
451
+
452
+ if (!activeResponse) {
453
+ message.error('未找到有效的响应内容');
459
454
  return;
460
455
  }
456
+
457
+ // 根据内容类型格式化响应内容
458
+ const contentType = form.getFieldValue('contentType') || '';
459
+ const formattedContent = formatResponseContent(activeResponse.content, contentType);
461
460
 
462
- const responseBody = form.getFieldValue('responseBody');
463
- if (responseBody) {
464
- try {
465
- const formattedJson = JSON.stringify(JSON.parse(responseBody), null, 2);
466
- form.setFieldsValue({ responseBody: formattedJson });
467
- message.success('JSON格式化成功');
468
- } catch (error) {
469
- message.error('JSON格式不正确,无法格式化');
470
- }
471
- }
461
+ // 设置预览内容,包含响应名称
462
+ setPreviewContent({
463
+ title: `预览: ${activeResponse.name || '未命名响应'}`,
464
+ content: formattedContent,
465
+ description: '',
466
+ contentType
467
+ });
468
+ setPreviewVisible(true);
472
469
  };
473
470
 
474
- const handlePreview = () => {
475
- const proxyType = form.getFieldValue('proxyType');
476
- if (proxyType !== 'response') {
477
- return;
478
- }
479
-
480
- const responseBody = form.getFieldValue('responseBody');
481
- if (responseBody) {
482
- try {
483
- // 如果是JSON,格式化显示
484
- const contentType = form.getFieldValue('contentType');
485
- if (contentType && contentType.includes('json')) {
486
- setPreviewContent(JSON.stringify(JSON.parse(responseBody), null, 2));
487
- } else {
488
- setPreviewContent(responseBody);
489
- }
490
- setPreviewVisible(true);
491
- } catch (error) {
492
- message.error('内容格式不正确,无法预览');
493
- }
494
- } else {
495
- message.warning('响应内容为空,无法预览');
496
- }
471
+ const handleResponseSelect = (responseId) => {
472
+ setCurrentResponseId(responseId);
497
473
  };
498
474
 
499
475
  const filteredInterfaces = interfaces.filter(item =>
@@ -678,6 +654,7 @@ const InterfaceManagement = () => {
678
654
  添加接口
679
655
  </Button>
680
656
  </div>
657
+
681
658
  {!features.length && (
682
659
  <Alert
683
660
  message="未找到功能模块"
@@ -709,6 +686,7 @@ const InterfaceManagement = () => {
709
686
  />
710
687
  </div>
711
688
 
689
+ {/* 接口编辑/创建模态框 */}
712
690
  <Modal
713
691
  title={editingInterface ? '编辑接口' : '添加接口'}
714
692
  open={modalVisible}
@@ -729,12 +707,13 @@ const InterfaceManagement = () => {
729
707
  proxyType: 'response',
730
708
  statusCode: '200',
731
709
  contentType: 'application/json; charset=utf-8',
732
- responseBody: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
710
+ responses: [],
733
711
  httpMethod: 'ALL',
734
712
  targetUrl: '',
735
713
  headerItems: []
736
714
  }}
737
715
  >
716
+ {/* 基础信息表单项保持原样 */}
738
717
  <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
739
718
  <div style={{ flex: 1 }}>
740
719
  <Form.Item
@@ -885,7 +864,7 @@ const InterfaceManagement = () => {
885
864
 
886
865
  {/* 根据proxyType显示不同的表单项 */}
887
866
  <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
888
- {({ getFieldValue }) => {
867
+ {({ getFieldValue, setFieldsValue }) => {
889
868
  const proxyType = getFieldValue('proxyType');
890
869
 
891
870
  if (proxyType === 'response') {
@@ -920,453 +899,69 @@ const InterfaceManagement = () => {
920
899
  </div>
921
900
  </div>
922
901
 
923
- <Form.Item
924
- name="responseBody"
925
- labelCol={{
926
- span: 8, /* 宽度比例 */
927
- }}
928
- label={
929
- <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
930
- <span>响应内容</span>
931
- <Space>
932
- <Button
933
- type="link"
934
- icon={<EyeOutlined />}
935
- onClick={handlePreview}
936
- style={{ padding: 0 }}
937
- >
938
- 预览
939
- </Button>
940
- <Button
941
- type="link"
942
- icon={<FormatPainterOutlined />}
943
- onClick={formatJsonContent}
944
- style={{ padding: 0 }}
945
- >
946
- 格式化JSON
947
- </Button>
948
- </Space>
949
- </div>
950
- }
951
- rules={[{ required: true, message: '请输入响应内容' }]}
902
+ {/* 隐藏的表单字段用于存储响应数据 */}
903
+ <Form.Item
904
+ name="responses"
905
+ initialValue={[]}
906
+ hidden
952
907
  >
953
- <TextArea rows={12} placeholder="请输入响应内容" style={{ fontFamily: 'monospace' }} />
908
+ <Input />
954
909
  </Form.Item>
955
- </>
956
- );
957
- }
958
-
959
- if (proxyType === 'redirect' || proxyType === 'url_redirect') {
960
- return (
961
- <>
962
- <Form.Item
963
- name="targetUrl"
964
- label="重定向目标URL"
965
- rules={[
966
- { required: true, message: '请输入重定向目标URL' },
967
- {
968
- validator: (_, value) => {
969
- if (!value) return Promise.resolve();
970
-
971
- try {
972
- new URL(value);
973
- return Promise.resolve();
974
- } catch (e) {
975
- return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
910
+ <Form.Item name="activeResponseId" hidden>
911
+ <Input />
912
+ </Form.Item>
913
+
914
+ {/* 使用新的合并组件 */}
915
+ <Form.Item noStyle shouldUpdate>
916
+ {({ getFieldValue }) => {
917
+ let responses = getFieldValue('responses') || [];
918
+ const activeResponseId = getFieldValue('activeResponseId');
919
+
920
+ // 确保是数组
921
+ if (!Array.isArray(responses)) {
922
+ try {
923
+ if (typeof responses === 'string') {
924
+ responses = JSON.parse(responses);
976
925
  }
926
+ } catch (e) {
927
+ responses = [];
977
928
  }
978
929
  }
979
- ]}
980
- tooltip={proxyType === 'redirect' ?
981
- "重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
982
- "URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
983
- }
984
- >
985
- <Input
986
- placeholder={proxyType === 'redirect' ?
987
- "例如:https://example.com/api" :
988
- "例如:https://example.com/api/users"
989
- }
990
- addonBefore={
991
- <Select
992
- defaultValue="https://"
993
- className="select-before"
994
- style={{ width: 100 }}
995
- onChange={(value) => {
996
- const currentUrl = form.getFieldValue('targetUrl') || '';
997
- const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
998
- form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
999
- }}
1000
- >
1001
- <Option value="http://">http://</Option>
1002
- <Option value="https://">https://</Option>
1003
- </Select>
1004
- }
1005
- />
1006
- </Form.Item>
1007
-
1008
- <Form.Item
1009
- label="自定义请求头"
1010
- tooltip="为请求添加或替换HTTP请求头"
1011
- className="custom-headers-container"
1012
- >
1013
- <Form.List name="headerItems">
1014
- {(fields, { add, remove }) => (
1015
- <>
1016
- {fields.map(({ key, name, ...restField }) => (
1017
- <Row key={key} gutter={8} style={{ marginBottom: 8 }}>
1018
- <Col span={10}>
1019
- <Form.Item
1020
- {...restField}
1021
- name={[name, 'headerName']}
1022
- noStyle
1023
- rules={[
1024
- {
1025
- required: true,
1026
- message: '请输入请求头名称'
1027
- },
1028
- {
1029
- pattern: /^[^:]+$/,
1030
- message: '请求头名称不能包含冒号'
1031
- }
1032
- ]}
1033
- >
1034
- <Input placeholder="Header-Name" />
1035
- </Form.Item>
1036
- </Col>
1037
- <Col span={12}>
1038
- <Form.Item
1039
- {...restField}
1040
- name={[name, 'headerValue']}
1041
- noStyle
1042
- rules={[
1043
- {
1044
- validator: (_, value) => {
1045
- if (!value) return Promise.resolve();
1046
-
1047
- // 检查是否是随机数格式 @xxxx-xxx
1048
- if (value.startsWith('@')) {
1049
- const randomPattern = value.substring(1);
1050
- // 验证格式: 只能包含x和-,且首尾不能是-
1051
- if (!/^[x][-x]*[x]$/.test(randomPattern) ||
1052
- randomPattern.startsWith('-') ||
1053
- randomPattern.endsWith('-')) {
1054
- return Promise.reject(
1055
- new Error('随机数格式错误,应为@xxxx-xxx格式,只能包含x和-,且首尾不能是-')
1056
- );
1057
- }
1058
- }
1059
- return Promise.resolve();
1060
- }
1061
- }
1062
- ]}
1063
- >
1064
- <Input placeholder="Header-Value" />
1065
- </Form.Item>
1066
- </Col>
1067
- <Col span={2}>
1068
- <Button
1069
- type="text"
1070
- icon={<MinusCircleOutlined />}
1071
- onClick={() => remove(name)}
1072
- style={{ color: '#ff4d4f' }}
1073
- />
1074
- </Col>
1075
- </Row>
1076
- ))}
1077
-
1078
- <Form.Item>
1079
- <Button
1080
- type="dashed"
1081
- onClick={() => add()}
1082
- block
1083
- icon={<PlusOutlined />}
1084
- >
1085
- 添加请求头
1086
- </Button>
1087
- </Form.Item>
1088
-
1089
- <div style={{ marginTop: 8 }}>
1090
- <Button
1091
- type="link"
1092
- onClick={() => {
1093
- add({ headerName: 'Content-Type', headerValue: 'application/json' });
1094
- }}
1095
- >
1096
- 添加 Content-Type
1097
- </Button>
1098
- <Button
1099
- type="link"
1100
- onClick={() => {
1101
- add({ headerName: 'Authorization', headerValue: 'Bearer ' });
1102
- }}
1103
- >
1104
- 添加 Authorization
1105
- </Button>
1106
- <Button
1107
- type="link"
1108
- onClick={() => {
1109
- add({ headerName: 'User-Agent', headerValue: 'Mozilla/5.0' });
1110
- }}
1111
- >
1112
- 添加 User-Agent
1113
- </Button>
1114
- <Button
1115
- type="link"
1116
- onClick={() => {
1117
- add({ headerName: 'X-Random-ID', headerValue: '@xxxx-xxxx' });
1118
- }}
1119
- >
1120
- 添加随机ID
1121
- </Button>
1122
- </div>
1123
-
1124
- <div style={{ marginTop: 8, background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
1125
- <div style={{ marginBottom: '8px', fontWeight: 'bold' }}>随机数格式说明:</div>
1126
- <ul style={{ margin: 0, paddingLeft: '20px' }}>
1127
- <li>使用 <code>@</code> 开头表示这是一个随机数值</li>
1128
- <li>格式示例:<code>@xxxx-xxxx</code> 将生成如 <code>a1b2-c3d4</code> 的随机值</li>
1129
- <li>每个 <code>x</code> 将替换为随机字母或数字</li>
1130
- <li><code>-</code> 将保留在输出中作为分隔符</li>
1131
- </ul>
1132
- </div>
1133
- </>
1134
- )}
1135
- </Form.List>
930
+
931
+ return (
932
+ <ResponseContentEditor
933
+ form={form}
934
+ responses={responses}
935
+ activeResponseId={activeResponseId}
936
+ onPreview={handlePreview}
937
+ />
938
+ );
939
+ }}
1136
940
  </Form.Item>
1137
941
  </>
1138
942
  );
1139
943
  }
1140
944
 
945
+ // 这里可以添加其他 proxyType 的处理逻辑
1141
946
  return null;
1142
947
  }}
1143
948
  </Form.Item>
1144
949
  </Form>
1145
950
  </Modal>
1146
951
 
1147
- <Modal
1148
- title="测试接口"
1149
- open={testModalVisible}
952
+ {/* 使用拆分后的测试模态框组件 */}
953
+ <InterfaceTestModal
954
+ visible={testModalVisible}
1150
955
  onCancel={() => setTestModalVisible(false)}
1151
- footer={[
1152
- <Button key="back" onClick={() => setTestModalVisible(false)}>
1153
- 关闭
1154
- </Button>,
1155
- <Button key="submit" type="primary" loading={testLoading} onClick={handleTestSubmit}>
1156
- 测试
1157
- </Button>
1158
- ]}
1159
- width={800}
1160
- >
1161
- {editingInterface && (
1162
- <div className="test-interface-container">
1163
- <Alert
1164
- message="URL匹配规则说明"
1165
- description={
1166
- <div>
1167
- <p>当前接口匹配规则:<code>{editingInterface.urlPattern}</code></p>
1168
- <p>代理类型:{
1169
- proxyTypes.find(item => item.value === editingInterface.proxyType)?.label ||
1170
- editingInterface.proxyType || '模拟响应'
1171
- }</p>
1172
-
1173
- {editingInterface.proxyType === 'url_redirect' && (
1174
- <div>
1175
- <p><b>URL重定向模式规则:</b></p>
1176
- <ul>
1177
- <li>URL匹配规则必须是完整URL(包括http://或https://)</li>
1178
- <li>需要完全匹配URL路径,测试URL必须与匹配规则<b>完全一致</b></li>
1179
- <li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
1180
- </ul>
1181
- </div>
1182
- )}
1183
-
1184
- {editingInterface.proxyType === 'redirect' && (
1185
- <div>
1186
- <p><b>重定向模式规则:</b></p>
1187
- <ul>
1188
- <li>URL匹配规则必须以http://或https://开头</li>
1189
- <li>测试URL必须以匹配规则开头(<b>前缀匹配</b>)</li>
1190
- <li>命中后将直接跳转到目标URL: {editingInterface.targetUrl}</li>
1191
- </ul>
1192
- </div>
1193
- )}
1194
-
1195
- {editingInterface.proxyType === 'response' && (
1196
- <div>
1197
- <p>在Whistle规则中,您需要配置完整URL或域名指向whistle.mock-plugins://,然后插件会根据接口的匹配规则处理请求。</p>
1198
- <p>插件支持以下匹配方式:</p>
1199
- <ul>
1200
- <li>精确匹配:完全匹配URL路径部分</li>
1201
- <li>通配符匹配:使用*代表任意字符(例如:/api/users/*)</li>
1202
- <li>正则表达式:使用/pattern/格式(例如:/\/api\/users\/\d+/)</li>
1203
- </ul>
1204
- </div>
1205
- )}
1206
-
1207
- <p>请在下方输入完整测试URL进行测试</p>
1208
- </div>
1209
- }
1210
- type="info"
1211
- showIcon
1212
- style={{ marginBottom: 16 }}
1213
- />
1214
-
1215
- <Form
1216
- form={testForm}
1217
- layout="vertical"
1218
- >
1219
- <Form.Item
1220
- name="testUrl"
1221
- label="测试URL"
1222
- rules={[{ required: true, message: '请输入测试URL' }]}
1223
- >
1224
- <Input
1225
- placeholder="输入完整URL,例如:https://example.com/api/users"
1226
- onChange={(e) => setTestUrl(e.target.value)}
1227
- />
1228
- </Form.Item>
1229
- </Form>
1230
-
1231
- {testResult && (
1232
- <div className="test-result">
1233
- <div className="result-header">
1234
- <h3>测试结果</h3>
1235
- {testResult.success ? (
1236
- <span className="status success">成功</span>
1237
- ) : (
1238
- <span className="status error">失败</span>
1239
- )}
1240
- </div>
1241
-
1242
- {testResult.success ? (
1243
- <div className="result-content">
1244
- <div className="result-item">
1245
- <span className="label">请求URL:</span>
1246
- <span className="value">{testResult.requestUrl}</span>
1247
- </div>
1248
- <div className="result-item">
1249
- <span className="label">匹配规则:</span>
1250
- <span className="value">{testResult.matchedRule}</span>
1251
- </div>
1252
- <div className="result-item">
1253
- <span className="label">请求方法:</span>
1254
- <span className="value">{testResult.httpMethod || 'ALL'}</span>
1255
- </div>
1256
- <div className="result-item">
1257
- <span className="label">处理方式:</span>
1258
- <span className="value">
1259
- {(() => {
1260
- const found = proxyTypes.find(item => item.value === editingInterface.proxyType);
1261
- return found ? found.label : editingInterface.proxyType || '模拟响应';
1262
- })()}
1263
- </span>
1264
- </div>
1265
-
1266
- {(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') && (
1267
- <div className="result-item">
1268
- <span className="label">重定向目标:</span>
1269
- <span className="value">{testResult.targetUrl || editingInterface.targetUrl}</span>
1270
- </div>
1271
- )}
1272
-
1273
- {(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') &&
1274
- testResult.formattedHeaders && (
1275
- <div className="result-item">
1276
- <span className="label">自定义请求头:</span>
1277
- <div className="value" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
1278
- {
1279
- editingInterface.customHeaders &&
1280
- Object.entries(editingInterface.customHeaders)
1281
- .map(([key, value]) => {
1282
- const generatedValue = testResult.customHeaders[key];
1283
- return (
1284
- <div key={key}>
1285
- {key}: {value.startsWith('@') ? (
1286
- <span>
1287
- <span style={{ color: '#d9d9d9' }}>{value}</span>
1288
- {' '} → {' '}
1289
- <span style={{ color: '#52c41a', fontWeight: 'bold' }}>{generatedValue}</span>
1290
- <span style={{ color: '#8c8c8c', fontSize: '12px' }}> (随机生成)</span>
1291
- </span>
1292
- ) : generatedValue}
1293
- </div>
1294
- );
1295
- })
1296
- }
1297
- </div>
1298
- </div>
1299
- )}
1300
-
1301
- {editingInterface.proxyType === 'response' && (
1302
- <>
1303
- <div className="result-item">
1304
- <span className="label">状态码:</span>
1305
- <span className="value">{testResult.statusCode}</span>
1306
- </div>
1307
- <div className="result-item">
1308
- <span className="label">内容类型:</span>
1309
- <span className="value">{testResult.contentType}</span>
1310
- </div>
1311
- </>
1312
- )}
1313
-
1314
- {testResult.mockInfo && (
1315
- <>
1316
- <div className="result-item">
1317
- <span className="label">模拟延迟:</span>
1318
- <span className="value">{testResult.mockInfo.delay}ms</span>
1319
- </div>
1320
- <div className="result-item">
1321
- <span className="label">响应时间:</span>
1322
- <span className="value">{new Date(testResult.mockInfo.timestamp).toLocaleString()}</span>
1323
- </div>
1324
- </>
1325
- )}
1326
-
1327
- {editingInterface.proxyType === 'response' && (
1328
- <div className="result-body">
1329
- <div className="label">响应内容:</div>
1330
- <pre>{formatResponseContent(testResult.responseBody, testResult.contentType)}</pre>
1331
- </div>
1332
- )}
1333
- </div>
1334
- ) : (
1335
- <div className="result-error">
1336
- <ExclamationCircleOutlined /> {testResult.error}
1337
- </div>
1338
- )}
1339
- </div>
1340
- )}
1341
- </div>
1342
- )}
1343
- </Modal>
956
+ editingInterface={editingInterface}
957
+ />
1344
958
 
1345
- {/* 预览内容弹窗 */}
1346
- <Modal
1347
- title="预览响应内容"
1348
- open={previewVisible}
959
+ {/* 使用拆分后的预览模态框组件 */}
960
+ <PreviewModal
961
+ visible={previewVisible}
1349
962
  onCancel={() => setPreviewVisible(false)}
1350
- footer={[
1351
- <Button key="close" onClick={() => setPreviewVisible(false)}>
1352
- 关闭
1353
- </Button>
1354
- ]}
1355
- width={600}
1356
- >
1357
- <div style={{
1358
- maxHeight: '60vh',
1359
- overflow: 'auto',
1360
- backgroundColor: '#f5f5f5',
1361
- padding: '16px',
1362
- borderRadius: '4px',
1363
- fontFamily: 'monospace'
1364
- }}>
1365
- <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
1366
- {previewContent}
1367
- </pre>
1368
- </div>
1369
- </Modal>
963
+ previewContent={previewContent}
964
+ />
1370
965
  </div>
1371
966
  </AppLayout>
1372
967
  );