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