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.
- package/VERSION.md +11 -1
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +4 -0
- package/lib/ruleManager.js +55 -23
- package/package.json +1 -1
- package/ui/src/components/interface-management/ResponseContentEditor.js +191 -30
- package/ui/src/pages/InterfaceManagementV2.js +157 -142
- package/ui/src/pages/MockDataV2.js +172 -8
- package/ui/src/styles/mock-data.css +67 -0
|
@@ -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: '
|
|
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: '
|
|
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:
|
|
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:
|
|
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
|
|
831
|
+
return <span style={{ color: '#ff4d4f', fontSize: '12px' }}>无响应数据</span>;
|
|
811
832
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
<
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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: '
|
|
1014
|
+
title: '匹配规则',
|
|
1015
|
+
dataIndex: 'responses',
|
|
950
1016
|
key: 'paramMatchers',
|
|
951
1017
|
width: 100,
|
|
952
|
-
render: (
|
|
1018
|
+
render: (responses, record) => {
|
|
953
1019
|
if (record.proxyType !== 'response') {
|
|
954
|
-
return '
|
|
1020
|
+
return <span style={{ color: '#ccc' }}>-</span>;
|
|
955
1021
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
<
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
653
|
-
{
|
|
654
|
-
|
|
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
|
}
|