whistle.mock-plugins 1.1.3-beta.2 → 1.1.3-dev.4
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.
|
@@ -249,9 +249,719 @@ const InterfaceManagement = () => {
|
|
|
249
249
|
return featureFiltered;
|
|
250
250
|
};
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
const handleAddInterface = () => {
|
|
253
|
+
if (!selectedFeatureId) {
|
|
254
|
+
message.warning('请先选择一个功能模块');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
form.resetFields();
|
|
258
|
+
|
|
259
|
+
// 创建默认响应
|
|
260
|
+
const defaultResponseId = generateResponseId();
|
|
261
|
+
const defaultResponses = [{
|
|
262
|
+
id: defaultResponseId,
|
|
263
|
+
name: '默认响应',
|
|
264
|
+
description: '',
|
|
265
|
+
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
266
|
+
}];
|
|
267
|
+
|
|
268
|
+
console.log('创建新接口,初始化响应数据:', defaultResponses);
|
|
269
|
+
|
|
270
|
+
form.setFieldsValue({
|
|
271
|
+
proxyType: 'response',
|
|
272
|
+
statusCode: '200',
|
|
273
|
+
contentType: 'application/json; charset=utf-8',
|
|
274
|
+
responses: defaultResponses,
|
|
275
|
+
activeResponseId: defaultResponseId,
|
|
276
|
+
httpMethod: 'ALL',
|
|
277
|
+
headerItems: [], // 初始化为空数组
|
|
278
|
+
paramMatchers: [], // 初始化为空数组
|
|
279
|
+
group: undefined // 确保分组字段为undefined,而不是空字符串
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
setCurrentResponseId(defaultResponseId);
|
|
283
|
+
setEditingInterface(null);
|
|
284
|
+
setModalVisible(true);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const handleEditInterface = (record) => {
|
|
288
|
+
if (!record) {
|
|
289
|
+
message.warning('接口数据不完整,无法编辑');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log('编辑接口:', record);
|
|
294
|
+
|
|
295
|
+
// 重置表单
|
|
296
|
+
form.resetFields();
|
|
297
|
+
|
|
298
|
+
// 处理响应数据
|
|
299
|
+
let responses = [];
|
|
300
|
+
let activeResponseId = '';
|
|
301
|
+
|
|
302
|
+
if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
|
|
303
|
+
responses = record.responses;
|
|
304
|
+
activeResponseId = record.activeResponseId || responses[0].id;
|
|
305
|
+
} else if (record.responseContent) {
|
|
306
|
+
// 兼容旧数据格式,创建默认响应
|
|
307
|
+
const defaultResponseId = generateResponseId();
|
|
308
|
+
responses = [{
|
|
309
|
+
id: defaultResponseId,
|
|
310
|
+
name: '默认响应',
|
|
311
|
+
description: '',
|
|
312
|
+
content: record.responseContent
|
|
313
|
+
}];
|
|
314
|
+
activeResponseId = defaultResponseId;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 处理自定义请求头
|
|
318
|
+
let headerItems = [];
|
|
319
|
+
if (record.headers && typeof record.headers === 'object') {
|
|
320
|
+
headerItems = Object.entries(record.headers).map(([headerName, headerValue]) => ({
|
|
321
|
+
headerName,
|
|
322
|
+
headerValue
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 处理分组值,确保它是字符串而不是数组
|
|
327
|
+
let groupValue = record.group;
|
|
328
|
+
if (Array.isArray(groupValue)) {
|
|
329
|
+
groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 设置表单值
|
|
333
|
+
form.setFieldsValue({
|
|
334
|
+
name: record.name,
|
|
335
|
+
group: groupValue || undefined, // 使用undefined而不是空字符串
|
|
336
|
+
pattern: record.urlPattern,
|
|
337
|
+
proxyType: record.proxyType || 'response',
|
|
338
|
+
statusCode: (record.httpStatus || record.statusCode || 200).toString(),
|
|
339
|
+
contentType: record.contentType || 'application/json; charset=utf-8',
|
|
340
|
+
responses,
|
|
341
|
+
activeResponseId,
|
|
342
|
+
httpMethod: record.httpMethod || record.method || 'ALL',
|
|
343
|
+
targetUrl: record.targetUrl || '',
|
|
344
|
+
headerItems,
|
|
345
|
+
paramMatchers: record.paramMatchers || [],
|
|
346
|
+
responseDelay: record.responseDelay ? record.responseDelay.toString() : '0'
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
console.log('编辑接口时设置的分组值:', groupValue);
|
|
350
|
+
|
|
351
|
+
setCurrentResponseId(activeResponseId);
|
|
352
|
+
setEditingInterface(record);
|
|
353
|
+
setModalVisible(true);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const handleDeleteInterface = async (id) => {
|
|
357
|
+
try {
|
|
358
|
+
const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
|
|
359
|
+
if (response.data && response.data.code === 0) {
|
|
360
|
+
message.success('接口删除成功');
|
|
361
|
+
fetchInterfaces();
|
|
362
|
+
|
|
363
|
+
// 刷新规则缓存
|
|
364
|
+
refreshCacheAfterUpdate();
|
|
365
|
+
} else {
|
|
366
|
+
throw new Error(response.data?.message || '接口删除失败');
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('接口删除失败:', error);
|
|
370
|
+
message.error(error.response?.data?.message || error.message || '接口删除失败');
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const handleToggleActive = async (id, currentActive) => {
|
|
375
|
+
try {
|
|
376
|
+
const response = await axios.patch(`/cgi-bin/interfaces/${id}`, {
|
|
377
|
+
active: !currentActive
|
|
378
|
+
});
|
|
379
|
+
if (response.data && response.data.code === 0) {
|
|
380
|
+
message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
|
|
381
|
+
fetchInterfaces();
|
|
382
|
+
|
|
383
|
+
// 刷新规则缓存
|
|
384
|
+
refreshCacheAfterUpdate();
|
|
385
|
+
} else {
|
|
386
|
+
throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`接口${!currentActive ? '启用' : '禁用'}失败:`, error);
|
|
390
|
+
message.error(error.response?.data?.message || error.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const handleSubmit = async () => {
|
|
395
|
+
try {
|
|
396
|
+
// 表单验证
|
|
397
|
+
const values = await form.validateFields();
|
|
398
|
+
console.log('表单提交数据:', values);
|
|
399
|
+
|
|
400
|
+
// 处理自定义请求头
|
|
401
|
+
let headers = {};
|
|
402
|
+
if (values.headerItems && Array.isArray(values.headerItems)) {
|
|
403
|
+
values.headerItems.forEach(item => {
|
|
404
|
+
if (item && item.headerName) {
|
|
405
|
+
headers[item.headerName] = item.headerValue;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 处理分组值,确保它是字符串而不是数组
|
|
411
|
+
let groupValue = values.group;
|
|
412
|
+
if (Array.isArray(groupValue)) {
|
|
413
|
+
groupValue = groupValue.length > 0 ? groupValue[0] : '';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 构建接口数据
|
|
417
|
+
const interfaceData = {
|
|
418
|
+
name: values.name,
|
|
419
|
+
group: groupValue || '', // 确保group不为undefined
|
|
420
|
+
urlPattern: values.pattern,
|
|
421
|
+
proxyType: values.proxyType,
|
|
422
|
+
featureId: selectedFeatureId,
|
|
423
|
+
responses: values.responses,
|
|
424
|
+
activeResponseId: values.activeResponseId,
|
|
425
|
+
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
426
|
+
contentType: values.contentType,
|
|
427
|
+
responseDelay: parseInt(values.responseDelay, 10) || 0,
|
|
428
|
+
httpMethod: values.httpMethod,
|
|
429
|
+
active: true
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
console.log('提交的分组值:', groupValue);
|
|
433
|
+
|
|
434
|
+
// 根据代理类型添加不同字段
|
|
435
|
+
if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
|
|
436
|
+
interfaceData.targetUrl = values.targetUrl;
|
|
437
|
+
interfaceData.headers = headers;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 添加参数匹配规则
|
|
441
|
+
if (values.paramMatchers && Array.isArray(values.paramMatchers) && values.paramMatchers.length > 0) {
|
|
442
|
+
// 过滤掉空的匹配规则
|
|
443
|
+
interfaceData.paramMatchers = values.paramMatchers.filter(matcher =>
|
|
444
|
+
matcher && matcher.paramPath && matcher.paramValue
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.log('提交接口数据:', interfaceData);
|
|
449
|
+
|
|
450
|
+
if (editingInterface) {
|
|
451
|
+
// 更新接口
|
|
452
|
+
const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
|
|
453
|
+
if (response.data && response.data.code === 0) {
|
|
454
|
+
message.success('接口更新成功');
|
|
455
|
+
setModalVisible(false);
|
|
456
|
+
fetchInterfaces();
|
|
457
|
+
|
|
458
|
+
// 刷新规则缓存
|
|
459
|
+
refreshCacheAfterUpdate();
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error(response.data?.message || '接口更新失败');
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
// 创建接口
|
|
465
|
+
const response = await axios.post('/cgi-bin/interfaces', interfaceData);
|
|
466
|
+
if (response.data && response.data.code === 0) {
|
|
467
|
+
message.success('接口创建成功');
|
|
468
|
+
console.log('接口创建成功,返回数据:', response.data);
|
|
469
|
+
console.log('返回的接口数据中的分组信息:', response.data.data?.group);
|
|
470
|
+
setModalVisible(false);
|
|
471
|
+
fetchInterfaces();
|
|
472
|
+
|
|
473
|
+
// 刷新规则缓存
|
|
474
|
+
refreshCacheAfterUpdate();
|
|
475
|
+
} else {
|
|
476
|
+
throw new Error(response.data?.message || '接口创建失败');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error('表单提交失败:', error);
|
|
481
|
+
message.error(error.response?.data?.message || error.message || '操作失败');
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const handleCancel = () => {
|
|
486
|
+
setModalVisible(false);
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const handleSelectFeature = (featureId) => {
|
|
490
|
+
setSelectedFeatureId(featureId);
|
|
491
|
+
// 切换功能模块时重置分页到第一页,但保留其他配置
|
|
492
|
+
saveTableConfig({ current: 1 });
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// 预览响应内容
|
|
496
|
+
const handlePreview = () => {
|
|
497
|
+
// 获取当前选中的响应内容
|
|
498
|
+
const responses = form.getFieldValue('responses') || [];
|
|
499
|
+
const activeResponseId = form.getFieldValue('activeResponseId');
|
|
500
|
+
const activeResponse = responses.find(r => r.id === activeResponseId);
|
|
501
|
+
|
|
502
|
+
if (!activeResponse) {
|
|
503
|
+
message.error('未找到有效的响应内容');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 根据内容类型格式化响应内容
|
|
508
|
+
const contentType = form.getFieldValue('contentType') || '';
|
|
509
|
+
const formattedContent = formatResponseContent(activeResponse.content, contentType);
|
|
510
|
+
|
|
511
|
+
// 设置预览内容,包含响应名称
|
|
512
|
+
setPreviewContent({
|
|
513
|
+
title: `预览: ${activeResponse.name || '未命名响应'}`,
|
|
514
|
+
content: formattedContent,
|
|
515
|
+
description: '',
|
|
516
|
+
contentType
|
|
517
|
+
});
|
|
518
|
+
setPreviewVisible(true);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const handleResponseSelect = (responseId) => {
|
|
522
|
+
setCurrentResponseId(responseId);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// 在列表页面直接切换响应数据
|
|
526
|
+
const handleResponseSwitch = async (interfaceId, responseId) => {
|
|
527
|
+
try {
|
|
528
|
+
const response = await axios.patch(`/cgi-bin/interfaces?id=${interfaceId}`, {
|
|
529
|
+
activeResponseId: responseId
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (response.data.code === 0) {
|
|
533
|
+
// 更新本地状态
|
|
534
|
+
setInterfaces(interfaces.map(item =>
|
|
535
|
+
item.id === interfaceId
|
|
536
|
+
? { ...item, activeResponseId: responseId }
|
|
537
|
+
: item
|
|
538
|
+
));
|
|
539
|
+
|
|
540
|
+
const activeResponse = interfaces
|
|
541
|
+
.find(item => item.id === interfaceId)
|
|
542
|
+
?.responses
|
|
543
|
+
?.find(resp => resp.id === responseId);
|
|
544
|
+
|
|
545
|
+
message.success(`已切换到响应: ${activeResponse?.name || '未命名'}`);
|
|
546
|
+
|
|
547
|
+
// 刷新缓存以立即生效
|
|
548
|
+
await refreshCacheAfterUpdate();
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
console.error('切换响应失败:', error);
|
|
552
|
+
message.error('切换响应失败: ' + (error.response?.data?.message || error.message));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const filteredInterfaces = getFilteredInterfaces();
|
|
557
|
+
|
|
558
|
+
// 处理表格变化(排序、分页)
|
|
559
|
+
const handleTableChange = (pagination, filters, sorter) => {
|
|
560
|
+
console.log('表格变化:', { pagination, filters, sorter });
|
|
561
|
+
|
|
562
|
+
// 保存排序配置
|
|
563
|
+
const sortConfig = {
|
|
564
|
+
sortField: sorter.field || null,
|
|
565
|
+
sortOrder: sorter.order || null,
|
|
566
|
+
current: pagination.current,
|
|
567
|
+
pageSize: pagination.pageSize
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
saveTableConfig(sortConfig);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// 处理列配置变更
|
|
574
|
+
const handleColumnConfigChange = (checkedValues) => {
|
|
575
|
+
// 确保必须显示的列始终被选中
|
|
576
|
+
const requiredColumns = COLUMN_CONFIG.filter(col => col.required).map(col => col.key);
|
|
577
|
+
const finalColumns = [...new Set([...requiredColumns, ...checkedValues])];
|
|
578
|
+
|
|
579
|
+
saveTableConfig({ visibleColumns: finalColumns });
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// 全选列配置
|
|
583
|
+
const handleSelectAllColumns = () => {
|
|
584
|
+
const allColumns = COLUMN_CONFIG.map(col => col.key);
|
|
585
|
+
saveTableConfig({ visibleColumns: allColumns });
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// 重置列配置
|
|
589
|
+
const handleResetColumns = () => {
|
|
590
|
+
const defaultColumns = COLUMN_CONFIG.map(col => col.key);
|
|
591
|
+
saveTableConfig({ visibleColumns: defaultColumns });
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// 切换列配置面板显示
|
|
595
|
+
const handleColumnConfigToggle = (visible) => {
|
|
596
|
+
setColumnConfigVisible(visible);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// 搜索处理
|
|
600
|
+
const handleSearch = (value) => {
|
|
601
|
+
setSearchValue(value);
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// 清除搜索
|
|
605
|
+
const handleClearSearch = () => {
|
|
606
|
+
setSearchValue('');
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// 表格行选择处理
|
|
610
|
+
const onSelectChange = (selectedKeys) => {
|
|
611
|
+
setSelectedRowKeys(selectedKeys);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// 切换视图模式
|
|
615
|
+
const toggleViewMode = () => {
|
|
616
|
+
setViewMode(viewMode === 'table' ? 'card' : 'table');
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// 刷新数据
|
|
620
|
+
const refreshData = () => {
|
|
621
|
+
fetchInterfaces();
|
|
622
|
+
message.success('数据已刷新');
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// 批量操作接口状态
|
|
626
|
+
const handleBatchOperation = async (active) => {
|
|
627
|
+
if (!selectedRowKeys || selectedRowKeys.length === 0) {
|
|
628
|
+
message.warning('请先选择要操作的接口');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
setLoading(true);
|
|
634
|
+
const operations = selectedRowKeys.map(id =>
|
|
635
|
+
axios.patch(`/cgi-bin/interfaces/${id}`, { active })
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
await Promise.all(operations);
|
|
639
|
+
message.success(`已${active ? '启用' : '禁用'} ${selectedRowKeys.length} 个接口`);
|
|
640
|
+
setSelectedRowKeys([]);
|
|
641
|
+
fetchInterfaces();
|
|
642
|
+
|
|
643
|
+
// 刷新规则缓存
|
|
644
|
+
refreshCacheAfterUpdate();
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.error('批量操作失败:', error);
|
|
647
|
+
message.error('批量操作失败: ' + (error.response?.data?.message || error.message));
|
|
648
|
+
} finally {
|
|
649
|
+
setLoading(false);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// 定义所有可用的列
|
|
654
|
+
const allColumns = [
|
|
655
|
+
{
|
|
656
|
+
title: '状态',
|
|
657
|
+
dataIndex: 'active',
|
|
658
|
+
key: 'active',
|
|
659
|
+
width: 80,
|
|
660
|
+
render: (active, record) => (
|
|
661
|
+
<Switch
|
|
662
|
+
checked={active !== false}
|
|
663
|
+
onChange={(checked) => handleToggleActive(record.id, active !== false)}
|
|
664
|
+
size="small"
|
|
665
|
+
/>
|
|
666
|
+
),
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
title: '名称',
|
|
670
|
+
dataIndex: 'name',
|
|
671
|
+
key: 'name',
|
|
672
|
+
sorter: (a, b) => a.name.localeCompare(b.name),
|
|
673
|
+
render: (text) => <span className="interface-name">{text}</span>,
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
title: '分组',
|
|
677
|
+
dataIndex: 'group',
|
|
678
|
+
key: 'group',
|
|
679
|
+
width: 120,
|
|
680
|
+
sorter: (a, b) => {
|
|
681
|
+
const groupA = a.group || '';
|
|
682
|
+
const groupB = b.group || '';
|
|
683
|
+
return groupA.localeCompare(groupB);
|
|
684
|
+
},
|
|
685
|
+
render: (group) => {
|
|
686
|
+
if (!group) {
|
|
687
|
+
return <span style={{ color: '#999', fontStyle: 'italic' }}>未分组</span>;
|
|
688
|
+
}
|
|
689
|
+
return (
|
|
690
|
+
<Tag color="blue" style={{ cursor: 'pointer' }} onClick={() => setSelectedGroup(group)}>
|
|
691
|
+
{group}
|
|
692
|
+
</Tag>
|
|
693
|
+
);
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
title: 'URL匹配规则',
|
|
698
|
+
dataIndex: 'urlPattern',
|
|
699
|
+
key: 'urlPattern',
|
|
700
|
+
ellipsis: true,
|
|
701
|
+
render: (text) => <span className="url-pattern">{text}</span>,
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
title: '处理方式',
|
|
705
|
+
dataIndex: 'proxyType',
|
|
706
|
+
key: 'proxyType',
|
|
707
|
+
width: 120,
|
|
708
|
+
render: (text) => {
|
|
709
|
+
const proxyType = proxyTypes.find(item => item.value === text);
|
|
710
|
+
return (
|
|
711
|
+
<Badge
|
|
712
|
+
color={proxyType?.color || '#999'}
|
|
713
|
+
text={proxyType?.label || text}
|
|
714
|
+
/>
|
|
715
|
+
);
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
title: '当前响应',
|
|
720
|
+
dataIndex: 'activeResponseId',
|
|
721
|
+
key: 'currentResponse',
|
|
722
|
+
width: 180,
|
|
723
|
+
render: (activeResponseId, record) => {
|
|
724
|
+
// 如果没有响应数据,返回空
|
|
725
|
+
if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
|
|
726
|
+
return <span className="no-response">无响应数据</span>;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 找到当前激活的响应
|
|
730
|
+
const activeResponse = record.responses.find(resp => resp.id === activeResponseId);
|
|
731
|
+
|
|
732
|
+
// 如果没有找到激活的响应,使用第一个
|
|
733
|
+
const currentResponse = activeResponse || record.responses[0];
|
|
734
|
+
|
|
735
|
+
return (
|
|
736
|
+
<Select
|
|
737
|
+
value={activeResponseId || record.responses[0]?.id}
|
|
738
|
+
style={{ width: '100%' }}
|
|
739
|
+
onChange={(value) => handleResponseSwitch(record.id, value)}
|
|
740
|
+
disabled={record.proxyType !== 'response'}
|
|
741
|
+
>
|
|
742
|
+
{record.responses.map(resp => (
|
|
743
|
+
<Option key={resp.id} value={resp.id}>
|
|
744
|
+
{resp.name}
|
|
745
|
+
</Option>
|
|
746
|
+
))}
|
|
747
|
+
</Select>
|
|
748
|
+
);
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
title: '延迟(毫秒)',
|
|
753
|
+
dataIndex: 'responseDelay',
|
|
754
|
+
key: 'responseDelay',
|
|
755
|
+
width: 100,
|
|
756
|
+
sorter: (a, b) => (a.responseDelay || 0) - (b.responseDelay || 0),
|
|
757
|
+
render: (delay) => {
|
|
758
|
+
const delayValue = parseInt(delay, 10) || 0;
|
|
759
|
+
return (
|
|
760
|
+
<span className={delayValue > 0 ? 'delay-active' : 'delay-inactive'}>
|
|
761
|
+
{delayValue > 0 ? delayValue : '无延迟'}
|
|
762
|
+
</span>
|
|
763
|
+
);
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
title: '状态码',
|
|
768
|
+
dataIndex: 'httpStatus',
|
|
769
|
+
key: 'httpStatus',
|
|
770
|
+
width: 100,
|
|
771
|
+
render: (status) => {
|
|
772
|
+
const statusCode = status || 200;
|
|
773
|
+
let statusClass = 'status-success';
|
|
774
|
+
if (statusCode >= 400) {
|
|
775
|
+
statusClass = 'status-error';
|
|
776
|
+
} else if (statusCode >= 300) {
|
|
777
|
+
statusClass = 'status-warning';
|
|
778
|
+
}
|
|
779
|
+
return <span className={statusClass}>{statusCode}</span>;
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
title: '内容类型',
|
|
784
|
+
dataIndex: 'contentType',
|
|
785
|
+
key: 'contentType',
|
|
786
|
+
width: 120,
|
|
787
|
+
sorter: (a, b) => {
|
|
788
|
+
const aType = a.contentType || '';
|
|
789
|
+
const bType = b.contentType || '';
|
|
790
|
+
return aType.localeCompare(bType);
|
|
791
|
+
},
|
|
792
|
+
sortDirections: ['ascend', 'descend'],
|
|
793
|
+
render: (text, record) => {
|
|
794
|
+
if (record.proxyType !== 'response') {
|
|
795
|
+
return '-';
|
|
796
|
+
}
|
|
797
|
+
const found = contentTypes.find(item => item.value === text);
|
|
798
|
+
return found ? found.label : text;
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
title: '目标URL',
|
|
803
|
+
dataIndex: 'targetUrl',
|
|
804
|
+
key: 'targetUrl',
|
|
805
|
+
ellipsis: true,
|
|
806
|
+
sorter: (a, b) => {
|
|
807
|
+
const aUrl = a.targetUrl || '';
|
|
808
|
+
const bUrl = b.targetUrl || '';
|
|
809
|
+
return aUrl.localeCompare(bUrl);
|
|
810
|
+
},
|
|
811
|
+
sortDirections: ['ascend', 'descend'],
|
|
812
|
+
render: (text, record) => {
|
|
813
|
+
return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
title: '自定义头',
|
|
818
|
+
dataIndex: 'customHeaders',
|
|
819
|
+
key: 'customHeaders',
|
|
820
|
+
width: 100,
|
|
821
|
+
sorter: (a, b) => {
|
|
822
|
+
const aCount = Object.keys(a.customHeaders || {}).length;
|
|
823
|
+
const bCount = Object.keys(b.customHeaders || {}).length;
|
|
824
|
+
return aCount - bCount;
|
|
825
|
+
},
|
|
826
|
+
sortDirections: ['ascend', 'descend'],
|
|
827
|
+
render: (_, record) => {
|
|
828
|
+
if (record.proxyType !== 'redirect' && record.proxyType !== 'url_redirect') {
|
|
829
|
+
return '-';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const headers = record.customHeaders || {};
|
|
833
|
+
const count = Object.keys(headers).length;
|
|
834
|
+
|
|
835
|
+
if (count === 0) {
|
|
836
|
+
return '-';
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 检查是否包含随机值
|
|
840
|
+
const hasRandomValue = Object.values(headers).some(v => v && v.startsWith('@'));
|
|
841
|
+
|
|
842
|
+
return (
|
|
843
|
+
<Tooltip title={
|
|
844
|
+
<div>
|
|
845
|
+
{Object.entries(headers).map(([key, value]) => (
|
|
846
|
+
<div key={key}>
|
|
847
|
+
{key}: {value}
|
|
848
|
+
{value && value.startsWith('@') && (
|
|
849
|
+
<span style={{ color: '#52c41a' }}> (随机)</span>
|
|
850
|
+
)}
|
|
851
|
+
</div>
|
|
852
|
+
))}
|
|
853
|
+
</div>
|
|
854
|
+
}>
|
|
855
|
+
<span style={{ color: '#1890ff' }}>
|
|
856
|
+
{count}个
|
|
857
|
+
{hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
|
|
858
|
+
</span>
|
|
859
|
+
</Tooltip>
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
title: '参数匹配',
|
|
865
|
+
dataIndex: 'paramMatchers',
|
|
866
|
+
key: 'paramMatchers',
|
|
867
|
+
width: 100,
|
|
868
|
+
render: (_, record) => {
|
|
869
|
+
if (record.proxyType !== 'response') {
|
|
870
|
+
return '-';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const matchers = record.paramMatchers || [];
|
|
874
|
+
const count = matchers.length;
|
|
875
|
+
|
|
876
|
+
if (count === 0) {
|
|
877
|
+
return '-';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return (
|
|
881
|
+
<Tooltip title={
|
|
882
|
+
<div>
|
|
883
|
+
{matchers.map((matcher, index) => (
|
|
884
|
+
<div key={index}>
|
|
885
|
+
{matcher.paramPath}: {matcher.paramValue}
|
|
886
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
887
|
+
({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
|
|
888
|
+
</span>
|
|
889
|
+
</div>
|
|
890
|
+
))}
|
|
891
|
+
</div>
|
|
892
|
+
}>
|
|
893
|
+
<span style={{ color: '#1890ff' }}>
|
|
894
|
+
{count}条规则
|
|
895
|
+
</span>
|
|
896
|
+
</Tooltip>
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
title: '请求方法',
|
|
902
|
+
dataIndex: 'httpMethod',
|
|
903
|
+
key: 'httpMethod',
|
|
904
|
+
width: 120,
|
|
905
|
+
sorter: (a, b) => {
|
|
906
|
+
const aMethod = a.httpMethod || '';
|
|
907
|
+
const bMethod = b.httpMethod || '';
|
|
908
|
+
return aMethod.localeCompare(bMethod);
|
|
909
|
+
},
|
|
910
|
+
sortDirections: ['ascend', 'descend'],
|
|
911
|
+
render: (text) => {
|
|
912
|
+
const found = httpMethods.find(item => item.value === text);
|
|
913
|
+
return found ? found.label : text;
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
title: '操作',
|
|
918
|
+
key: 'action',
|
|
919
|
+
width: 180,
|
|
920
|
+
render: (_, record) => (
|
|
921
|
+
<div className="action-buttons">
|
|
922
|
+
<Button
|
|
923
|
+
type="text"
|
|
924
|
+
icon={<EditOutlined />}
|
|
925
|
+
onClick={() => handleEditInterface(record)}
|
|
926
|
+
/>
|
|
927
|
+
<Popconfirm
|
|
928
|
+
title="确定要删除此接口吗?"
|
|
929
|
+
onConfirm={() => handleDeleteInterface(record.id)}
|
|
930
|
+
okText="确定"
|
|
931
|
+
cancelText="取消"
|
|
932
|
+
>
|
|
933
|
+
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
934
|
+
</Popconfirm>
|
|
935
|
+
</div>
|
|
936
|
+
),
|
|
937
|
+
},
|
|
938
|
+
];
|
|
939
|
+
|
|
940
|
+
// 根据配置过滤可见列
|
|
941
|
+
const visibleColumns = tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key);
|
|
942
|
+
const columns = allColumns.filter(col => visibleColumns.includes(col.key));
|
|
943
|
+
|
|
944
|
+
// 分组筛选和操作区域
|
|
945
|
+
const handleBatchToggleActive = async (active) => {
|
|
946
|
+
try {
|
|
947
|
+
setGroupActionLoading(true);
|
|
948
|
+
const response = await axios.patch(`/cgi-bin/interfaces?active=${active}`, {
|
|
949
|
+
featureId: selectedFeatureId,
|
|
950
|
+
group: selectedGroup
|
|
951
|
+
});
|
|
952
|
+
if (response.data && response.data.code === 0) {
|
|
953
|
+
message.success(`分组 "${selectedGroup}" 中的接口已成功${active ? '启用' : '禁用'}`);
|
|
954
|
+
fetchInterfaces();
|
|
955
|
+
} else {
|
|
956
|
+
throw new Error(response.data?.message || '批量操作失败');
|
|
957
|
+
}
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.error('批量操作失败:', error);
|
|
960
|
+
message.error(error.response?.data?.message || error.message || '批量操作失败');
|
|
961
|
+
} finally {
|
|
962
|
+
setGroupActionLoading(false);
|
|
963
|
+
}
|
|
964
|
+
};
|
|
255
965
|
|
|
256
966
|
return (
|
|
257
967
|
<AppLayout>
|
|
@@ -278,7 +988,7 @@ const InterfaceManagement = () => {
|
|
|
278
988
|
<span>功能模块:</span>
|
|
279
989
|
<Select
|
|
280
990
|
value={selectedFeatureId}
|
|
281
|
-
onChange={
|
|
991
|
+
onChange={handleSelectFeature}
|
|
282
992
|
style={{ width: 240 }}
|
|
283
993
|
placeholder="选择功能模块"
|
|
284
994
|
loading={featuresLoading}
|
|
@@ -294,13 +1004,37 @@ const InterfaceManagement = () => {
|
|
|
294
1004
|
))}
|
|
295
1005
|
</Select>
|
|
296
1006
|
</div>
|
|
1007
|
+
|
|
1008
|
+
<Search
|
|
1009
|
+
placeholder="搜索接口名称或URL"
|
|
1010
|
+
onSearch={handleSearch}
|
|
1011
|
+
value={searchValue}
|
|
1012
|
+
onChange={(e) => setSearchValue(e.target.value)}
|
|
1013
|
+
style={{ width: 250, marginLeft: 16 }}
|
|
1014
|
+
enterButton={<SearchOutlined />}
|
|
1015
|
+
allowClear
|
|
1016
|
+
/>
|
|
297
1017
|
</div>
|
|
298
1018
|
|
|
299
1019
|
<div className="interface-actions">
|
|
300
1020
|
<Space>
|
|
1021
|
+
<Tooltip title="刷新数据">
|
|
1022
|
+
<Button
|
|
1023
|
+
icon={<ReloadOutlined />}
|
|
1024
|
+
onClick={refreshData}
|
|
1025
|
+
loading={interfacesLoading}
|
|
1026
|
+
/>
|
|
1027
|
+
</Tooltip>
|
|
1028
|
+
<Tooltip title={viewMode === 'table' ? '切换到卡片视图' : '切换到表格视图'}>
|
|
1029
|
+
<Button
|
|
1030
|
+
icon={viewMode === 'table' ? <AppstoreOutlined /> : <UnorderedListOutlined />}
|
|
1031
|
+
onClick={toggleViewMode}
|
|
1032
|
+
/>
|
|
1033
|
+
</Tooltip>
|
|
301
1034
|
<Button
|
|
302
1035
|
type="primary"
|
|
303
1036
|
icon={<PlusOutlined />}
|
|
1037
|
+
onClick={handleAddInterface}
|
|
304
1038
|
disabled={!selectedFeatureId || selectedFeature?.active === false}
|
|
305
1039
|
className="add-interface-button"
|
|
306
1040
|
>
|
|
@@ -310,7 +1044,7 @@ const InterfaceManagement = () => {
|
|
|
310
1044
|
</div>
|
|
311
1045
|
</div>
|
|
312
1046
|
</Card>
|
|
313
|
-
|
|
1047
|
+
|
|
314
1048
|
{!features.length && (
|
|
315
1049
|
<Alert
|
|
316
1050
|
message="未找到功能模块"
|
|
@@ -321,11 +1055,842 @@ const InterfaceManagement = () => {
|
|
|
321
1055
|
/>
|
|
322
1056
|
)}
|
|
323
1057
|
|
|
324
|
-
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
1058
|
+
{selectedFeature?.active === false && (
|
|
1059
|
+
<Alert
|
|
1060
|
+
message="功能模块已禁用"
|
|
1061
|
+
description="当前功能模块已被禁用,所有关联接口不会生效。您可以在模拟数据页面启用此功能模块。"
|
|
1062
|
+
type="warning"
|
|
1063
|
+
showIcon
|
|
1064
|
+
style={{ marginBottom: 16 }}
|
|
1065
|
+
/>
|
|
1066
|
+
)}
|
|
1067
|
+
|
|
1068
|
+
{/* 分组筛选和操作区域 */}
|
|
1069
|
+
<Card className="filter-card" bordered={false}>
|
|
1070
|
+
<div className="filter-header">
|
|
1071
|
+
<div className="filter-title">
|
|
1072
|
+
<FilterOutlined /> 分组筛选
|
|
1073
|
+
</div>
|
|
1074
|
+
{selectedGroup && (
|
|
1075
|
+
<Button
|
|
1076
|
+
type="link"
|
|
1077
|
+
onClick={() => setSelectedGroup(null)}
|
|
1078
|
+
size="small"
|
|
1079
|
+
>
|
|
1080
|
+
清除筛选
|
|
1081
|
+
</Button>
|
|
1082
|
+
)}
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
<div className="group-tags-container">
|
|
1086
|
+
{groups.length === 0 ? (
|
|
1087
|
+
<div className="empty-groups">暂无分组</div>
|
|
1088
|
+
) : (
|
|
1089
|
+
<div className="group-tags">
|
|
1090
|
+
{groups.map(group => (
|
|
1091
|
+
<Tag
|
|
1092
|
+
key={group}
|
|
1093
|
+
color={selectedGroup === group ? "blue" : "default"}
|
|
1094
|
+
onClick={() => setSelectedGroup(group === selectedGroup ? null : group)}
|
|
1095
|
+
className={`group-tag ${selectedGroup === group ? 'active' : ''}`}
|
|
1096
|
+
>
|
|
1097
|
+
{group} ({interfaces.filter(item => item.group === group).length})
|
|
1098
|
+
</Tag>
|
|
1099
|
+
))}
|
|
1100
|
+
</div>
|
|
1101
|
+
)}
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
{selectedGroup && (
|
|
1105
|
+
<div className="group-actions">
|
|
1106
|
+
<Popconfirm
|
|
1107
|
+
title={`确定要启用分组 "${selectedGroup}" 中的所有接口吗?`}
|
|
1108
|
+
onConfirm={() => handleBatchToggleActive(true)}
|
|
1109
|
+
okText="确定"
|
|
1110
|
+
cancelText="取消"
|
|
1111
|
+
>
|
|
1112
|
+
<Button
|
|
1113
|
+
type="primary"
|
|
1114
|
+
size="small"
|
|
1115
|
+
loading={groupActionLoading}
|
|
1116
|
+
>
|
|
1117
|
+
批量启用
|
|
1118
|
+
</Button>
|
|
1119
|
+
</Popconfirm>
|
|
1120
|
+
|
|
1121
|
+
<Popconfirm
|
|
1122
|
+
title={`确定要禁用分组 "${selectedGroup}" 中的所有接口吗?`}
|
|
1123
|
+
onConfirm={() => handleBatchToggleActive(false)}
|
|
1124
|
+
okText="确定"
|
|
1125
|
+
cancelText="取消"
|
|
1126
|
+
>
|
|
1127
|
+
<Button
|
|
1128
|
+
danger
|
|
1129
|
+
size="small"
|
|
1130
|
+
loading={groupActionLoading}
|
|
1131
|
+
>
|
|
1132
|
+
批量禁用
|
|
1133
|
+
</Button>
|
|
1134
|
+
</Popconfirm>
|
|
1135
|
+
</div>
|
|
1136
|
+
)}
|
|
1137
|
+
</Card>
|
|
1138
|
+
|
|
1139
|
+
{/* 接口列表状态栏 */}
|
|
1140
|
+
<Card className="list-header-card" bordered={false}>
|
|
1141
|
+
<div className="list-header">
|
|
1142
|
+
<div className="list-header-info">
|
|
1143
|
+
<div className="feature-info">
|
|
1144
|
+
<span className="label">功能模块:</span>
|
|
1145
|
+
<span className="value">{selectedFeature?.name}</span>
|
|
1146
|
+
{selectedGroup && (
|
|
1147
|
+
<Tag color="blue" className="group-badge">{selectedGroup}</Tag>
|
|
1148
|
+
)}
|
|
1149
|
+
</div>
|
|
1150
|
+
<div className="interface-stats">
|
|
1151
|
+
{searchValue ? (
|
|
1152
|
+
<Badge
|
|
1153
|
+
count={`搜索"${searchValue}" - ${filteredInterfaces.length}个结果`}
|
|
1154
|
+
style={{ backgroundColor: '#108ee9' }}
|
|
1155
|
+
/>
|
|
1156
|
+
) : (
|
|
1157
|
+
<Badge
|
|
1158
|
+
count={`共 ${filteredInterfaces.length} 个接口`}
|
|
1159
|
+
style={{ backgroundColor: '#52c41a' }}
|
|
1160
|
+
/>
|
|
1161
|
+
)}
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
<div className="batch-actions">
|
|
1165
|
+
{selectedRowKeys.length > 0 && (
|
|
1166
|
+
<Space>
|
|
1167
|
+
<span className="selected-count">
|
|
1168
|
+
已选择 {selectedRowKeys.length} 项
|
|
1169
|
+
</span>
|
|
1170
|
+
<Popconfirm
|
|
1171
|
+
title="确定要批量启用选中的接口吗?"
|
|
1172
|
+
onConfirm={() => handleBatchOperation(true)}
|
|
1173
|
+
okText="确定"
|
|
1174
|
+
cancelText="取消"
|
|
1175
|
+
>
|
|
1176
|
+
<Button size="small" type="primary">批量启用</Button>
|
|
1177
|
+
</Popconfirm>
|
|
1178
|
+
<Popconfirm
|
|
1179
|
+
title="确定要批量禁用选中的接口吗?"
|
|
1180
|
+
onConfirm={() => handleBatchOperation(false)}
|
|
1181
|
+
okText="确定"
|
|
1182
|
+
cancelText="取消"
|
|
1183
|
+
>
|
|
1184
|
+
<Button size="small" danger>批量禁用</Button>
|
|
1185
|
+
</Popconfirm>
|
|
1186
|
+
</Space>
|
|
1187
|
+
)}
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
</Card>
|
|
1191
|
+
|
|
1192
|
+
{/* 列配置区域 */}
|
|
1193
|
+
<div style={{
|
|
1194
|
+
marginBottom: 16,
|
|
1195
|
+
display: 'flex',
|
|
1196
|
+
justifyContent: 'flex-end',
|
|
1197
|
+
alignItems: 'center'
|
|
1198
|
+
}}>
|
|
1199
|
+
<Popover
|
|
1200
|
+
title="自定义显示列"
|
|
1201
|
+
trigger="click"
|
|
1202
|
+
open={columnConfigVisible}
|
|
1203
|
+
onOpenChange={handleColumnConfigToggle}
|
|
1204
|
+
content={
|
|
1205
|
+
<div style={{ width: 280 }}>
|
|
1206
|
+
<div style={{ marginBottom: 12 }}>
|
|
1207
|
+
<Space>
|
|
1208
|
+
<Button size="small" onClick={handleSelectAllColumns}>
|
|
1209
|
+
全选
|
|
1210
|
+
</Button>
|
|
1211
|
+
<Button size="small" onClick={handleResetColumns}>
|
|
1212
|
+
重置
|
|
1213
|
+
</Button>
|
|
1214
|
+
</Space>
|
|
1215
|
+
</div>
|
|
1216
|
+
<Checkbox.Group
|
|
1217
|
+
value={tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key)}
|
|
1218
|
+
onChange={handleColumnConfigChange}
|
|
1219
|
+
style={{ width: '100%' }}
|
|
1220
|
+
>
|
|
1221
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
1222
|
+
{COLUMN_CONFIG.map(col => (
|
|
1223
|
+
<Checkbox
|
|
1224
|
+
key={col.key}
|
|
1225
|
+
value={col.key}
|
|
1226
|
+
disabled={col.required}
|
|
1227
|
+
style={{
|
|
1228
|
+
width: '100%',
|
|
1229
|
+
color: col.required ? '#999' : undefined
|
|
1230
|
+
}}
|
|
1231
|
+
>
|
|
1232
|
+
{col.title}
|
|
1233
|
+
{col.required && (
|
|
1234
|
+
<span style={{ color: '#999', fontSize: '12px', marginLeft: 4 }}>
|
|
1235
|
+
(必须)
|
|
1236
|
+
</span>
|
|
1237
|
+
)}
|
|
1238
|
+
</Checkbox>
|
|
1239
|
+
))}
|
|
1240
|
+
</div>
|
|
1241
|
+
</Checkbox.Group>
|
|
1242
|
+
</div>
|
|
1243
|
+
}
|
|
1244
|
+
>
|
|
1245
|
+
<Button
|
|
1246
|
+
icon={<SettingOutlined />}
|
|
1247
|
+
size="small"
|
|
1248
|
+
type="text"
|
|
1249
|
+
>
|
|
1250
|
+
列设置
|
|
1251
|
+
</Button>
|
|
1252
|
+
</Popover>
|
|
328
1253
|
</div>
|
|
1254
|
+
|
|
1255
|
+
<Card className="list-container-card" bordered={false} bodyStyle={{ padding: 0 }}>
|
|
1256
|
+
{/* 表格视图 */}
|
|
1257
|
+
{viewMode === 'table' && (
|
|
1258
|
+
<div className="interface-list-container">
|
|
1259
|
+
<Table
|
|
1260
|
+
columns={columns}
|
|
1261
|
+
dataSource={filteredInterfaces}
|
|
1262
|
+
rowKey="id"
|
|
1263
|
+
loading={interfacesLoading}
|
|
1264
|
+
onChange={handleTableChange}
|
|
1265
|
+
rowSelection={{
|
|
1266
|
+
selectedRowKeys,
|
|
1267
|
+
onChange: onSelectChange,
|
|
1268
|
+
}}
|
|
1269
|
+
size="middle"
|
|
1270
|
+
pagination={{
|
|
1271
|
+
current: tableConfig.current,
|
|
1272
|
+
pageSize: tableConfig.pageSize,
|
|
1273
|
+
showSizeChanger: true,
|
|
1274
|
+
showQuickJumper: true,
|
|
1275
|
+
showTotal: (total, range) => `共 ${total} 个接口,显示第 ${range[0]}-${range[1]} 个`,
|
|
1276
|
+
pageSizeOptions: ['10', '20', '50', '100'],
|
|
1277
|
+
size: 'default'
|
|
1278
|
+
}}
|
|
1279
|
+
locale={{ emptyText: '暂无接口配置' }}
|
|
1280
|
+
sortDirections={['ascend', 'descend']}
|
|
1281
|
+
className="interface-table"
|
|
1282
|
+
/>
|
|
1283
|
+
</div>
|
|
1284
|
+
)}
|
|
1285
|
+
|
|
1286
|
+
{/* 卡片视图 */}
|
|
1287
|
+
{viewMode === 'card' && (
|
|
1288
|
+
<div className="interface-card-view">
|
|
1289
|
+
<Row gutter={[16, 16]} style={{ padding: 16 }}>
|
|
1290
|
+
{interfacesLoading ? (
|
|
1291
|
+
[1,2,3,4,5,6].map(i => (
|
|
1292
|
+
<Col xs={24} sm={12} md={8} lg={8} xl={6} key={`loading-${i}`}>
|
|
1293
|
+
<Card loading style={{ height: 180 }} />
|
|
1294
|
+
</Col>
|
|
1295
|
+
))
|
|
1296
|
+
) : filteredInterfaces.length === 0 ? (
|
|
1297
|
+
<Col span={24}>
|
|
1298
|
+
<div className="empty-data">暂无接口配置</div>
|
|
1299
|
+
</Col>
|
|
1300
|
+
) : (
|
|
1301
|
+
filteredInterfaces.map(item => (
|
|
1302
|
+
<Col xs={24} sm={12} md={8} lg={8} xl={6} key={item.id}>
|
|
1303
|
+
<Card
|
|
1304
|
+
className={`interface-card ${item.active === false ? 'inactive' : ''}`}
|
|
1305
|
+
hoverable
|
|
1306
|
+
>
|
|
1307
|
+
<div className="interface-card-header">
|
|
1308
|
+
<Switch
|
|
1309
|
+
checked={item.active !== false}
|
|
1310
|
+
onChange={(checked) => handleToggleActive(item.id, item.active !== false)}
|
|
1311
|
+
size="small"
|
|
1312
|
+
className="interface-card-switch"
|
|
1313
|
+
/>
|
|
1314
|
+
<div className="interface-card-title">{item.name}</div>
|
|
1315
|
+
{item.group && (
|
|
1316
|
+
<Tag
|
|
1317
|
+
color="blue"
|
|
1318
|
+
className="interface-card-group"
|
|
1319
|
+
onClick={() => setSelectedGroup(item.group)}
|
|
1320
|
+
>
|
|
1321
|
+
{item.group}
|
|
1322
|
+
</Tag>
|
|
1323
|
+
)}
|
|
1324
|
+
</div>
|
|
1325
|
+
|
|
1326
|
+
<div className="interface-card-content">
|
|
1327
|
+
<div className="url-pattern-container">
|
|
1328
|
+
<Tooltip title={item.urlPattern}>
|
|
1329
|
+
<div className="url-pattern">{item.urlPattern}</div>
|
|
1330
|
+
</Tooltip>
|
|
1331
|
+
</div>
|
|
1332
|
+
|
|
1333
|
+
<div className="interface-card-info">
|
|
1334
|
+
<div className="info-item">
|
|
1335
|
+
<span className="info-label">处理方式:</span>
|
|
1336
|
+
<Badge
|
|
1337
|
+
color={proxyTypes.find(t => t.value === item.proxyType)?.color || '#999'}
|
|
1338
|
+
text={proxyTypes.find(t => t.value === item.proxyType)?.label || item.proxyType}
|
|
1339
|
+
className="info-value"
|
|
1340
|
+
/>
|
|
1341
|
+
</div>
|
|
1342
|
+
<div className="info-item">
|
|
1343
|
+
<span className="info-label">状态码:</span>
|
|
1344
|
+
<span className={`status-badge status-${item.httpStatus >= 400 ? 'error' : item.httpStatus >= 300 ? 'warning' : 'success'}`}>
|
|
1345
|
+
{item.httpStatus || 200}
|
|
1346
|
+
</span>
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
</div>
|
|
1350
|
+
|
|
1351
|
+
<div className="interface-card-actions">
|
|
1352
|
+
<Button
|
|
1353
|
+
type="primary"
|
|
1354
|
+
size="small"
|
|
1355
|
+
icon={<EditOutlined />}
|
|
1356
|
+
onClick={() => handleEditInterface(item)}
|
|
1357
|
+
>
|
|
1358
|
+
编辑
|
|
1359
|
+
</Button>
|
|
1360
|
+
<Popconfirm
|
|
1361
|
+
title="确定要删除此接口吗?"
|
|
1362
|
+
onConfirm={() => handleDeleteInterface(item.id)}
|
|
1363
|
+
okText="确定"
|
|
1364
|
+
cancelText="取消"
|
|
1365
|
+
>
|
|
1366
|
+
<Button
|
|
1367
|
+
danger
|
|
1368
|
+
size="small"
|
|
1369
|
+
icon={<DeleteOutlined />}
|
|
1370
|
+
>
|
|
1371
|
+
删除
|
|
1372
|
+
</Button>
|
|
1373
|
+
</Popconfirm>
|
|
1374
|
+
</div>
|
|
1375
|
+
</Card>
|
|
1376
|
+
</Col>
|
|
1377
|
+
))
|
|
1378
|
+
)}
|
|
1379
|
+
</Row>
|
|
1380
|
+
|
|
1381
|
+
{/* 卡片视图分页 */}
|
|
1382
|
+
{filteredInterfaces.length > 0 && (
|
|
1383
|
+
<div className="card-pagination">
|
|
1384
|
+
<Pagination
|
|
1385
|
+
current={tableConfig.current}
|
|
1386
|
+
pageSize={tableConfig.pageSize}
|
|
1387
|
+
total={filteredInterfaces.length}
|
|
1388
|
+
showSizeChanger
|
|
1389
|
+
showQuickJumper
|
|
1390
|
+
showTotal={(total, range) => `共 ${total} 个接口,显示第 ${range[0]}-${range[1]} 个`}
|
|
1391
|
+
pageSizeOptions={['10', '20', '50', '100']}
|
|
1392
|
+
size="default"
|
|
1393
|
+
onChange={(page, pageSize) => {
|
|
1394
|
+
saveTableConfig({
|
|
1395
|
+
current: page,
|
|
1396
|
+
pageSize: pageSize
|
|
1397
|
+
});
|
|
1398
|
+
}}
|
|
1399
|
+
/>
|
|
1400
|
+
</div>
|
|
1401
|
+
)}
|
|
1402
|
+
</div>
|
|
1403
|
+
)}
|
|
1404
|
+
</Card>
|
|
1405
|
+
|
|
1406
|
+
{/* 接口编辑/创建模态框 */}
|
|
1407
|
+
<Modal
|
|
1408
|
+
title={editingInterface ? '编辑接口' : '添加接口'}
|
|
1409
|
+
open={modalVisible}
|
|
1410
|
+
onOk={handleSubmit}
|
|
1411
|
+
onCancel={handleCancel}
|
|
1412
|
+
width={800}
|
|
1413
|
+
destroyOnClose
|
|
1414
|
+
okText={editingInterface ? '保存' : '创建'}
|
|
1415
|
+
cancelText="取消"
|
|
1416
|
+
bodyStyle={{ maxHeight: '70vh', overflow: 'auto', padding: '24px' }}
|
|
1417
|
+
>
|
|
1418
|
+
<Form
|
|
1419
|
+
form={form}
|
|
1420
|
+
layout="vertical"
|
|
1421
|
+
initialValues={{
|
|
1422
|
+
name: '',
|
|
1423
|
+
group: '',
|
|
1424
|
+
pattern: '',
|
|
1425
|
+
proxyType: 'response',
|
|
1426
|
+
statusCode: '200',
|
|
1427
|
+
contentType: 'application/json; charset=utf-8',
|
|
1428
|
+
responses: [],
|
|
1429
|
+
httpMethod: 'ALL',
|
|
1430
|
+
targetUrl: '',
|
|
1431
|
+
headerItems: [],
|
|
1432
|
+
paramMatchers: [],
|
|
1433
|
+
responseDelay: '0'
|
|
1434
|
+
}}
|
|
1435
|
+
>
|
|
1436
|
+
{/* 基础信息表单项 */}
|
|
1437
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1438
|
+
<div style={{ flex: 1 }}>
|
|
1439
|
+
<Form.Item
|
|
1440
|
+
name="name"
|
|
1441
|
+
label="接口名称"
|
|
1442
|
+
rules={[{ required: true, message: '请输入接口名称' }]}
|
|
1443
|
+
>
|
|
1444
|
+
<Input placeholder="请输入接口名称" />
|
|
1445
|
+
</Form.Item>
|
|
1446
|
+
</div>
|
|
1447
|
+
<div style={{ flex: 1 }}>
|
|
1448
|
+
<Form.Item
|
|
1449
|
+
name="group"
|
|
1450
|
+
label="分组"
|
|
1451
|
+
tooltip="为接口设置分组,便于管理和批量操作"
|
|
1452
|
+
>
|
|
1453
|
+
<Select
|
|
1454
|
+
placeholder="选择或创建分组"
|
|
1455
|
+
showSearch
|
|
1456
|
+
allowClear
|
|
1457
|
+
style={{ width: '100%' }}
|
|
1458
|
+
mode="tags"
|
|
1459
|
+
maxTagCount="responsive"
|
|
1460
|
+
maxTagTextLength={10}
|
|
1461
|
+
onChange={(value) => {
|
|
1462
|
+
// 如果值是数组(tags模式),只取第一个元素
|
|
1463
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
1464
|
+
// 如果第一个元素是空字符串,则设置为undefined
|
|
1465
|
+
const groupValue = value[0] === '' ? undefined : value[0];
|
|
1466
|
+
form.setFieldsValue({ group: groupValue });
|
|
1467
|
+
}
|
|
1468
|
+
}}
|
|
1469
|
+
onInputKeyDown={(e) => {
|
|
1470
|
+
// 防止回车键提交表单
|
|
1471
|
+
if (e.key === 'Enter') {
|
|
1472
|
+
e.stopPropagation();
|
|
1473
|
+
}
|
|
1474
|
+
}}
|
|
1475
|
+
>
|
|
1476
|
+
{Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
|
|
1477
|
+
<Option key={group} value={group}>{group}</Option>
|
|
1478
|
+
))}
|
|
1479
|
+
</Select>
|
|
1480
|
+
</Form.Item>
|
|
1481
|
+
</div>
|
|
1482
|
+
</div>
|
|
1483
|
+
|
|
1484
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1485
|
+
<div style={{ flex: 1 }}>
|
|
1486
|
+
<Form.Item
|
|
1487
|
+
name="pattern"
|
|
1488
|
+
label="URL匹配规则"
|
|
1489
|
+
rules={[
|
|
1490
|
+
{ required: true, message: '请输入URL匹配规则' },
|
|
1491
|
+
({ getFieldValue }) => ({
|
|
1492
|
+
validator(_, value) {
|
|
1493
|
+
const proxyType = getFieldValue('proxyType');
|
|
1494
|
+
if (!value) return Promise.resolve();
|
|
1495
|
+
|
|
1496
|
+
// URL重定向模式下,必须是完整URL
|
|
1497
|
+
if (proxyType === 'url_redirect') {
|
|
1498
|
+
try {
|
|
1499
|
+
new URL(value);
|
|
1500
|
+
return Promise.resolve();
|
|
1501
|
+
} catch (e) {
|
|
1502
|
+
return Promise.reject(new Error('URL重定向模式下,URL匹配规则必须是完整的URL(包括http://或https://)'));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// 重定向模式下,必须以http://或https://开头
|
|
1507
|
+
if (proxyType === 'redirect') {
|
|
1508
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
1509
|
+
return Promise.resolve();
|
|
1510
|
+
}
|
|
1511
|
+
return Promise.reject(new Error('重定向模式下,URL匹配规则必须以http://或https://开头'));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// 响应模式下的验证
|
|
1515
|
+
if (proxyType === 'response') {
|
|
1516
|
+
// 正则表达式验证
|
|
1517
|
+
if (value.startsWith('/') && value.endsWith('/')) {
|
|
1518
|
+
try {
|
|
1519
|
+
new RegExp(value.slice(1, -1));
|
|
1520
|
+
return Promise.resolve();
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
return Promise.reject(new Error('无效的正则表达式格式'));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// 通配符验证
|
|
1527
|
+
if (value.includes('*')) {
|
|
1528
|
+
if (/^[a-zA-Z0-9\-_/.*]+$/.test(value)) {
|
|
1529
|
+
return Promise.resolve();
|
|
1530
|
+
}
|
|
1531
|
+
return Promise.reject(new Error('通配符URL格式不正确'));
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// 普通路径验证
|
|
1535
|
+
if (/^[a-zA-Z0-9\-_/]+$/.test(value)) {
|
|
1536
|
+
return Promise.resolve();
|
|
1537
|
+
}
|
|
1538
|
+
return Promise.reject(new Error('URL路径格式不正确'));
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
return Promise.resolve();
|
|
1542
|
+
},
|
|
1543
|
+
}),
|
|
1544
|
+
]}
|
|
1545
|
+
tooltip={{
|
|
1546
|
+
title: (
|
|
1547
|
+
<>
|
|
1548
|
+
<div>不同处理方式下URL匹配规则要求:</div>
|
|
1549
|
+
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
1550
|
+
<li><b>模拟响应:</b> 支持路径格式如 /api/users,通配符如 /api/*,正则如 /\/api\/\d+/</li>
|
|
1551
|
+
<li><b>重定向:</b> 必须以http://或https://开头,例如:https://example.com/api</li>
|
|
1552
|
+
<li><b>URL重定向:</b> 必须是完整URL,包括http://或https://,例如:https://example.com/api/users</li>
|
|
1553
|
+
</ul>
|
|
1554
|
+
</>
|
|
1555
|
+
),
|
|
1556
|
+
overlayStyle: { maxWidth: '450px' }
|
|
1557
|
+
}}
|
|
1558
|
+
>
|
|
1559
|
+
<Input
|
|
1560
|
+
placeholder="根据选择的处理方式输入相应格式的URL"
|
|
1561
|
+
/>
|
|
1562
|
+
</Form.Item>
|
|
1563
|
+
</div>
|
|
1564
|
+
<div style={{ flex: 1 }}>
|
|
1565
|
+
<Form.Item
|
|
1566
|
+
name="proxyType"
|
|
1567
|
+
label="处理方式"
|
|
1568
|
+
rules={[{ required: true, message: '请选择处理方式' }]}
|
|
1569
|
+
tooltip={{
|
|
1570
|
+
title: (
|
|
1571
|
+
<>
|
|
1572
|
+
<div>不同处理方式的规则说明:</div>
|
|
1573
|
+
<ul style={{margin: '5px 0 0 15px', padding: 0}}>
|
|
1574
|
+
<li><b>模拟响应:</b> 返回您定义的响应内容</li>
|
|
1575
|
+
<li><b>重定向:</b> 将请求重定向到其他URL,URL匹配规则必须以http://或https://开头</li>
|
|
1576
|
+
<li><b>URL重定向:</b> 完全匹配URL时重定向,URL匹配规则必须是完整URL</li>
|
|
1577
|
+
</ul>
|
|
1578
|
+
</>
|
|
1579
|
+
),
|
|
1580
|
+
overlayStyle: { maxWidth: '450px' }
|
|
1581
|
+
}}
|
|
1582
|
+
>
|
|
1583
|
+
<Select onChange={(value) => {
|
|
1584
|
+
// 当切换代理类型时,清空pattern字段,并提供不同的placeholder
|
|
1585
|
+
form.setFieldsValue({ pattern: '' });
|
|
1586
|
+
|
|
1587
|
+
// 为不同的代理类型提供不同的pattern占位符
|
|
1588
|
+
const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
|
|
1589
|
+
if (patternInput) {
|
|
1590
|
+
if (value === 'response') {
|
|
1591
|
+
patternInput.placeholder = "例如:/api/users,/api/users/*,/api\\/users\\/\\d+/";
|
|
1592
|
+
} else if (value === 'redirect') {
|
|
1593
|
+
patternInput.placeholder = "例如:https://example.com/api";
|
|
1594
|
+
} else if (value === 'url_redirect') {
|
|
1595
|
+
patternInput.placeholder = "例如:https://example.com/api/users";
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}}>
|
|
1599
|
+
{proxyTypes.map(item => (
|
|
1600
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
1601
|
+
))}
|
|
1602
|
+
</Select>
|
|
1603
|
+
</Form.Item>
|
|
1604
|
+
</div>
|
|
1605
|
+
</div>
|
|
1606
|
+
|
|
1607
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1608
|
+
<div style={{ flex: 1 }}>
|
|
1609
|
+
<Form.Item
|
|
1610
|
+
name="httpMethod"
|
|
1611
|
+
label="请求方法"
|
|
1612
|
+
rules={[{ required: true, message: '请选择请求方法' }]}
|
|
1613
|
+
>
|
|
1614
|
+
<Select>
|
|
1615
|
+
{httpMethods.map(item => (
|
|
1616
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
1617
|
+
))}
|
|
1618
|
+
</Select>
|
|
1619
|
+
</Form.Item>
|
|
1620
|
+
</div>
|
|
1621
|
+
<div style={{ flex: 1 }}>
|
|
1622
|
+
{/* 这里可以留空或添加其他字段 */}
|
|
1623
|
+
</div>
|
|
1624
|
+
</div>
|
|
1625
|
+
|
|
1626
|
+
{/* 根据proxyType显示不同的表单项 */}
|
|
1627
|
+
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}>
|
|
1628
|
+
{({ getFieldValue, setFieldsValue }) => {
|
|
1629
|
+
const proxyType = getFieldValue('proxyType');
|
|
1630
|
+
|
|
1631
|
+
if (proxyType === 'response') {
|
|
1632
|
+
return (
|
|
1633
|
+
<>
|
|
1634
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1635
|
+
<div style={{ flex: 1 }}>
|
|
1636
|
+
<Form.Item
|
|
1637
|
+
name="statusCode"
|
|
1638
|
+
label="状态码"
|
|
1639
|
+
rules={[{ required: true, message: '请选择状态码' }]}
|
|
1640
|
+
>
|
|
1641
|
+
<Select>
|
|
1642
|
+
{statusCodes.map(item => (
|
|
1643
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
1644
|
+
))}
|
|
1645
|
+
</Select>
|
|
1646
|
+
</Form.Item>
|
|
1647
|
+
</div>
|
|
1648
|
+
<div style={{ flex: 1 }}>
|
|
1649
|
+
<Form.Item
|
|
1650
|
+
name="contentType"
|
|
1651
|
+
label="内容类型"
|
|
1652
|
+
rules={[{ required: true, message: '请选择内容类型' }]}
|
|
1653
|
+
>
|
|
1654
|
+
<Select>
|
|
1655
|
+
{contentTypes.map(item => (
|
|
1656
|
+
<Option key={item.value} value={item.value}>{item.label}</Option>
|
|
1657
|
+
))}
|
|
1658
|
+
</Select>
|
|
1659
|
+
</Form.Item>
|
|
1660
|
+
</div>
|
|
1661
|
+
<div style={{ flex: 1 }}>
|
|
1662
|
+
<Form.Item
|
|
1663
|
+
name="responseDelay"
|
|
1664
|
+
label="延迟响应(毫秒)"
|
|
1665
|
+
tooltip="设置接口响应延迟时间,单位为毫秒"
|
|
1666
|
+
rules={[
|
|
1667
|
+
{
|
|
1668
|
+
pattern: /^\d*$/,
|
|
1669
|
+
message: '请输入有效的数字'
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
validator: (_, value) => {
|
|
1673
|
+
if (!value || parseInt(value, 10) <= 60000) {
|
|
1674
|
+
return Promise.resolve();
|
|
1675
|
+
}
|
|
1676
|
+
return Promise.reject(new Error('延迟时间不能超过60秒(60000毫秒)'));
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
]}
|
|
1680
|
+
>
|
|
1681
|
+
<Input placeholder="例如:500" />
|
|
1682
|
+
</Form.Item>
|
|
1683
|
+
</div>
|
|
1684
|
+
</div>
|
|
1685
|
+
|
|
1686
|
+
{/* 请求入参匹配设置 */}
|
|
1687
|
+
<Form.Item
|
|
1688
|
+
label="请求入参匹配"
|
|
1689
|
+
tooltip="设置请求参数的匹配条件,只有当请求参数满足条件时才返回对应的响应。支持嵌套属性路径,如:a.b.c"
|
|
1690
|
+
>
|
|
1691
|
+
<Form.List name="paramMatchers">
|
|
1692
|
+
{(fields, { add, remove }) => (
|
|
1693
|
+
<>
|
|
1694
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
1695
|
+
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
1696
|
+
<Form.Item
|
|
1697
|
+
{...restField}
|
|
1698
|
+
name={[name, 'paramPath']}
|
|
1699
|
+
rules={[
|
|
1700
|
+
{ required: true, message: '请输入参数路径' },
|
|
1701
|
+
{
|
|
1702
|
+
pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
|
|
1703
|
+
message: '请输入有效的参数路径,如:userId 或 data.user.id'
|
|
1704
|
+
}
|
|
1705
|
+
]}
|
|
1706
|
+
style={{ flex: 1, margin: 0 }}
|
|
1707
|
+
>
|
|
1708
|
+
<Input placeholder="参数路径(如:userId 或 data.user.id)" />
|
|
1709
|
+
</Form.Item>
|
|
1710
|
+
<Form.Item
|
|
1711
|
+
{...restField}
|
|
1712
|
+
name={[name, 'paramValue']}
|
|
1713
|
+
rules={[{ required: true, message: '请输入期望值' }]}
|
|
1714
|
+
style={{ flex: 1, margin: 0 }}
|
|
1715
|
+
>
|
|
1716
|
+
<Input placeholder="期望值(如:123 或 admin)" />
|
|
1717
|
+
</Form.Item>
|
|
1718
|
+
<Form.Item
|
|
1719
|
+
{...restField}
|
|
1720
|
+
name={[name, 'matchType']}
|
|
1721
|
+
style={{ width: 100, margin: 0 }}
|
|
1722
|
+
initialValue="exact"
|
|
1723
|
+
>
|
|
1724
|
+
<Select placeholder="匹配类型">
|
|
1725
|
+
<Option value="exact">精确匹配</Option>
|
|
1726
|
+
<Option value="contains">包含</Option>
|
|
1727
|
+
<Option value="regex">正则</Option>
|
|
1728
|
+
</Select>
|
|
1729
|
+
</Form.Item>
|
|
1730
|
+
<Button
|
|
1731
|
+
type="text"
|
|
1732
|
+
danger
|
|
1733
|
+
icon={<DeleteOutlined />}
|
|
1734
|
+
onClick={() => remove(name)}
|
|
1735
|
+
/>
|
|
1736
|
+
</div>
|
|
1737
|
+
))}
|
|
1738
|
+
<Button
|
|
1739
|
+
type="dashed"
|
|
1740
|
+
onClick={() => add()}
|
|
1741
|
+
icon={<PlusOutlined />}
|
|
1742
|
+
style={{ width: '100%', marginTop: '8px' }}
|
|
1743
|
+
>
|
|
1744
|
+
添加参数匹配规则
|
|
1745
|
+
</Button>
|
|
1746
|
+
{fields.length > 0 && (
|
|
1747
|
+
<div style={{
|
|
1748
|
+
fontSize: '12px',
|
|
1749
|
+
color: '#666',
|
|
1750
|
+
marginTop: '8px',
|
|
1751
|
+
padding: '8px',
|
|
1752
|
+
backgroundColor: '#f5f5f5',
|
|
1753
|
+
borderRadius: '4px'
|
|
1754
|
+
}}>
|
|
1755
|
+
<div><strong>说明:</strong></div>
|
|
1756
|
+
<div>• <strong>精确匹配</strong>:参数值完全相等</div>
|
|
1757
|
+
<div>• <strong>包含</strong>:参数值包含指定内容</div>
|
|
1758
|
+
<div>• <strong>正则</strong>:参数值符合正则表达式</div>
|
|
1759
|
+
<div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
|
|
1760
|
+
</div>
|
|
1761
|
+
)}
|
|
1762
|
+
</>
|
|
1763
|
+
)}
|
|
1764
|
+
</Form.List>
|
|
1765
|
+
</Form.Item>
|
|
1766
|
+
|
|
1767
|
+
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
1768
|
+
<Form.Item
|
|
1769
|
+
name="responses"
|
|
1770
|
+
initialValue={[]}
|
|
1771
|
+
hidden
|
|
1772
|
+
>
|
|
1773
|
+
<Input />
|
|
1774
|
+
</Form.Item>
|
|
1775
|
+
<Form.Item name="activeResponseId" hidden>
|
|
1776
|
+
<Input />
|
|
1777
|
+
</Form.Item>
|
|
1778
|
+
|
|
1779
|
+
{/* 使用新的合并组件 */}
|
|
1780
|
+
<Form.Item noStyle shouldUpdate>
|
|
1781
|
+
{({ getFieldValue }) => {
|
|
1782
|
+
let responses = getFieldValue('responses') || [];
|
|
1783
|
+
const activeResponseId = getFieldValue('activeResponseId');
|
|
1784
|
+
|
|
1785
|
+
// 确保是数组
|
|
1786
|
+
if (!Array.isArray(responses)) {
|
|
1787
|
+
try {
|
|
1788
|
+
if (typeof responses === 'string') {
|
|
1789
|
+
responses = JSON.parse(responses);
|
|
1790
|
+
}
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
responses = [];
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
return (
|
|
1797
|
+
<ResponseContentEditor
|
|
1798
|
+
form={form}
|
|
1799
|
+
responses={responses}
|
|
1800
|
+
activeResponseId={activeResponseId}
|
|
1801
|
+
onPreview={handlePreview}
|
|
1802
|
+
/>
|
|
1803
|
+
);
|
|
1804
|
+
}}
|
|
1805
|
+
</Form.Item>
|
|
1806
|
+
</>
|
|
1807
|
+
);
|
|
1808
|
+
} else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
1809
|
+
return (
|
|
1810
|
+
<>
|
|
1811
|
+
<Form.Item
|
|
1812
|
+
name="targetUrl"
|
|
1813
|
+
label="重定向目标URL"
|
|
1814
|
+
rules={[
|
|
1815
|
+
{ required: true, message: '请输入重定向目标URL' },
|
|
1816
|
+
{
|
|
1817
|
+
validator(_, value) {
|
|
1818
|
+
if (!value) return Promise.resolve();
|
|
1819
|
+
try {
|
|
1820
|
+
new URL(value);
|
|
1821
|
+
return Promise.resolve();
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
return Promise.reject(new Error('请输入有效的URL格式(包括http://或https://)'));
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
]}
|
|
1828
|
+
tooltip="重定向的目标URL,必须是完整的URL格式"
|
|
1829
|
+
>
|
|
1830
|
+
<Input
|
|
1831
|
+
placeholder="例如:https://api.example.com/users"
|
|
1832
|
+
/>
|
|
1833
|
+
</Form.Item>
|
|
1834
|
+
|
|
1835
|
+
{/* 自定义请求头设置 */}
|
|
1836
|
+
<Form.Item label="自定义请求头">
|
|
1837
|
+
<Form.List name="headerItems">
|
|
1838
|
+
{(fields, { add, remove }) => (
|
|
1839
|
+
<>
|
|
1840
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
1841
|
+
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
1842
|
+
<Form.Item
|
|
1843
|
+
{...restField}
|
|
1844
|
+
name={[name, 'headerName']}
|
|
1845
|
+
rules={[{ required: true, message: '请输入请求头名称' }]}
|
|
1846
|
+
style={{ flex: 1, margin: 0 }}
|
|
1847
|
+
>
|
|
1848
|
+
<Input placeholder="请求头名称(如:Authorization)" />
|
|
1849
|
+
</Form.Item>
|
|
1850
|
+
<Form.Item
|
|
1851
|
+
{...restField}
|
|
1852
|
+
name={[name, 'headerValue']}
|
|
1853
|
+
rules={[{ required: true, message: '请输入请求头值' }]}
|
|
1854
|
+
style={{ flex: 1, margin: 0 }}
|
|
1855
|
+
>
|
|
1856
|
+
<Input placeholder="请求头值(如:Bearer token123)" />
|
|
1857
|
+
</Form.Item>
|
|
1858
|
+
<Button
|
|
1859
|
+
type="text"
|
|
1860
|
+
danger
|
|
1861
|
+
icon={<DeleteOutlined />}
|
|
1862
|
+
onClick={() => remove(name)}
|
|
1863
|
+
/>
|
|
1864
|
+
</div>
|
|
1865
|
+
))}
|
|
1866
|
+
<Button
|
|
1867
|
+
type="dashed"
|
|
1868
|
+
onClick={() => add()}
|
|
1869
|
+
icon={<PlusOutlined />}
|
|
1870
|
+
style={{ width: '100%', marginTop: '8px' }}
|
|
1871
|
+
>
|
|
1872
|
+
添加请求头
|
|
1873
|
+
</Button>
|
|
1874
|
+
</>
|
|
1875
|
+
)}
|
|
1876
|
+
</Form.List>
|
|
1877
|
+
</Form.Item>
|
|
1878
|
+
</>
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
return null;
|
|
1883
|
+
}}
|
|
1884
|
+
</Form.Item>
|
|
1885
|
+
</Form>
|
|
1886
|
+
</Modal>
|
|
1887
|
+
|
|
1888
|
+
{/* 使用拆分后的预览模态框组件 */}
|
|
1889
|
+
<PreviewModal
|
|
1890
|
+
visible={previewVisible}
|
|
1891
|
+
onCancel={() => setPreviewVisible(false)}
|
|
1892
|
+
previewContent={previewContent}
|
|
1893
|
+
/>
|
|
329
1894
|
</div>
|
|
330
1895
|
</AppLayout>
|
|
331
1896
|
);
|