whistle.mock-plugins 1.1.1-dev.8 → 1.1.1-dev.9

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.
@@ -516,6 +516,175 @@ const initRouter = () => {
516
516
  }
517
517
  });
518
518
 
519
+ // 批量更新接口状态
520
+ router.patch('/interfaces', (req, res) => {
521
+ try {
522
+ const { active } = req.query;
523
+ const { featureId, group } = req.body;
524
+
525
+ if (active === undefined) {
526
+ return res.status(400).json({
527
+ code: 1,
528
+ message: '状态参数为必填项'
529
+ });
530
+ }
531
+
532
+ if (!featureId) {
533
+ return res.status(400).json({
534
+ code: 1,
535
+ message: '功能ID为必填项'
536
+ });
537
+ }
538
+
539
+ // 检查功能是否存在
540
+ const featuresFile = path.join(DATA_DIR, 'features.json');
541
+ if (!fs.existsSync(featuresFile)) {
542
+ return res.status(404).json({
543
+ code: 1,
544
+ message: '功能不存在'
545
+ });
546
+ }
547
+
548
+ const features = fs.readJsonSync(featuresFile);
549
+ const feature = features.find(f => f.id === featureId);
550
+ if (!feature) {
551
+ return res.status(404).json({
552
+ code: 1,
553
+ message: '功能不存在'
554
+ });
555
+ }
556
+
557
+ // 读取接口列表
558
+ const interfacesFile = path.join(DATA_DIR, `interfaces-${featureId}.json`);
559
+ if (!fs.existsSync(interfacesFile)) {
560
+ return res.status(404).json({
561
+ code: 1,
562
+ message: '接口列表不存在'
563
+ });
564
+ }
565
+
566
+ const interfaces = fs.readJsonSync(interfacesFile);
567
+
568
+ // 根据分组筛选要更新的接口
569
+ let updatedCount = 0;
570
+ const updatedInterfaces = interfaces.map(item => {
571
+ // 如果指定了分组,只更新该分组下的接口
572
+ if (group && item.group !== group) {
573
+ return item;
574
+ }
575
+
576
+ // 更新接口状态
577
+ if (item.active !== (active === 'true')) {
578
+ updatedCount++;
579
+ return {
580
+ ...item,
581
+ active: active === 'true',
582
+ updatedAt: new Date().toISOString()
583
+ };
584
+ }
585
+
586
+ return item;
587
+ });
588
+
589
+ // 保存更新后的接口列表
590
+ fs.writeJsonSync(interfacesFile, updatedInterfaces, { spaces: 2 });
591
+
592
+ res.json({
593
+ code: 0,
594
+ message: `成功${active === 'true' ? '启用' : '禁用'} ${updatedCount} 个接口`,
595
+ data: { updatedCount }
596
+ });
597
+ } catch (err) {
598
+ console.error('批量更新接口状态失败:', err);
599
+ res.status(500).json({
600
+ code: 1,
601
+ message: '批量更新接口状态失败: ' + err.message
602
+ });
603
+ }
604
+ });
605
+
606
+ // 部分更新接口
607
+ router.patch('/interfaces/:id', (req, res) => {
608
+ try {
609
+ const { id } = req.params;
610
+ const updateData = req.body;
611
+
612
+ if (!id) {
613
+ return res.status(400).json({
614
+ code: 1,
615
+ message: '接口ID为必填项'
616
+ });
617
+ }
618
+
619
+ // 检查功能是否存在
620
+ const featuresFile = path.join(DATA_DIR, 'features.json');
621
+ if (!fs.existsSync(featuresFile)) {
622
+ return res.status(404).json({
623
+ code: 1,
624
+ message: '功能不存在'
625
+ });
626
+ }
627
+
628
+ const features = fs.readJsonSync(featuresFile);
629
+
630
+ // 在所有功能模块中查找接口
631
+ let foundInterface = null;
632
+ let foundFeature = null;
633
+ let foundInterfaceIndex = -1;
634
+ let interfacesFile = '';
635
+
636
+ for (const feature of features) {
637
+ const featureInterfacesFile = path.join(DATA_DIR, `interfaces-${feature.id}.json`);
638
+ if (fs.existsSync(featureInterfacesFile)) {
639
+ const interfaces = fs.readJsonSync(featureInterfacesFile);
640
+ const index = interfaces.findIndex(item => item.id === id);
641
+
642
+ if (index !== -1) {
643
+ foundInterface = interfaces[index];
644
+ foundFeature = feature;
645
+ foundInterfaceIndex = index;
646
+ interfacesFile = featureInterfacesFile;
647
+ break;
648
+ }
649
+ }
650
+ }
651
+
652
+ if (!foundInterface) {
653
+ return res.status(404).json({
654
+ code: 1,
655
+ message: '接口不存在'
656
+ });
657
+ }
658
+
659
+ // 读取接口所在文件的所有接口
660
+ const interfaces = fs.readJsonSync(interfacesFile);
661
+
662
+ // 更新接口
663
+ const updatedInterface = {
664
+ ...foundInterface,
665
+ ...updateData,
666
+ updatedAt: new Date().toISOString()
667
+ };
668
+
669
+ interfaces[foundInterfaceIndex] = updatedInterface;
670
+
671
+ // 保存更新后的接口列表
672
+ fs.writeJsonSync(interfacesFile, interfaces, { spaces: 2 });
673
+
674
+ res.json({
675
+ code: 0,
676
+ message: '接口更新成功',
677
+ data: updatedInterface
678
+ });
679
+ } catch (err) {
680
+ console.error('更新接口失败:', err);
681
+ res.status(500).json({
682
+ code: 1,
683
+ message: '更新接口失败: ' + err.message
684
+ });
685
+ }
686
+ });
687
+
519
688
  return router;
