whistle.mock-plugins 1.1.3-beta.3 → 1.1.3-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +0 -6
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +30 -162
- package/ui/src/pages/InterfaceManagementV2.js +1434 -21
- package/ui/src/pages/MockDataV2.js +464 -440
- package/ui/src/styles/interface-management.css +89 -38
|
@@ -1,48 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MockDataV2.js -
|
|
3
|
-
*
|
|
4
|
-
* 当前内容与V1版本相同,等待后续重构
|
|
2
|
+
* MockDataV2.js - 功能模块管理页面(V2版本)
|
|
3
|
+
* 功能和UI与V1版本完全一致,仅保留V2版本标识
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import React, { useState, useEffect } from 'react';
|
|
8
7
|
import { useHistory } from 'react-router-dom';
|
|
9
8
|
import AppLayout from '../components/AppLayout';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
Row, Col, Badge, Popconfirm, Alert, Space, Select,
|
|
13
|
-
Tooltip, Menu, Dropdown, Pagination, AutoComplete
|
|
14
|
-
} from 'antd';
|
|
15
|
-
import {
|
|
16
|
-
PlusOutlined, DeleteOutlined, EditOutlined, SettingOutlined,
|
|
17
|
-
ExportOutlined, ImportOutlined, SearchOutlined, FilterOutlined,
|
|
18
|
-
SortAscendingOutlined, ReloadOutlined, ClearOutlined, DownloadOutlined
|
|
19
|
-
} from '@ant-design/icons';
|
|
9
|
+
import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination } from 'antd';
|
|
10
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined } from '@ant-design/icons';
|
|
20
11
|
import '../styles/mock-data.css';
|
|
12
|
+
import axios from 'axios';
|
|
21
13
|
|
|
22
|
-
const {
|
|
14
|
+
const { Text, Title, Paragraph } = Typography;
|
|
23
15
|
const { TextArea } = Input;
|
|
24
16
|
|
|
25
|
-
//
|
|
17
|
+
// 刷新缓存服务
|
|
26
18
|
const flushCache = async () => {
|
|
27
19
|
try {
|
|
28
|
-
|
|
20
|
+
const response = await axios.get(`/_flush_cache`);
|
|
21
|
+
return response.data;
|
|
29
22
|
} catch (error) {
|
|
30
23
|
console.error('刷新缓存失败:', error);
|
|
31
24
|
throw error;
|
|
32
25
|
}
|
|
33
26
|
};
|
|
34
27
|
|
|
35
|
-
|
|
36
28
|
const MockData = () => {
|
|
37
29
|
const history = useHistory();
|
|
38
30
|
const [mockFeatures, setMockFeatures] = useState([]);
|
|
39
31
|
const [showModal, setShowModal] = useState(false);
|
|
40
32
|
const [currentFeature, setCurrentFeature] = useState(null);
|
|
41
|
-
|
|
42
|
-
name: '',
|
|
43
|
-
description: '',
|
|
44
|
-
active: true
|
|
45
|
-
});
|
|
33
|
+
|
|
46
34
|
const [loading, setLoading] = useState(true);
|
|
47
35
|
const [form] = Form.useForm();
|
|
48
36
|
|
|
@@ -91,353 +79,435 @@ const MockData = () => {
|
|
|
91
79
|
};
|
|
92
80
|
|
|
93
81
|
const openModal = (feature = null) => {
|
|
94
|
-
setCurrentFeature(feature);
|
|
95
82
|
if (feature) {
|
|
96
|
-
|
|
97
|
-
name: feature.name,
|
|
98
|
-
description: feature.description || '',
|
|
99
|
-
active: feature.active !== false
|
|
100
|
-
});
|
|
83
|
+
setCurrentFeature(feature);
|
|
101
84
|
form.setFieldsValue({
|
|
102
85
|
name: feature.name,
|
|
103
|
-
description: feature.description
|
|
86
|
+
description: feature.description,
|
|
104
87
|
active: feature.active !== false
|
|
105
88
|
});
|
|
106
89
|
} else {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
90
|
+
setCurrentFeature(null);
|
|
91
|
+
form.resetFields();
|
|
92
|
+
form.setFieldsValue({
|
|
110
93
|
active: true
|
|
111
94
|
});
|
|
112
|
-
form.resetFields();
|
|
113
95
|
}
|
|
114
96
|
setShowModal(true);
|
|
115
97
|
};
|
|
116
98
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const submitData = {
|
|
121
|
-
name: values.name.trim(),
|
|
122
|
-
description: values.description?.trim() || '',
|
|
123
|
-
active: values.active !== false
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
console.log('处理后的提交数据:', submitData);
|
|
99
|
+
const closeModal = () => {
|
|
100
|
+
setShowModal(false);
|
|
101
|
+
};
|
|
127
102
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'Content-Type': 'application/json',
|
|
134
|
-
},
|
|
135
|
-
body: JSON.stringify(submitData),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const result = await response.json();
|
|
139
|
-
console.log('更新响应:', result);
|
|
140
|
-
|
|
141
|
-
if (result.code === 0) {
|
|
142
|
-
message.success('功能模块更新成功');
|
|
143
|
-
setShowModal(false);
|
|
144
|
-
fetchFeatures();
|
|
145
|
-
|
|
146
|
-
// 刷新缓存
|
|
147
|
-
await flushCache();
|
|
148
|
-
} else {
|
|
149
|
-
message.error('功能模块更新失败: ' + (result.message || '未知错误'));
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
// 创建新功能模块
|
|
153
|
-
const response = await fetch('/cgi-bin/features', {
|
|
154
|
-
method: 'POST',
|
|
155
|
-
headers: {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
},
|
|
158
|
-
body: JSON.stringify(submitData),
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const result = await response.json();
|
|
162
|
-
console.log('创建响应:', result);
|
|
163
|
-
|
|
164
|
-
if (result.code === 0) {
|
|
165
|
-
message.success('功能模块创建成功');
|
|
166
|
-
setShowModal(false);
|
|
167
|
-
fetchFeatures();
|
|
168
|
-
|
|
169
|
-
// 刷新缓存
|
|
170
|
-
await flushCache();
|
|
171
|
-
} else {
|
|
172
|
-
message.error('功能模块创建失败: ' + (result.message || '未知错误'));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
103
|
+
// 更新成功后刷新缓存
|
|
104
|
+
const refreshCacheAfterUpdate = async () => {
|
|
105
|
+
try {
|
|
106
|
+
await flushCache();
|
|
107
|
+
// 不显示提示,避免干扰主操作的反馈
|
|
175
108
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
109
|
+
// 记录错误但不影响用户体验
|
|
110
|
+
console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
|
|
178
111
|
}
|
|
179
112
|
};
|
|
180
113
|
|
|
181
|
-
const
|
|
114
|
+
const handleSubmit = async (values) => {
|
|
182
115
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
116
|
+
const featureData = {
|
|
117
|
+
...values
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// 如果是编辑已有功能,添加ID
|
|
121
|
+
if (currentFeature) {
|
|
122
|
+
featureData.id = currentFeature.id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setLoading(true);
|
|
126
|
+
const response = await fetch('/cgi-bin/features', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/json'
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify(featureData)
|
|
185
132
|
});
|
|
186
133
|
|
|
187
134
|
const result = await response.json();
|
|
135
|
+
|
|
188
136
|
if (result.code === 0) {
|
|
189
|
-
|
|
137
|
+
// 刷新功能列表
|
|
138
|
+
message.success(currentFeature ? '功能模块更新成功' : '功能模块创建成功');
|
|
190
139
|
fetchFeatures();
|
|
140
|
+
closeModal();
|
|
191
141
|
|
|
192
142
|
// 刷新缓存
|
|
193
|
-
|
|
143
|
+
refreshCacheAfterUpdate();
|
|
194
144
|
} else {
|
|
195
|
-
message.error('
|
|
145
|
+
message.error('操作失败: ' + result.message);
|
|
196
146
|
}
|
|
197
147
|
} catch (error) {
|
|
198
|
-
console.error('
|
|
199
|
-
message.error('
|
|
148
|
+
console.error('保存功能模块错误:', error);
|
|
149
|
+
message.error('保存失败: ' + error.message);
|
|
150
|
+
} finally {
|
|
151
|
+
setLoading(false);
|
|
200
152
|
}
|
|
201
153
|
};
|
|
202
154
|
|
|
203
155
|
const handleToggleActive = async (id, currentActive) => {
|
|
204
156
|
try {
|
|
205
157
|
const response = await fetch(`/cgi-bin/features?id=${id}`, {
|
|
206
|
-
method: '
|
|
158
|
+
method: 'PATCH',
|
|
207
159
|
headers: {
|
|
208
|
-
'Content-Type': 'application/json'
|
|
160
|
+
'Content-Type': 'application/json'
|
|
209
161
|
},
|
|
210
162
|
body: JSON.stringify({
|
|
211
163
|
active: !currentActive
|
|
212
|
-
})
|
|
164
|
+
})
|
|
213
165
|
});
|
|
214
166
|
|
|
215
167
|
const result = await response.json();
|
|
168
|
+
|
|
216
169
|
if (result.code === 0) {
|
|
217
|
-
message.success(
|
|
218
|
-
|
|
170
|
+
message.success(`${currentActive ? '禁用' : '启用'}功能模块成功`);
|
|
171
|
+
// 更新本地状态
|
|
172
|
+
setMockFeatures(mockFeatures.map(feature =>
|
|
173
|
+
feature.id === id ? { ...feature, active: !currentActive } : feature
|
|
174
|
+
));
|
|
219
175
|
|
|
220
|
-
// 刷新缓存
|
|
221
|
-
|
|
176
|
+
// 刷新缓存 - 功能启用状态变化是最关键的需要刷新缓存的操作
|
|
177
|
+
refreshCacheAfterUpdate();
|
|
222
178
|
} else {
|
|
223
|
-
message.error(
|
|
179
|
+
message.error(`${currentActive ? '禁用' : '启用'}功能模块失败: ${result.message}`);
|
|
224
180
|
}
|
|
225
181
|
} catch (error) {
|
|
226
|
-
console.error(`${
|
|
227
|
-
message.error(
|
|
182
|
+
console.error(`${currentActive ? '禁用' : '启用'}功能模块错误:`, error);
|
|
183
|
+
message.error(`操作失败: ${error.message}`);
|
|
228
184
|
}
|
|
229
185
|
};
|
|
230
186
|
|
|
231
|
-
const
|
|
232
|
-
|
|
187
|
+
const deleteFeature = async (id) => {
|
|
188
|
+
Modal.confirm({
|
|
189
|
+
title: '确定要删除此功能吗?',
|
|
190
|
+
content: '这将删除所有相关的接口和模拟数据,此操作无法恢复。',
|
|
191
|
+
okText: '删除',
|
|
192
|
+
okType: 'danger',
|
|
193
|
+
cancelText: '取消',
|
|
194
|
+
onOk: async () => {
|
|
195
|
+
try {
|
|
196
|
+
setLoading(true);
|
|
197
|
+
const response = await fetch(`/cgi-bin/features?id=${id}`, {
|
|
198
|
+
method: 'DELETE'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const result = await response.json();
|
|
202
|
+
|
|
203
|
+
if (result.code === 0) {
|
|
204
|
+
message.success('功能模块已成功删除');
|
|
205
|
+
// 更新本地状态
|
|
206
|
+
setMockFeatures(mockFeatures.filter(f => f.id !== id));
|
|
207
|
+
|
|
208
|
+
// 刷新缓存
|
|
209
|
+
refreshCacheAfterUpdate();
|
|
210
|
+
} else {
|
|
211
|
+
message.error('删除失败: ' + result.message);
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('删除功能错误:', error);
|
|
215
|
+
message.error('操作失败: ' + error.message);
|
|
216
|
+
} finally {
|
|
217
|
+
setLoading(false);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const viewInterfaces = (feature) => {
|
|
224
|
+
// 导航到该功能的接口列表页面
|
|
225
|
+
history.push(`/interface/${feature.id}`);
|
|
233
226
|
};
|
|
234
227
|
|
|
235
|
-
|
|
236
|
-
const exportFeatureConfig = async () => {
|
|
228
|
+
const exportFeatureConfig = async (feature) => {
|
|
237
229
|
try {
|
|
238
|
-
|
|
230
|
+
// 获取该功能的所有接口
|
|
231
|
+
const response = await fetch(`/cgi-bin/interfaces?featureId=${feature.id}`);
|
|
239
232
|
const result = await response.json();
|
|
240
233
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
234
|
+
// 创建完整配置
|
|
235
|
+
const config = {
|
|
236
|
+
...feature,
|
|
237
|
+
interfaces: result.code === 0 ? result.data : []
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const dataStr = JSON.stringify(config, null, 2);
|
|
241
|
+
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
|
242
|
+
|
|
243
|
+
const exportFileDefaultName = `whistle-mock-feature-${feature.id}.json`;
|
|
244
|
+
|
|
245
|
+
const linkElement = document.createElement('a');
|
|
246
|
+
linkElement.setAttribute('href', dataUri);
|
|
247
|
+
linkElement.setAttribute('download', exportFileDefaultName);
|
|
248
|
+
linkElement.click();
|
|
249
|
+
|
|
250
|
+
message.success('配置导出成功');
|
|
258
251
|
} catch (error) {
|
|
259
|
-
console.error('
|
|
252
|
+
console.error('导出配置错误:', error);
|
|
260
253
|
message.error('导出失败: ' + error.message);
|
|
261
254
|
}
|
|
262
255
|
};
|
|
263
256
|
|
|
264
|
-
// 导入功能配置
|
|
265
257
|
const importFeatureConfig = () => {
|
|
266
258
|
const input = document.createElement('input');
|
|
267
259
|
input.type = 'file';
|
|
268
260
|
input.accept = '.json';
|
|
261
|
+
|
|
269
262
|
input.onchange = async (e) => {
|
|
270
263
|
const file = e.target.files[0];
|
|
271
264
|
if (!file) return;
|
|
272
265
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
'
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
266
|
+
const reader = new FileReader();
|
|
267
|
+
reader.onload = async (event) => {
|
|
268
|
+
try {
|
|
269
|
+
const config = JSON.parse(event.target.result);
|
|
270
|
+
|
|
271
|
+
// 验证导入的配置
|
|
272
|
+
if (!config.name) {
|
|
273
|
+
message.error('无效的配置文件: 缺少功能名称');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 创建新功能
|
|
278
|
+
const featureData = {
|
|
279
|
+
name: config.name,
|
|
280
|
+
description: config.description || '',
|
|
281
|
+
active: config.active !== false
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// 保存功能
|
|
285
|
+
const featureResponse = await fetch('/cgi-bin/features', {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'application/json'
|
|
289
|
+
},
|
|
290
|
+
body: JSON.stringify(featureData)
|
|
291
|
+
});
|
|
289
292
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
293
|
+
const featureResult = await featureResponse.json();
|
|
294
|
+
|
|
295
|
+
if (featureResult.code === 0) {
|
|
296
|
+
const newFeature = featureResult.data;
|
|
297
|
+
|
|
298
|
+
// 导入接口配置
|
|
299
|
+
if (Array.isArray(config.interfaces) && config.interfaces.length > 0) {
|
|
300
|
+
message.loading({ content: '正在导入接口配置...', key: 'importInfo' });
|
|
301
|
+
|
|
302
|
+
let successCount = 0;
|
|
303
|
+
let errorCount = 0;
|
|
304
|
+
|
|
305
|
+
for (const interfaceItem of config.interfaces) {
|
|
306
|
+
try {
|
|
307
|
+
// 创建接口,使用新功能ID
|
|
308
|
+
const interfaceData = {
|
|
309
|
+
...interfaceItem,
|
|
310
|
+
featureId: newFeature.id,
|
|
311
|
+
id: undefined // 不使用原接口ID
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
await fetch('/cgi-bin/interfaces', {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
headers: {
|
|
317
|
+
'Content-Type': 'application/json'
|
|
318
|
+
},
|
|
319
|
+
body: JSON.stringify(interfaceData)
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
successCount++;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('导入接口配置失败:', error);
|
|
325
|
+
errorCount++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (errorCount > 0) {
|
|
330
|
+
message.info({ content: `导入完成,成功 ${successCount} 个接口,失败 ${errorCount} 个接口`, key: 'importInfo', duration: 3 });
|
|
331
|
+
} else {
|
|
332
|
+
message.success({ content: `成功导入 ${successCount} 个接口`, key: 'importInfo', duration: 2 });
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
message.success('功能导入成功(无接口配置)');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 刷新功能列表
|
|
339
|
+
fetchFeatures();
|
|
340
|
+
} else {
|
|
341
|
+
message.error('导入功能失败: ' + featureResult.message);
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('导入配置错误:', error);
|
|
345
|
+
message.error('导入失败: ' + error.message);
|
|
294
346
|
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
reader.readAsText(file);
|
|
299
350
|
};
|
|
351
|
+
|
|
300
352
|
input.click();
|
|
301
353
|
};
|
|
302
354
|
|
|
303
|
-
//
|
|
304
|
-
const
|
|
305
|
-
|
|
355
|
+
// 排序处理
|
|
356
|
+
const handleSortChange = (value) => {
|
|
357
|
+
saveListConfig({ sortBy: value, current: 1 });
|
|
358
|
+
};
|
|
306
359
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
360
|
+
const handleSortOrderChange = (value) => {
|
|
361
|
+
saveListConfig({ sortOrder: value, current: 1 });
|
|
362
|
+
};
|
|
310
363
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
filtered = filtered.filter(item =>
|
|
315
|
-
item.name.toLowerCase().includes(searchLower) ||
|
|
316
|
-
(item.description && item.description.toLowerCase().includes(searchLower))
|
|
317
|
-
);
|
|
318
|
-
}
|
|
364
|
+
const handlePageSizeChange = (value) => {
|
|
365
|
+
saveListConfig({ pageSize: value, current: 1 });
|
|
366
|
+
};
|
|
319
367
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
} else if (filterStatus === 'inactive') {
|
|
324
|
-
filtered = filtered.filter(item => item.active === false);
|
|
325
|
-
}
|
|
368
|
+
const handlePageChange = (page, pageSize) => {
|
|
369
|
+
saveListConfig({ current: page, pageSize });
|
|
370
|
+
};
|
|
326
371
|
|
|
327
|
-
|
|
328
|
-
|
|
372
|
+
// 获取排序后的功能列表
|
|
373
|
+
const getSortedFeatures = () => {
|
|
374
|
+
const sorted = [...mockFeatures].sort((a, b) => {
|
|
329
375
|
let aValue, bValue;
|
|
330
376
|
|
|
331
377
|
switch (listConfig.sortBy) {
|
|
332
378
|
case 'name':
|
|
333
379
|
aValue = a.name || '';
|
|
334
380
|
bValue = b.name || '';
|
|
335
|
-
|
|
381
|
+
return listConfig.sortOrder === 'ascend'
|
|
382
|
+
? aValue.localeCompare(bValue, 'zh-CN')
|
|
383
|
+
: bValue.localeCompare(aValue, 'zh-CN');
|
|
384
|
+
|
|
336
385
|
case 'createdAt':
|
|
337
|
-
aValue = new Date(a.createdAt || 0);
|
|
338
|
-
bValue = new Date(b.createdAt || 0);
|
|
339
|
-
|
|
386
|
+
aValue = new Date(a.createdAt || 0).getTime();
|
|
387
|
+
bValue = new Date(b.createdAt || 0).getTime();
|
|
388
|
+
return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue;
|
|
389
|
+
|
|
340
390
|
case 'interfaceCount':
|
|
341
391
|
aValue = a.interfaceCount || 0;
|
|
342
392
|
bValue = b.interfaceCount || 0;
|
|
343
|
-
|
|
393
|
+
return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue;
|
|
394
|
+
|
|
344
395
|
case 'active':
|
|
345
|
-
aValue = a.active
|
|
346
|
-
bValue = b.active
|
|
347
|
-
|
|
396
|
+
aValue = a.active ? 1 : 0;
|
|
397
|
+
bValue = b.active ? 1 : 0;
|
|
398
|
+
return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue;
|
|
399
|
+
|
|
348
400
|
default:
|
|
349
|
-
|
|
350
|
-
bValue = b.name || '';
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (listConfig.sortOrder === 'descend') {
|
|
354
|
-
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
|
|
355
|
-
} else {
|
|
356
|
-
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
|
|
401
|
+
return 0;
|
|
357
402
|
}
|
|
358
403
|
});
|
|
359
|
-
|
|
360
|
-
return
|
|
404
|
+
|
|
405
|
+
return sorted;
|
|
361
406
|
};
|
|
362
407
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// 排序菜单
|
|
371
|
-
const sortMenu = (
|
|
372
|
-
<Menu onClick={({ key }) => {
|
|
373
|
-
const [sortBy, sortOrder] = key.split('-');
|
|
374
|
-
saveListConfig({ sortBy, sortOrder, current: 1 });
|
|
375
|
-
}}>
|
|
376
|
-
<Menu.Item key="name-ascend">名称 (A-Z)</Menu.Item>
|
|
377
|
-
<Menu.Item key="name-descend">名称 (Z-A)</Menu.Item>
|
|
378
|
-
<Menu.Item key="createdAt-descend">创建时间 (新-旧)</Menu.Item>
|
|
379
|
-
<Menu.Item key="createdAt-ascend">创建时间 (旧-新)</Menu.Item>
|
|
380
|
-
<Menu.Item key="interfaceCount-descend">接口数量 (多-少)</Menu.Item>
|
|
381
|
-
<Menu.Item key="interfaceCount-ascend">接口数量 (少-多)</Menu.Item>
|
|
382
|
-
<Menu.Item key="active-descend">状态 (启用优先)</Menu.Item>
|
|
383
|
-
<Menu.Item key="active-ascend">状态 (禁用优先)</Menu.Item>
|
|
384
|
-
</Menu>
|
|
385
|
-
);
|
|
408
|
+
// 获取分页后的功能列表
|
|
409
|
+
const getPaginatedFeatures = () => {
|
|
410
|
+
const sortedFeatures = getSortedFeatures();
|
|
411
|
+
const startIndex = (listConfig.current - 1) * listConfig.pageSize;
|
|
412
|
+
const endIndex = startIndex + listConfig.pageSize;
|
|
413
|
+
return sortedFeatures.slice(startIndex, endIndex);
|
|
414
|
+
};
|
|
386
415
|
|
|
416
|
+
// 渲染功能模块卡片
|
|
387
417
|
const renderFeatureCard = (feature) => {
|
|
388
|
-
const
|
|
418
|
+
const formattedDate = feature.createdAt
|
|
419
|
+
? new Date(feature.createdAt).toLocaleDateString()
|
|
420
|
+
: '未知日期';
|
|
421
|
+
|
|
422
|
+
// 处理卡片点击事件
|
|
423
|
+
const handleCardClick = (e) => {
|
|
424
|
+
// 检查点击目标是否是开关或其父元素
|
|
425
|
+
const target = e.target;
|
|
426
|
+
const switchElement = target.closest('.ant-switch') || target.closest('.feature-name .ant-switch');
|
|
427
|
+
|
|
428
|
+
// 如果点击的是开关区域,则不执行跳转
|
|
429
|
+
if (switchElement) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 检查是否点击了操作按钮区域
|
|
434
|
+
const actionElement = target.closest('.ant-card-actions') || target.closest('.ant-card-actions li');
|
|
435
|
+
if (actionElement) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 执行跳转到接口管理界面
|
|
440
|
+
viewInterfaces(feature);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// 处理开关点击事件,阻止事件冒泡
|
|
444
|
+
const handleSwitchClick = (checked, e) => {
|
|
445
|
+
e.stopPropagation(); // 阻止事件冒泡到卡片
|
|
446
|
+
handleToggleActive(feature.id, feature.active);
|
|
447
|
+
};
|
|
389
448
|
|
|
390
449
|
return (
|
|
391
|
-
<Col xs={24} sm={12} md={8} lg={6} key={feature.id}>
|
|
450
|
+
<Col xs={24} sm={12} md={8} lg={6} key={feature.id} style={{ marginBottom: 16 }}>
|
|
392
451
|
<Badge.Ribbon
|
|
393
|
-
text={
|
|
394
|
-
color={
|
|
452
|
+
text={feature.active ? '已启用' : '已禁用'}
|
|
453
|
+
color={feature.active ? '#52c41a' : '#f5222d'}
|
|
454
|
+
style={{ display: 'block' }}
|
|
395
455
|
>
|
|
396
456
|
<Card
|
|
397
|
-
className={`feature-card ${!
|
|
398
|
-
|
|
457
|
+
className={`feature-card ${!feature.active ? 'inactive-feature' : ''}`}
|
|
458
|
+
onClick={handleCardClick}
|
|
459
|
+
style={{ cursor: 'pointer' }}
|
|
399
460
|
actions={[
|
|
461
|
+
<Tooltip title="管理接口">
|
|
462
|
+
<ApiOutlined key="interfaces" onClick={(e) => {
|
|
463
|
+
e.stopPropagation();
|
|
464
|
+
viewInterfaces(feature);
|
|
465
|
+
}} />
|
|
466
|
+
</Tooltip>,
|
|
400
467
|
<Tooltip title="编辑功能">
|
|
401
|
-
<EditOutlined key="edit" onClick={() =>
|
|
468
|
+
<EditOutlined key="edit" onClick={(e) => {
|
|
469
|
+
e.stopPropagation();
|
|
470
|
+
openModal(feature);
|
|
471
|
+
}} />
|
|
402
472
|
</Tooltip>,
|
|
403
|
-
<Tooltip title="
|
|
404
|
-
<
|
|
473
|
+
<Tooltip title="删除功能">
|
|
474
|
+
<DeleteOutlined key="delete" onClick={(e) => {
|
|
475
|
+
e.stopPropagation();
|
|
476
|
+
deleteFeature(feature.id);
|
|
477
|
+
}} />
|
|
405
478
|
</Tooltip>,
|
|
406
|
-
<
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
>
|
|
413
|
-
<Tooltip title="删除功能">
|
|
414
|
-
<DeleteOutlined key="delete" style={{ color: '#ff4d4f' }} />
|
|
415
|
-
</Tooltip>
|
|
416
|
-
</Popconfirm>,
|
|
479
|
+
<Tooltip title="导出配置">
|
|
480
|
+
<ExportOutlined key="export" onClick={(e) => {
|
|
481
|
+
e.stopPropagation();
|
|
482
|
+
exportFeatureConfig(feature);
|
|
483
|
+
}} />
|
|
484
|
+
</Tooltip>
|
|
417
485
|
]}
|
|
418
486
|
>
|
|
419
|
-
<div className="feature-card-
|
|
487
|
+
<div className="feature-card-content">
|
|
420
488
|
<div className="feature-name">
|
|
421
|
-
{feature.name}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
</div>
|
|
432
|
-
<div className="feature-stats">
|
|
433
|
-
<div className="stat-item">
|
|
434
|
-
<span className="stat-label">接口数量:</span>
|
|
435
|
-
<span className="stat-value">{feature.interfaceCount || 0}</span>
|
|
489
|
+
<Title level={4} ellipsis={{ tooltip: feature.name }}>
|
|
490
|
+
{feature.name}
|
|
491
|
+
</Title>
|
|
492
|
+
|
|
493
|
+
<Switch
|
|
494
|
+
checked={feature.active}
|
|
495
|
+
onChange={handleSwitchClick}
|
|
496
|
+
size="small"
|
|
497
|
+
onClick={() => {event.stopPropagation()}}
|
|
498
|
+
/>
|
|
436
499
|
</div>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
500
|
+
|
|
501
|
+
<Paragraph className="feature-description" ellipsis={{ rows: 2, expandable: false, tooltip: feature.description }}>
|
|
502
|
+
{feature.description || '无描述'}
|
|
503
|
+
</Paragraph>
|
|
504
|
+
|
|
505
|
+
<div className="feature-stat">
|
|
506
|
+
<span>
|
|
507
|
+
<InfoCircleOutlined /> {feature.interfaceCount || 0} 个接口
|
|
508
|
+
</span>
|
|
509
|
+
<span className="feature-date">
|
|
510
|
+
<CalendarOutlined /> {formattedDate}
|
|
441
511
|
</span>
|
|
442
512
|
</div>
|
|
443
513
|
</div>
|
|
@@ -462,7 +532,7 @@ const MockData = () => {
|
|
|
462
532
|
fontSize: '12px',
|
|
463
533
|
zIndex: 1000
|
|
464
534
|
}}>
|
|
465
|
-
|
|
535
|
+
V2版本
|
|
466
536
|
</div>
|
|
467
537
|
|
|
468
538
|
<div className="page-title-bar">
|
|
@@ -493,208 +563,162 @@ const MockData = () => {
|
|
|
493
563
|
|
|
494
564
|
{/* 排序和分页控制栏 */}
|
|
495
565
|
{mockFeatures.length > 0 && !loading && (
|
|
496
|
-
<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
<Button
|
|
532
|
-
icon={<ReloadOutlined />}
|
|
533
|
-
onClick={fetchFeatures}
|
|
534
|
-
loading={loading}
|
|
535
|
-
>
|
|
536
|
-
刷新
|
|
537
|
-
</Button>
|
|
538
|
-
{(searchText || filterStatus !== 'all') && (
|
|
539
|
-
<Button
|
|
540
|
-
icon={<ClearOutlined />}
|
|
541
|
-
onClick={() => {
|
|
542
|
-
setSearchText('');
|
|
543
|
-
setFilterStatus('all');
|
|
544
|
-
}}
|
|
545
|
-
>
|
|
546
|
-
清除筛选
|
|
547
|
-
</Button>
|
|
548
|
-
)}
|
|
549
|
-
</Space>
|
|
550
|
-
</div>
|
|
566
|
+
<div style={{
|
|
567
|
+
marginBottom: 16,
|
|
568
|
+
padding: '12px 16px',
|
|
569
|
+
background: '#fafafa',
|
|
570
|
+
borderRadius: '6px',
|
|
571
|
+
display: 'flex',
|
|
572
|
+
justifyContent: 'space-between',
|
|
573
|
+
alignItems: 'center',
|
|
574
|
+
flexWrap: 'wrap',
|
|
575
|
+
gap: '8px'
|
|
576
|
+
}}>
|
|
577
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
|
|
578
|
+
<span style={{ fontSize: '14px', color: '#666' }}>
|
|
579
|
+
<SortAscendingOutlined /> 排序:
|
|
580
|
+
</span>
|
|
581
|
+
<Select
|
|
582
|
+
value={listConfig.sortBy}
|
|
583
|
+
onChange={handleSortChange}
|
|
584
|
+
style={{ width: 120 }}
|
|
585
|
+
size="small"
|
|
586
|
+
>
|
|
587
|
+
<Select.Option value="name">名称</Select.Option>
|
|
588
|
+
<Select.Option value="createdAt">创建时间</Select.Option>
|
|
589
|
+
<Select.Option value="interfaceCount">接口数量</Select.Option>
|
|
590
|
+
<Select.Option value="active">状态</Select.Option>
|
|
591
|
+
</Select>
|
|
592
|
+
<Select
|
|
593
|
+
value={listConfig.sortOrder}
|
|
594
|
+
onChange={handleSortOrderChange}
|
|
595
|
+
style={{ width: 80 }}
|
|
596
|
+
size="small"
|
|
597
|
+
>
|
|
598
|
+
<Select.Option value="ascend">升序</Select.Option>
|
|
599
|
+
<Select.Option value="descend">降序</Select.Option>
|
|
600
|
+
</Select>
|
|
551
601
|
</div>
|
|
552
602
|
|
|
553
|
-
<div
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
<
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
title="禁用的功能数量"
|
|
570
|
-
/>
|
|
571
|
-
</Space>
|
|
603
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
604
|
+
<span style={{ fontSize: '14px', color: '#666' }}>每页显示:</span>
|
|
605
|
+
<Select
|
|
606
|
+
value={listConfig.pageSize}
|
|
607
|
+
onChange={handlePageSizeChange}
|
|
608
|
+
style={{ width: 80 }}
|
|
609
|
+
size="small"
|
|
610
|
+
>
|
|
611
|
+
<Select.Option value={8}>8</Select.Option>
|
|
612
|
+
<Select.Option value={12}>12</Select.Option>
|
|
613
|
+
<Select.Option value={16}>16</Select.Option>
|
|
614
|
+
<Select.Option value={24}>24</Select.Option>
|
|
615
|
+
</Select>
|
|
616
|
+
<span style={{ fontSize: '14px', color: '#999' }}>
|
|
617
|
+
共 {mockFeatures.length} 个功能模块
|
|
618
|
+
</span>
|
|
572
619
|
</div>
|
|
573
|
-
</
|
|
620
|
+
</div>
|
|
574
621
|
)}
|
|
575
|
-
|
|
576
|
-
{/* 功能模块列表 */}
|
|
622
|
+
|
|
577
623
|
<div className="feature-list-container">
|
|
578
624
|
{loading ? (
|
|
579
|
-
<div className="loading
|
|
580
|
-
<
|
|
581
|
-
|
|
582
|
-
<Col xs={24} sm={12} md={8} lg={6} key={i}>
|
|
583
|
-
<Card loading style={{ height: 200 }} />
|
|
584
|
-
</Col>
|
|
585
|
-
))}
|
|
586
|
-
</Row>
|
|
625
|
+
<div className="loading">
|
|
626
|
+
<div className="loading-spinner"></div>
|
|
627
|
+
<div>正在加载功能模块...</div>
|
|
587
628
|
</div>
|
|
588
|
-
) : mockFeatures.length
|
|
589
|
-
<Alert
|
|
590
|
-
message="暂无功能模块"
|
|
591
|
-
description="点击 '新建功能' 按钮创建第一个功能模块"
|
|
592
|
-
type="info"
|
|
593
|
-
showIcon
|
|
594
|
-
style={{ textAlign: 'center', padding: '60px 20px' }}
|
|
595
|
-
/>
|
|
596
|
-
) : filteredData.length === 0 ? (
|
|
597
|
-
<Alert
|
|
598
|
-
message="无匹配结果"
|
|
599
|
-
description="请调整搜索条件或筛选设置"
|
|
600
|
-
type="warning"
|
|
601
|
-
showIcon
|
|
602
|
-
style={{ textAlign: 'center', padding: '60px 20px' }}
|
|
603
|
-
/>
|
|
604
|
-
) : (
|
|
629
|
+
) : mockFeatures.length > 0 ? (
|
|
605
630
|
<>
|
|
606
|
-
<Row gutter={[16, 16]}
|
|
607
|
-
{
|
|
631
|
+
<Row gutter={[16, 16]}>
|
|
632
|
+
{getPaginatedFeatures().map(feature => renderFeatureCard(feature))}
|
|
608
633
|
</Row>
|
|
609
634
|
|
|
610
|
-
{/*
|
|
611
|
-
{
|
|
612
|
-
<div
|
|
635
|
+
{/* 分页组件 */}
|
|
636
|
+
{mockFeatures.length > listConfig.pageSize && (
|
|
637
|
+
<div style={{
|
|
638
|
+
marginTop: 24,
|
|
639
|
+
textAlign: 'center',
|
|
640
|
+
padding: '16px',
|
|
641
|
+
borderTop: '1px solid #f0f0f0'
|
|
642
|
+
}}>
|
|
613
643
|
<Pagination
|
|
614
644
|
current={listConfig.current}
|
|
615
645
|
pageSize={listConfig.pageSize}
|
|
616
|
-
total={
|
|
617
|
-
|
|
618
|
-
|
|
646
|
+
total={mockFeatures.length}
|
|
647
|
+
onChange={handlePageChange}
|
|
648
|
+
onShowSizeChange={handlePageChange}
|
|
649
|
+
showSizeChanger={true}
|
|
650
|
+
showQuickJumper={true}
|
|
619
651
|
showTotal={(total, range) =>
|
|
620
|
-
`第 ${range[0]}-${range[1]} 条,共 ${total}
|
|
652
|
+
`第 ${range[0]}-${range[1]} 条,共 ${total} 个功能模块`
|
|
621
653
|
}
|
|
622
654
|
pageSizeOptions={['8', '12', '16', '24']}
|
|
623
|
-
|
|
624
|
-
saveListConfig({ current, pageSize });
|
|
625
|
-
}}
|
|
655
|
+
size="default"
|
|
626
656
|
/>
|
|
627
657
|
</div>
|
|
628
658
|
)}
|
|
629
659
|
</>
|
|
660
|
+
) : (
|
|
661
|
+
<div className="empty-data">
|
|
662
|
+
<div className="empty-icon">📂</div>
|
|
663
|
+
<div className="empty-text">暂无功能,请点击"新建功能"按钮创建</div>
|
|
664
|
+
<div className="empty-actions">
|
|
665
|
+
<button className="create-button" onClick={() => openModal()}>
|
|
666
|
+
创建新功能
|
|
667
|
+
</button>
|
|
668
|
+
<button className="import-button-large" onClick={importFeatureConfig}>
|
|
669
|
+
导入已有功能
|
|
670
|
+
</button>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
630
673
|
)}
|
|
631
674
|
</div>
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
675
|
+
</div>
|
|
676
|
+
|
|
677
|
+
<Modal
|
|
678
|
+
title={currentFeature ? '编辑功能' : '新建功能'}
|
|
679
|
+
open={showModal}
|
|
680
|
+
onCancel={closeModal}
|
|
681
|
+
footer={null}
|
|
682
|
+
destroyOnClose
|
|
683
|
+
width={500}
|
|
684
|
+
>
|
|
685
|
+
<Form
|
|
686
|
+
form={form}
|
|
687
|
+
layout="vertical"
|
|
688
|
+
onFinish={handleSubmit}
|
|
689
|
+
initialValues={{
|
|
690
|
+
name: '',
|
|
691
|
+
description: '',
|
|
692
|
+
active: true
|
|
693
|
+
}}
|
|
640
694
|
>
|
|
641
|
-
<Form
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
initialValues={{
|
|
646
|
-
name: '',
|
|
647
|
-
description: '',
|
|
648
|
-
active: true
|
|
649
|
-
}}
|
|
695
|
+
<Form.Item
|
|
696
|
+
name="name"
|
|
697
|
+
label="功能名称"
|
|
698
|
+
rules={[{ required: true, message: '请输入功能名称' }]}
|
|
650
699
|
>
|
|
651
|
-
<
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
maxLength={200}
|
|
674
|
-
/>
|
|
675
|
-
</Form.Item>
|
|
676
|
-
|
|
677
|
-
<Form.Item
|
|
678
|
-
name="active"
|
|
679
|
-
label="启用状态"
|
|
680
|
-
valuePropName="checked"
|
|
681
|
-
>
|
|
682
|
-
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
|
683
|
-
</Form.Item>
|
|
684
|
-
|
|
685
|
-
<Form.Item className="form-actions">
|
|
686
|
-
<Space>
|
|
687
|
-
<Button onClick={() => setShowModal(false)}>
|
|
688
|
-
取消
|
|
689
|
-
</Button>
|
|
690
|
-
<Button type="primary" htmlType="submit">
|
|
691
|
-
{currentFeature ? '保存' : '创建'}
|
|
692
|
-
</Button>
|
|
693
|
-
</Space>
|
|
694
|
-
</Form.Item>
|
|
695
|
-
</Form>
|
|
696
|
-
</Modal>
|
|
697
|
-
</div>
|
|
700
|
+
<Input placeholder="请输入功能名称" />
|
|
701
|
+
</Form.Item>
|
|
702
|
+
|
|
703
|
+
<Form.Item
|
|
704
|
+
name="description"
|
|
705
|
+
label="功能描述"
|
|
706
|
+
>
|
|
707
|
+
<TextArea
|
|
708
|
+
placeholder="请输入功能描述(可选)"
|
|
709
|
+
autoSize={{ minRows: 3, maxRows: 6 }}
|
|
710
|
+
/>
|
|
711
|
+
</Form.Item>
|
|
712
|
+
<div className="form-actions">
|
|
713
|
+
<Button onClick={closeModal}>
|
|
714
|
+
取消
|
|
715
|
+
</Button>
|
|
716
|
+
<Button type="primary" htmlType="submit" loading={loading}>
|
|
717
|
+
确定
|
|
718
|
+
</Button>
|
|
719
|
+
</div>
|
|
720
|
+
</Form>
|
|
721
|
+
</Modal>
|
|
698
722
|
</AppLayout>
|
|
699
723
|
);
|
|
700
724
|
};
|