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