whistle.mock-plugins 1.2.1-beta.6 → 1.2.2-beta.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.
@@ -42,13 +42,13 @@ const COLUMN_CONFIG = [
42
42
  { key: 'group', title: '分组', required: false },
43
43
  { key: 'urlPattern', title: 'URL匹配规则', required: false },
44
44
  { key: 'proxyType', title: '处理方式', required: false },
45
- { key: 'currentResponse', title: '当前响应', required: false },
45
+ { key: 'currentResponse', title: '响应配置', required: false },
46
46
  { key: 'responseDelay', title: '延迟(毫秒)', required: false },
47
47
  { key: 'httpStatus', title: '状态码', required: false },
48
48
  { key: 'contentType', title: '内容类型', required: false },
49
49
  { key: 'targetUrl', title: '目标URL', required: false },
50
50
  { key: 'customHeaders', title: '自定义头', required: false },
51
- { key: 'paramMatchers', title: '参数匹配', required: false },
51
+ { key: 'paramMatchers', title: '匹配规则', required: false },
52
52
  { key: 'httpMethod', title: '请求方法', required: false },
53
53
  { key: 'action', title: '操作', required: true }
54
54
  ];
@@ -302,7 +302,17 @@ const InterfaceManagement = () => {
302
302
  let activeResponseId = '';
303
303
 
304
304
  if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
305
- responses = record.responses;
305
+ responses = record.responses.map(r => ({
306
+ ...r,
307
+ paramMatchers: r.paramMatchers || []
308
+ }));
309
+ // 向后兼容:旧数据接口级 paramMatchers 迁移到第一个响应
310
+ if (
311
+ record.paramMatchers && Array.isArray(record.paramMatchers) && record.paramMatchers.length > 0 &&
312
+ responses.every(r => !r.paramMatchers || r.paramMatchers.length === 0)
313
+ ) {
314
+ responses[0] = { ...responses[0], paramMatchers: record.paramMatchers };
315
+ }
306
316
  activeResponseId = record.activeResponseId || responses[0].id;
307
317
  } else if (record.responseContent) {
308
318
  // 兼容旧数据格式,创建默认响应
@@ -502,13 +512,22 @@ const InterfaceManagement = () => {
502
512
  }
503
513
 
504
514
  // 构建接口数据 - 在编辑模式下,确保关键字段不丢失
515
+ // responses 隐藏字段可能被 Input 序列化为字符串,需要解析还原
516
+ let responsesValue = values.responses;
517
+ if (typeof responsesValue === 'string') {
518
+ try { responsesValue = JSON.parse(responsesValue); } catch (e) { responsesValue = []; }
519
+ }
520
+ if (!Array.isArray(responsesValue)) {
521
+ responsesValue = editingInterface ? editingInterface.responses : [];
522
+ }
523
+
505
524
  const interfaceData = {
506
525
  name: values.name || (editingInterface ? editingInterface.name : ''),
507
526
  group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
508
527
  urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
509
528
  proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
510
529
  featureId: selectedFeatureId,
511
- responses: values.responses || (editingInterface ? editingInterface.responses : []),
530
+ responses: responsesValue,
512
531
  activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
513
532
  httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
514
533
  contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
@@ -800,35 +819,82 @@ const InterfaceManagement = () => {
800
819
  },
801
820
  },
802
821
  {
803
- title: '当前响应',
822
+ title: '响应配置',
804
823
  dataIndex: 'activeResponseId',
805
824
  key: 'currentResponse',
806
- width: 180,
825
+ width: 160,
807
826
  render: (activeResponseId, record) => {
808
- // 如果没有响应数据,返回空
827
+ if (record.proxyType !== 'response') {
828
+ return <span style={{ color: '#ccc' }}>-</span>;
829
+ }
809
830
  if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
810
- return <span className="no-response">无响应数据</span>;
831
+ return <span style={{ color: '#ff4d4f', fontSize: '12px' }}>无响应数据</span>;
811
832
  }
812
-
813
- // 找到当前激活的响应
814
- const activeResponse = record.responses.find(resp => resp.id === activeResponseId);
815
-
816
- // 如果没有找到激活的响应,使用第一个
817
- const currentResponse = activeResponse || record.responses[0];
818
-
833
+
834
+ const total = record.responses.length;
835
+ const hasSmartMatch = record.responses.some(
836
+ r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0
837
+ );
838
+
839
+ // 场景一:所有响应都没有规则 → 显示 Select 快捷切换
840
+ if (!hasSmartMatch) {
841
+ return (
842
+ <Select
843
+ value={activeResponseId || record.responses[0]?.id}
844
+ style={{ width: '100%' }}
845
+ size="small"
846
+ onChange={(value) => handleResponseSwitch(record.id, value)}
847
+ >
848
+ {record.responses.map(resp => (
849
+ <Option key={resp.id} value={resp.id}>
850
+ {resp.name}
851
+ </Option>
852
+ ))}
853
+ </Select>
854
+ );
855
+ }
856
+
857
+ // 场景二/三:有响应含规则 → 显示 Tag 摘要
858
+ const tooltipContent = (
859
+ <div style={{ maxWidth: 260 }}>
860
+ {record.responses.map((resp) => {
861
+ const matchers = resp.paramMatchers || [];
862
+ return (
863
+ <div key={resp.id} style={{ marginBottom: 6 }}>
864
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
865
+ {matchers.length > 0 ? (
866
+ <Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>{matchers.length}条规则</Tag>
867
+ ) : (
868
+ <Tag color="default" style={{ fontSize: '11px', margin: 0 }}>兜底</Tag>
869
+ )}
870
+ <span style={{ fontWeight: resp.id === activeResponseId ? 'bold' : 'normal' }}>
871
+ {resp.name}
872
+ </span>
873
+ </div>
874
+ {matchers.length > 0 && (
875
+ <div style={{ paddingLeft: 8, marginTop: 2 }}>
876
+ {matchers.map((m, i) => (
877
+ <div key={i} style={{ fontSize: '11px', color: '#666' }}>
878
+ {m.paramPath} = {m.paramValue}
879
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
880
+ ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
881
+ </span>
882
+ </div>
883
+ ))}
884
+ </div>
885
+ )}
886
+ </div>
887
+ );
888
+ })}
889
+ </div>
890
+ );
891
+
819
892
  return (
820
- <Select
821
- value={activeResponseId || record.responses[0]?.id}
822
- style={{ width: '100%' }}
823
- onChange={(value) => handleResponseSwitch(record.id, value)}
824
- disabled={record.proxyType !== 'response'}
825
- >
826
- {record.responses.map(resp => (
827
- <Option key={resp.id} value={resp.id}>
828
- {resp.name}
829
- </Option>
830
- ))}
831
- </Select>
893
+ <Tooltip title={tooltipContent} placement="left">
894
+ <Tag color="blue" style={{ cursor: 'default' }}>
895
+ 智能匹配 · {total}个响应
896
+ </Tag>
897
+ </Tooltip>
832
898
  );
833
899
  },
834
900
  },
