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
- // 其余的方法和渲染逻辑保持与V1版本相同
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={setSelectedFeatureId}
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
- <div style={{ textAlign: 'center', padding: '60px 20px' }}>
325
- <h3>接口管理页面 - 重构后版本</h3>
326
- <p>这是重构后的接口管理界面,当前显示基础布局。</p>
327
- <p>完整功能正在开发中,将提供更好的用户体验和功能优化。</p>
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
  );