520
689
  };
521
690
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.1.1-dev.8",
3
+ "version": "1.1.1-dev.9",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ import AppLayout from '../components/AppLayout';
4
4
  import {
5
5
  Table, Button, Modal, Form, Input, Select, message, Switch,
6
6
  Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col,
7
- Popover, Checkbox
7
+ Popover, Checkbox, Tag
8
8
  } from 'antd';
9
9
  import {
10
10
  PlusOutlined, EditOutlined, DeleteOutlined,
@@ -32,6 +32,7 @@ const { Option } = Select;
32
32
  const COLUMN_CONFIG = [
33
33
  { key: 'active', title: '状态', required: true },
34
34
  { key: 'name', title: '名称', required: true },
35
+ { key: 'group', title: '分组', required: false },
35
36
  { key: 'urlPattern', title: 'URL匹配规则', required: false },
36
37
  { key: 'proxyType', title: '处理方式', required: false },
37
38
  { key: 'currentResponse', title: '当前响应', required: false },
@@ -57,6 +58,11 @@ const InterfaceManagement = () => {
57
58
  const [interfacesLoading, setInterfacesLoading] = useState(false);
58
59
  const [selectedFeatureId, setSelectedFeatureId] = useState(null);
59
60
 
61
+ // 分组状态
62
+ const [groups, setGroups] = useState([]);
63
+ const [selectedGroup, setSelectedGroup] = useState(null);
64
+ const [groupActionLoading, setGroupActionLoading] = useState(false);
65
+
60
66
  // 模态框状态
61
67
  const [modalVisible, setModalVisible] = useState(false);
62
68
  const [previewVisible, setPreviewVisible] = useState(false);
@@ -166,6 +172,9 @@ const InterfaceManagement = () => {
166
172
 
167
173
  setInterfaces(processedInterfaces);
168
174
  console.log('处理后的接口列表:', processedInterfaces);
175
+
176
+ // 提取并更新分组列表
177
+ updateGroups(processedInterfaces);
169
178
  } else {
170
179
  console.warn('接口数据格式不正确:', response.data);
171
180
  setInterfaces([]);
@@ -179,6 +188,41 @@ const InterfaceManagement = () => {
179
188
  setInterfacesLoading(false);
180
189
  }
181
190
  };
191
+
192
+ // 提取并更新分组列表
193
+ const updateGroups = (interfaces) => {
194
+ // 从接口列表中提取所有分组
195
+ const groupSet = new Set();
196
+ interfaces.forEach(item => {
197
+ if (item.group) {
198
+ groupSet.add(item.group);
199
+ }
200
+ });
201
+
202
+ // 转换为数组并排序
203
+ const groupArray = Array.from(groupSet).sort();
204
+ setGroups(groupArray);
205
+
206
+ // 如果当前选中的分组不在列表中,重置选中的分组
207
+ if (selectedGroup && !groupArray.includes(selectedGroup)) {
208
+ setSelectedGroup(null);
209
+ }
210
+ };
211
+
212
+ // 根据分组筛选接口
213
+ const getFilteredInterfaces = () => {
214
+ // 首先按功能模块筛选
215
+ const featureFiltered = interfaces.filter(item =>
216
+ !selectedFeatureId || item.featureId === selectedFeatureId
217
+ );
218
+
219
+ // 然后按分组筛选
220
+ if (selectedGroup) {
221
+ return featureFiltered.filter(item => item.group === selectedGroup);
222
+ }
223
+
224
+ return featureFiltered;
225
+ };
182
226
 
183
227
  const handleAddInterface = () => {
184
228
  if (!selectedFeatureId) {
@@ -256,6 +300,7 @@ const InterfaceManagement = () => {
256
300
  // 设置表单值
257
301
  form.setFieldsValue({
258
302
  name: record.name,
303
+ group: record.group || '',
259
304
  pattern: record.urlPattern,
260
305
  proxyType: record.proxyType || 'response',
261
306
  statusCode: (record.httpStatus || record.statusCode || 200).toString(),
@@ -294,12 +339,15 @@ const InterfaceManagement = () => {
294
339
 
295
340
  const handleToggleActive = async (id, currentActive) => {
296
341
  try {
297
- const response = await axios.patch(`/cgi-bin/interfaces?id=${id}`, {
342
+ const response = await axios.patch(`/cgi-bin/interfaces/${id}`, {
298
343
  active: !currentActive
299
344
  });
300
345
  if (response.data && response.data.code === 0) {
301
346
  message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
302
347
  fetchInterfaces();
348
+
349
+ // 刷新规则缓存
350
+ refreshCacheAfterUpdate();
303
351
  } else {
304
352
  throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
305
353
  }
@@ -328,6 +376,7 @@ const InterfaceManagement = () => {
328
376
  // 构建接口数据
329
377
  const interfaceData = {
330
378
  name: values.name,
379
+ group: values.group,
331
380
  urlPattern: values.pattern,
332
381
  proxyType: values.proxyType,
333
382
  featureId: selectedFeatureId,
@@ -460,9 +509,7 @@ const InterfaceManagement = () => {
460
509
  }
461
510
  };
462
511
 
463
- const filteredInterfaces = interfaces.filter(item =>
464
- !selectedFeatureId || item.featureId === selectedFeatureId
465
- );
512
+ const filteredInterfaces = getFilteredInterfaces();
466
513
 
467
514
  // 处理表格变化(排序、分页)
468
515
  const handleTableChange = (pagination, filters, sorter) => {
@@ -527,6 +574,27 @@ const InterfaceManagement = () => {
527
574
  sorter: (a, b) => a.name.localeCompare(b.name),
528
575
  render: (text) => <span className="interface-name">{text}</span>,
529
576
  },
577
+ {
578
+ title: '分组',
579
+ dataIndex: 'group',
580
+ key: 'group',
581
+ width: 120,
582
+ sorter: (a, b) => {
583
+ const groupA = a.group || '';
584
+ const groupB = b.group || '';
585
+ return groupA.localeCompare(groupB);
586
+ },
587
+ render: (group) => {
588
+ if (!group) {
589
+ return <span style={{ color: '#999', fontStyle: 'italic' }}>未分组</span>;
590
+ }
591
+ return (
592
+ <Tag color="blue" style={{ cursor: 'pointer' }} onClick={() => setSelectedGroup(group)}>
593
+ {group}
594
+ </Tag>
595
+ );
596
+ },
597
+ },
530
598
  {
531
599
  title: 'URL匹配规则',
532
600
  dataIndex: 'urlPattern',
@@ -775,6 +843,28 @@ const InterfaceManagement = () => {
775
843
  const visibleColumns = tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key);
776
844
  const columns = allColumns.filter(col => visibleColumns.includes(col.key));
777
845
 
846
+ // 分组筛选和操作区域
847
+ const handleBatchToggleActive = async (active) => {
848
+ try {
849
+ setGroupActionLoading(true);
850
+ const response = await axios.patch(`/cgi-bin/interfaces?active=${active}`, {
851
+ featureId: selectedFeatureId,
852
+ group: selectedGroup
853
+ });
854
+ if (response.data && response.data.code === 0) {
855
+ message.success(`分组 "${selectedGroup}" 中的接口已成功${active ? '启用' : '禁用'}`);
856
+ fetchInterfaces();
857
+ } else {
858
+ throw new Error(response.data?.message || '批量操作失败');
859
+ }
860
+ } catch (error) {
861
+ console.error('批量操作失败:', error);
862
+ message.error(error.response?.data?.message || error.message || '批量操作失败');
863
+ } finally {
864
+ setGroupActionLoading(false);
865
+ }
866
+ };
867
+
778
868
  return (
779
869
  <AppLayout>
780
870
  <div className="interface-management-container">
@@ -828,6 +918,77 @@ const InterfaceManagement = () => {
828
918
  />
829
919
  )}
830
920
 
921
+ {/* 分组筛选和操作区域 */}
922
+ <div style={{
923
+ display: 'flex',
924
+ justifyContent: 'space-between',
925
+ alignItems: 'center',
926
+ marginBottom: 16,
927
+ padding: '12px 16px',
928
+ backgroundColor: '#f5f5f5',
929
+ borderRadius: '4px'
930
+ }}>
931
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
932
+ <span>分组筛选:</span>
933
+ <Select
934
+ value={selectedGroup}
935
+ onChange={value => setSelectedGroup(value)}
936
+ style={{ width: 200 }}
937
+ placeholder="选择分组"
938
+ allowClear
939
+ showSearch
940
+ loading={interfacesLoading}
941
+ >
942
+ {groups.map(group => (
943
+ <Option key={group} value={group}>{group}</Option>
944
+ ))}
945
+ </Select>
946
+ {selectedGroup && (
947
+ <Button
948
+ type="link"
949
+ onClick={() => setSelectedGroup(null)}
950
+ size="small"
951
+ >
952
+ 清除筛选
953
+ </Button>
954
+ )}
955
+ </div>
956
+
957
+ {selectedGroup && (
958
+ <div style={{ display: 'flex', gap: '8px' }}>
959
+ <Popconfirm
960
+ title={`确定要启用分组 "${selectedGroup}" 中的所有接口吗?`}
961
+ onConfirm={() => handleBatchToggleActive(true)}
962
+ okText="确定"
963
+ cancelText="取消"
964
+ >
965
+ <Button
966
+ type="primary"
967
+ size="small"
968
+ loading={groupActionLoading}
969
+ >
970
+ 批量启用
971
+ </Button>
972
+ </Popconfirm>
973
+
974
+ <Popconfirm
975
+ title={`确定要禁用分组 "${selectedGroup}" 中的所有接口吗?`}
976
+ onConfirm={() => handleBatchToggleActive(false)}
977
+ okText="确定"
978
+ cancelText="取消"
979
+ >
980
+ <Button
981
+ danger
982
+ size="small"
983
+ loading={groupActionLoading}
984
+ >
985
+ 批量禁用
986
+ </Button>
987
+ </Popconfirm>
988
+ </div>
989
+ )}
990
+ </div>
991
+
831
992
  {/* 接口列表状态栏 */}
832
993
  {filteredInterfaces.length > 0 && !interfacesLoading && (
833
994
  <div style={{
@@ -843,6 +1004,11 @@ const InterfaceManagement = () => {
843
1004
  }}>
844
1005
  <span>
845
1006
  当前功能模块:<strong>{selectedFeature?.name}</strong>
1007
+ {selectedGroup && (
1008
+ <span style={{ marginLeft: 8 }}>
1009
+ | 分组:<Tag color="blue">{selectedGroup}</Tag>
1010
+ </span>
1011
+ )}
846
1012
  </span>
847
1013
  <span>
848
1014
  共 {filteredInterfaces.length} 个接口,每页显示 {tableConfig.pageSize} 个
@@ -957,6 +1123,7 @@ const InterfaceManagement = () => {
957
1123
  layout="vertical"
958
1124
  initialValues={{
959
1125
  name: '',
1126
+ group: '',
960
1127
  pattern: '',
961
1128
  proxyType: 'response',
962
1129
  statusCode: '200',
@@ -969,7 +1136,7 @@ const InterfaceManagement = () => {
969
1136
  responseDelay: '0'
970
1137
  }}
971
1138
  >
972
- {/* 基础信息表单项保持原样 */}
1139
+ {/* 基础信息表单项 */}
973
1140
  <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
974
1141
  <div style={{ flex: 1 }}>
975
1142
  <Form.Item
@@ -980,6 +1147,26 @@ const InterfaceManagement = () => {
980
1147
  <Input placeholder="请输入接口名称" />
981
1148
  </Form.Item>
982
1149
  </div>
1150
+ <div style={{ flex: 1 }}>
1151
+ <Form.Item
1152
+ name="group"
1153
+ label="分组"
1154
+ tooltip="为接口设置分组,便于管理和批量操作"
1155
+ >
1156
+ <Select
1157
+ placeholder="选择或创建分组"
1158
+ showSearch
1159
+ allowClear
1160
+ >
1161
+ {Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
1162
+ <Option key={group} value={group}>{group}</Option>
1163
+ ))}
1164
+ </Select>
1165
+ </Form.Item>
1166
+ </div>
1167
+ </div>
1168
+
1169
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
983
1170
  <div style={{ flex: 1 }}>
984
1171
  <Form.Item
985
1172
  name="pattern"
@@ -1059,9 +1246,6 @@ const InterfaceManagement = () => {
1059
1246
  />
1060
1247
  </Form.Item>
1061
1248
  </div>
1062
- </div>
1063
-
1064
- <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1065
1249
  <div style={{ flex: 1 }}>
1066
1250
  <Form.Item
1067
1251
  name="proxyType"
@@ -1089,7 +1273,7 @@ const InterfaceManagement = () => {
1089
1273
  const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
1090
1274
  if (patternInput) {
1091
1275
  if (value === 'response') {
1092
- patternInput.placeholder = "例如:/api/users,/api/users/*,/api\/users\/\d+/";
1276
+ patternInput.placeholder = "例如:/api/users,/api/users/*,/api\\/users\\/\\d+/";
1093
1277
  } else if (value === 'redirect') {
1094
1278
  patternInput.placeholder = "例如:https://example.com/api";
1095
1279
  } else if (value === 'url_redirect') {
@@ -1103,6 +1287,9 @@ const InterfaceManagement = () => {
1103
1287
  </Select>
1104
1288
  </Form.Item>
1105
1289
  </div>
1290
+ </div>
1291
+
1292
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
1106
1293
  <div style={{ flex: 1 }}>
1107
1294
  <Form.Item
1108
1295
  name="httpMethod"
@@ -1116,6 +1303,9 @@ const InterfaceManagement = () => {
1116
1303
  </Select>
1117
1304
  </Form.Item>
1118
1305
  </div>
1306
+ <div style={{ flex: 1 }}>
1307
+ {/* 这里可以留空或添加其他字段 */}
1308
+ </div>
1119
1309
  </div>
1120
1310
 
1121
1311
  {/* 根据proxyType显示不同的表单项 */}