@@ -945,38 +1011,75 @@ const InterfaceManagement = () => {
945
1011
  }
946
1012
  },
947
1013
  {
948
- title: '参数匹配',
949
- dataIndex: 'paramMatchers',
1014
+ title: '匹配规则',
1015
+ dataIndex: 'responses',
950
1016
  key: 'paramMatchers',
951
1017
  width: 100,
952
- render: (_, record) => {
1018
+ render: (responses, record) => {
953
1019
  if (record.proxyType !== 'response') {
954
- return '-';
1020
+ return <span style={{ color: '#ccc' }}>-</span>;
955
1021
  }
956
-
957
- const matchers = record.paramMatchers || [];
958
- const count = matchers.length;
959
-
960
- if (count === 0) {
961
- return '-';
1022
+
1023
+ // 汇总所有响应的 paramMatchers(响应级),兼容旧数据接口级 paramMatchers
1024
+ const responsesWithMatchers = Array.isArray(responses)
1025
+ ? responses.filter(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0)
1026
+ : [];
1027
+ const totalRules = responsesWithMatchers.reduce((sum, r) => sum + r.paramMatchers.length, 0);
1028
+
1029
+ // 兼容旧数据:接口级 paramMatchers
1030
+ const legacyMatchers = record.paramMatchers || [];
1031
+
1032
+ if (totalRules === 0 && legacyMatchers.length === 0) {
1033
+ return <span style={{ color: '#ccc' }}>-</span>;
962
1034
  }
963
-
964
- return (
965
- <Tooltip title={
966
- <div>
967
- {matchers.map((matcher, index) => (
968
- <div key={index}>
969
- {matcher.paramPath}: {matcher.paramValue}
970
- <span style={{ color: '#52c41a', marginLeft: 4 }}>
971
- ({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
972
- </span>
1035
+
1036
+ // 新数据:按响应分组展示
1037
+ if (totalRules > 0) {
1038
+ const tooltipContent = (
1039
+ <div style={{ maxWidth: 280 }}>
1040
+ {responsesWithMatchers.map(resp => (
1041
+ <div key={resp.id} style={{ marginBottom: 6 }}>
1042
+ <div style={{ fontWeight: 'bold', marginBottom: 2 }}>{resp.name}:</div>
1043
+ {resp.paramMatchers.map((m, i) => (
1044
+ <div key={i} style={{ paddingLeft: 8, fontSize: '12px' }}>
1045
+ {m.paramPath} = {m.paramValue}
1046
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
1047
+ ({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
1048
+ </span>
1049
+ </div>
1050
+ ))}
973
1051
  </div>
974
1052
  ))}
1053
+ {Array.isArray(responses) && responses.some(r => !r.paramMatchers || r.paramMatchers.length === 0) && (
1054
+ <div style={{ color: '#999', fontSize: '12px', marginTop: 4 }}>
1055
+ + 兜底响应(无规则)
1056
+ </div>
1057
+ )}
975
1058
  </div>
976
- }>
977
- <span style={{ color: '#1890ff' }}>
978
- {count}条规则
979
- </span>
1059
+ );
1060
+ return (
1061
+ <Tooltip title={tooltipContent} placement="left">
1062
+ <span style={{ color: '#1890ff', cursor: 'default' }}>{totalRules}条规则</span>
1063
+ </Tooltip>
1064
+ );
1065
+ }
1066
+
1067
+ // 旧数据兼容展示
1068
+ const tooltipContent = (
1069
+ <div>
1070
+ {legacyMatchers.map((matcher, index) => (
1071
+ <div key={index}>
1072
+ {matcher.paramPath}: {matcher.paramValue}
1073
+ <span style={{ color: '#52c41a', marginLeft: 4 }}>
1074
+ ({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
1075
+ </span>
1076
+ </div>
1077
+ ))}
1078
+ </div>
1079
+ );
1080
+ return (
1081
+ <Tooltip title={tooltipContent} placement="left">
1082
+ <span style={{ color: '#fa8c16', cursor: 'default' }}>{legacyMatchers.length}条(旧)</span>
980
1083
  </Tooltip>
981
1084
  );
982
1085
  }
@@ -1698,97 +1801,9 @@ const InterfaceManagement = () => {
1698
1801
  </div>
1699
1802
  </div>
1700
1803
 
1701
- {/* 请求入参匹配设置 */}
1702
- <Form.Item
1703
- label="请求入参匹配"
1704
- tooltip="设置请求参数的匹配条件,只有当请求参数满足条件时才返回对应的响应。支持嵌套属性路径,如:a.b.c"
1705
- >
1706
- <Form.List name="paramMatchers">
1707
- {(fields, { add, remove }) => (
1708
- <>
1709
- {fields.map(({ key, name, ...restField }) => (
1710
- <div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
1711
- <Form.Item
1712
- {...restField}
1713
- name={[name, 'paramPath']}
1714
- rules={[
1715
- { required: true, message: '请输入参数路径' },
1716
- {
1717
- pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
1718
- message: '请输入有效的参数路径,如:userId 或 data.user.id'
1719
- }
1720
- ]}
1721
- style={{ flex: 1, margin: 0 }}
1722
- >
1723
- <Input placeholder="参数路径(如:userId 或 data.user.id)" />
1724
- </Form.Item>
1725
- <Form.Item
1726
- {...restField}
1727
- name={[name, 'paramValue']}
1728
- rules={[{ required: true, message: '请输入期望值' }]}
1729
- style={{ flex: 1, margin: 0 }}
1730
- >
1731
- <Input placeholder="期望值(如:123 或 admin)" />
1732
- </Form.Item>
1733
- <Form.Item
1734
- {...restField}
1735
- name={[name, 'matchType']}
1736
- style={{ width: 100, margin: 0 }}
1737
- initialValue="exact"
1738
- >
1739
- <Select placeholder="匹配类型">
1740
- <Option value="exact">精确匹配</Option>
1741
- <Option value="contains">包含</Option>
1742
- <Option value="regex">正则</Option>
1743
- </Select>
1744
- </Form.Item>
1745
- <Button
1746
- type="text"
1747
- danger
1748
- icon={<DeleteOutlined />}
1749
- onClick={() => remove(name)}
1750
- />
1751
- </div>
1752
- ))}
1753
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
1754
- <Button
1755
- type="dashed"
1756
- onClick={() => add()}
1757
- icon={<PlusOutlined />}
1758
- style={{ flex: 1 }}
1759
- >
1760
- 添加参数匹配规则
1761
- </Button>
1762
- <Popover
1763
- content={
1764
- <div style={{ maxWidth: '300px' }}>
1765
- <div style={{ marginBottom: '8px', fontWeight: 'bold' }}>匹配规则说明:</div>
1766
- <div style={{ lineHeight: '1.6' }}>
1767
- <div>• <strong>精确匹配</strong>:参数值完全相等</div>
1768
- <div>• <strong>包含</strong>:参数值包含指定内容</div>
1769
- <div>• <strong>正则</strong>:参数值符合正则表达式</div>
1770
- <div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
1771
- </div>
1772
- </div>
1773
- }
1774
- title="参数匹配规则"
1775
- trigger="click"
1776
- placement="top"
1777
- >
1778
- <Button
1779
- type="text"
1780
- icon={<QuestionCircleOutlined />}
1781
- style={{
1782
- color: '#999',
1783
- padding: '4px'
1784
- }}
1785
- size="small"
1786
- />
1787
- </Popover>
1788
- </div>
1789
- </>
1790
- )}
1791
- </Form.List>
1804
+ {/* 入参匹配规则已移至各响应内部配置,此处保留隐藏字段向后兼容旧数据 */}
1805
+ <Form.Item name="paramMatchers" hidden initialValue={[]}>
1806
+ <Input />
1792
1807
  </Form.Item>
1793
1808
 
1794
1809
  {/* 隐藏的表单字段用于存储响应数据 */}
@@ -5,8 +5,8 @@
5
5
  import React, { useState, useEffect } from 'react';
6
6
  import { useHistory } from 'react-router-dom';
7
7
  import AppLayout from '../components/AppLayout';
8
- import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination } from 'antd';
9
- import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined } from '@ant-design/icons';
8
+ import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag } from 'antd';
9
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons';
10
10
  import '../styles/mock-data.css';
11
11
  import axios from 'axios';
12
12
 
@@ -40,7 +40,8 @@ const MockData = () => {
40
40
  sortBy: 'name', // name, createdAt, interfaceCount, active
41
41
  sortOrder: 'ascend', // ascend, descend
42
42
  pageSize: 12,
43
- current: 1
43
+ current: 1,
44
+ viewMode: 'card' // card, list
44
45
  };
45
46
  });
46
47
 
@@ -368,6 +369,10 @@ const MockData = () => {
368
369
  saveListConfig({ current: page, pageSize });
369
370
  };
370
371
 
372
+ const handleViewModeChange = (mode) => {
373
+ saveListConfig({ viewMode: mode, current: 1 });
374
+ };
375
+
371
376
  // 获取排序后的功能列表
372
377
  const getSortedFeatures = () => {
373
378
  const sorted = [...mockFeatures].sort((a, b) => {
@@ -516,6 +521,139 @@ const MockData = () => {
516
521
  );
517
522
  };
518
523
 
524
+ // 渲染列表视图
525
+ const renderListView = () => {
526
+ const columns = [
527
+ {
528
+ title: '状态',
529
+ dataIndex: 'active',
530
+ key: 'active',
531
+ width: 100,
532
+ render: (active, record) => (
533
+ <Switch
534
+ checked={active}
535
+ onChange={(checked, e) => {
536
+ e.stopPropagation();
537
+ handleToggleActive(record.id, active);
538
+ }}
539
+ checkedChildren="启用"
540
+ unCheckedChildren="禁用"
541
+ />
542
+ ),
543
+ },
544
+ {
545
+ title: '功能名称',
546
+ dataIndex: 'name',
547
+ key: 'name',
548
+ ellipsis: { tooltip: true },
549
+ render: (name) => (
550
+ <Text strong style={{ fontSize: '14px' }}>{name}</Text>
551
+ ),
552
+ },
553
+ {
554
+ title: '功能描述',
555
+ dataIndex: 'description',
556
+ key: 'description',
557
+ ellipsis: { tooltip: true },
558
+ render: (description) => (
559
+ <Text type="secondary">{description || '无描述'}</Text>
560
+ ),
561
+ },
562
+ {
563
+ title: '接口数量',
564
+ dataIndex: 'interfaceCount',
565
+ key: 'interfaceCount',
566
+ width: 100,
567
+ align: 'center',
568
+ render: (count) => (
569
+ <Badge count={count || 0} showZero style={{ backgroundColor: '#1890ff' }} />
570
+ ),
571
+ },
572
+ {
573
+ title: '创建时间',
574
+ dataIndex: 'createdAt',
575
+ key: 'createdAt',
576
+ width: 120,
577
+ render: (createdAt) => (
578
+ <Text type="secondary">
579
+ {createdAt ? new Date(createdAt).toLocaleDateString() : '未知日期'}
580
+ </Text>
581
+ ),
582
+ },
583
+ {
584
+ title: '操作',
585
+ key: 'action',
586
+ width: 200,
587
+ fixed: 'right',
588
+ render: (_, record) => (
589
+ <Space size="small">
590
+ <Tooltip title="管理接口">
591
+ <Button
592
+ type="link"
593
+ size="small"
594
+ icon={<ApiOutlined />}
595
+ onClick={(e) => {
596
+ e.stopPropagation();
597
+ viewInterfaces(record);
598
+ }}
599
+ >
600
+ 接口
601
+ </Button>
602
+ </Tooltip>
603
+ <Tooltip title="编辑">
604
+ <Button
605
+ type="link"
606
+ size="small"
607
+ icon={<EditOutlined />}
608
+ onClick={(e) => {
609
+ e.stopPropagation();
610
+ openModal(record);
611
+ }}
612
+ />
613
+ </Tooltip>
614
+ <Tooltip title="导出">
615
+ <Button
616
+ type="link"
617
+ size="small"
618
+ icon={<ExportOutlined />}
619
+ onClick={(e) => {
620
+ e.stopPropagation();
621
+ exportFeatureConfig(record);
622
+ }}
623
+ />
624
+ </Tooltip>
625
+ <Tooltip title="删除">
626
+ <Button
627
+ type="link"
628
+ size="small"
629
+ danger
630
+ icon={<DeleteOutlined />}
631
+ onClick={(e) => {
632
+ e.stopPropagation();
633
+ deleteFeature(record.id);
634
+ }}
635
+ />
636
+ </Tooltip>
637
+ </Space>
638
+ ),
639
+ },
640
+ ];
641
+
642
+ return (
643
+ <Table
644
+ columns={columns}
645
+ dataSource={getPaginatedFeatures()}
646
+ rowKey="id"
647
+ pagination={false}
648
+ onRow={(record) => ({
649
+ onClick: () => viewInterfaces(record),
650
+ style: { cursor: 'pointer' }
651
+ })}
652
+ rowClassName={(record) => !record.active ? 'inactive-row' : ''}
653
+ />
654
+ );
655
+ };
656
+
519
657
  return (
520
658
  <AppLayout>
521
659
  <div className="page-container">
@@ -584,7 +722,7 @@ const MockData = () => {
584
722
  </div>
585
723
  </Col>
586
724
 
587
- {/* 排序控制区域 */}
725
+ {/* 排序和视图控制区域 */}
588
726
  <Col xs={24} sm={12} md={8} lg={9}>
589
727
  <div className="sort-controls">
590
728
  <div className="control-group">
@@ -615,8 +753,30 @@ const MockData = () => {
615
753
  </div>
616
754
  </Col>
617
755
 
756
+ {/* 视图切换区域 */}
757
+ <Col xs={24} sm={12} md={4} lg={3}>
758
+ <div className="view-mode-controls">
759
+ <Button.Group>
760
+ <Tooltip title="卡片视图">
761
+ <Button
762
+ type={listConfig.viewMode === 'card' ? 'primary' : 'default'}
763
+ icon={<AppstoreOutlined />}
764
+ onClick={() => handleViewModeChange('card')}
765
+ />
766
+ </Tooltip>
767
+ <Tooltip title="列表视图">
768
+ <Button
769
+ type={listConfig.viewMode === 'list' ? 'primary' : 'default'}
770
+ icon={<UnorderedListOutlined />}
771
+ onClick={() => handleViewModeChange('list')}
772
+ />
773
+ </Tooltip>
774
+ </Button.Group>
775
+ </div>
776
+ </Col>
777
+
618
778
  {/* 分页控制区域 */}
619
- <Col xs={24} sm={24} md={8} lg={9}>
779
+ <Col xs={24} sm={24} md={12} lg={6}>
620
780
  <div className="pagination-controls">
621
781
  <div className="control-group">
622
782
  <span className="control-label">每页显示</span>
@@ -649,9 +809,13 @@ const MockData = () => {
649
809
  </div>
650
810
  ) : mockFeatures.length > 0 ? (
651
811
  <>
652
- <Row gutter={[20, 20]} className="feature-grid">
653
- {getPaginatedFeatures().map(feature => renderFeatureCard(feature))}
654
- </Row>
812
+ {listConfig.viewMode === 'card' ? (
813
+ <Row gutter={[20, 20]} className="feature-grid">
814
+ {getPaginatedFeatures().map(feature => renderFeatureCard(feature))}
815
+ </Row>
816
+ ) : (
817
+ renderListView()
818
+ )}
655
819
 
656
820
  {/* 分页组件 */}
657
821
  {mockFeatures.length > listConfig.pageSize && (
@@ -1115,4 +1115,71 @@ input:checked + .slider:before {
1115
1115
  .ant-card-extra {
1116
1116
  margin-right: 60px;
1117
1117
  }
1118
+ }
1119
+
1120
+ /* 视图切换控制样式 */
1121
+ .view-mode-controls {
1122
+ display: flex;
1123
+ justify-content: flex-end;
1124
+ align-items: center;
1125
+ }
1126
+
1127
+ .view-mode-controls .ant-btn-group {
1128
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
1129
+ }
1130
+
1131
+ /* 列表视图样式 */
1132
+ .feature-list-card .ant-table-wrapper {
1133
+ margin-top: 0;
1134
+ }
1135
+
1136
+ .feature-list-card .ant-table {
1137
+ font-size: 14px;
1138
+ }
1139
+
1140
+ .feature-list-card .ant-table-thead > tr > th {
1141
+ background-color: #fafafa;
1142
+ font-weight: 600;
1143
+ color: #262626;
1144
+ padding: 16px;
1145
+ }
1146
+
1147
+ .feature-list-card .ant-table-tbody > tr > td {
1148
+ padding: 16px;
1149
+ }
1150
+
1151
+ .feature-list-card .ant-table-tbody > tr:hover {
1152
+ background-color: #f5f5f5;
1153
+ }
1154
+
1155
+ .feature-list-card .ant-table-tbody > tr.inactive-row {
1156
+ background-color: #fafafa;
1157
+ opacity: 0.7;
1158
+ }
1159
+
1160
+ .feature-list-card .ant-table-tbody > tr.inactive-row:hover {
1161
+ background-color: #f0f0f0;
1162
+ }
1163
+
1164
+ /* 列表视图中的操作按钮样式 */
1165
+ .feature-list-card .ant-btn-link {
1166
+ padding: 4px 8px;
1167
+ height: auto;
1168
+ }
1169
+
1170
+ /* 响应式调整 */
1171
+ @media (max-width: 768px) {
1172
+ .view-mode-controls {
1173
+ justify-content: center;
1174
+ margin-top: 12px;
1175
+ }
1176
+
1177
+ .feature-list-card .ant-table {
1178
+ font-size: 13px;
1179
+ }
1180
+
1181
+ .feature-list-card .ant-table-thead > tr > th,
1182
+ .feature-list-card .ant-table-tbody > tr > td {
1183
+ padding: 12px 8px;
1184
+ }
1118
1185
  }