whistle.mock-plugins 1.1.1-beta.7 → 1.1.1-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/VERSION.md +8 -2
- package/app/public/js/index.js +1 -1
- package/lib/routes/interfaces.js +169 -0
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +389 -298
- package/ui/src/styles/interface-management.css +44 -0
|
@@ -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,9 +32,11 @@ 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 },
|
|
39
|
+
{ key: 'responseDelay', title: '延迟(毫秒)', required: false },
|
|
38
40
|
{ key: 'httpStatus', title: '状态码', required: false },
|
|
39
41
|
{ key: 'contentType', title: '内容类型', required: false },
|
|
40
42
|
{ key: 'targetUrl', title: '目标URL', required: false },
|
|
@@ -56,6 +58,11 @@ const InterfaceManagement = () => {
|
|
|
56
58
|
const [interfacesLoading, setInterfacesLoading] = useState(false);
|
|
57
59
|
const [selectedFeatureId, setSelectedFeatureId] = useState(null);
|
|
58
60
|
|
|
61
|
+
// 分组状态
|
|
62
|
+
const [groups, setGroups] = useState([]);
|
|
63
|
+
const [selectedGroup, setSelectedGroup] = useState(null);
|
|
64
|
+
const [groupActionLoading, setGroupActionLoading] = useState(false);
|
|
65
|
+
|
|
59
66
|
// 模态框状态
|
|
60
67
|
const [modalVisible, setModalVisible] = useState(false);
|
|
61
68
|
const [previewVisible, setPreviewVisible] = useState(false);
|
|
@@ -165,6 +172,9 @@ const InterfaceManagement = () => {
|
|
|
165
172
|
|
|
166
173
|
setInterfaces(processedInterfaces);
|
|
167
174
|
console.log('处理后的接口列表:', processedInterfaces);
|
|
175
|
+
|
|
176
|
+
// 提取并更新分组列表
|
|
177
|
+
updateGroups(processedInterfaces);
|
|
168
178
|
} else {
|
|
169
179
|
console.warn('接口数据格式不正确:', response.data);
|
|
170
180
|
setInterfaces([]);
|
|
@@ -178,6 +188,41 @@ const InterfaceManagement = () => {
|
|
|
178
188
|
setInterfacesLoading(false);
|
|
179
189
|
}
|
|
180
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
|
+
};
|
|
181
226
|
|
|
182
227
|
const handleAddInterface = () => {
|
|
183
228
|
if (!selectedFeatureId) {
|
|
@@ -214,77 +259,63 @@ const InterfaceManagement = () => {
|
|
|
214
259
|
};
|
|
215
260
|
|
|
216
261
|
const handleEditInterface = (record) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// 将自定义请求头转换为数组格式,用于动态表单项
|
|
221
|
-
let headersArray = [];
|
|
222
|
-
if (record.customHeaders && typeof record.customHeaders === 'object') {
|
|
223
|
-
headersArray = Object.entries(record.customHeaders).map(([key, value]) => ({
|
|
224
|
-
headerName: key,
|
|
225
|
-
headerValue: value
|
|
226
|
-
}));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 处理参数匹配规则,转换为数组格式
|
|
230
|
-
let paramMatchersArray = [];
|
|
231
|
-
if (record.paramMatchers && Array.isArray(record.paramMatchers)) {
|
|
232
|
-
paramMatchersArray = record.paramMatchers.map(matcher => ({
|
|
233
|
-
paramPath: matcher.paramPath || '',
|
|
234
|
-
paramValue: matcher.paramValue || '',
|
|
235
|
-
matchType: matcher.matchType || 'exact'
|
|
236
|
-
}));
|
|
262
|
+
if (!record) {
|
|
263
|
+
message.warning('接口数据不完整,无法编辑');
|
|
264
|
+
return;
|
|
237
265
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
|
|
267
|
+
console.log('编辑接口:', record);
|
|
268
|
+
|
|
269
|
+
// 重置表单
|
|
270
|
+
form.resetFields();
|
|
271
|
+
|
|
272
|
+
// 处理响应数据
|
|
273
|
+
let responses = [];
|
|
274
|
+
let activeResponseId = '';
|
|
275
|
+
|
|
244
276
|
if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
initialResponses = JSON.parse(JSON.stringify(record.responses));
|
|
248
|
-
initialResponseId = record.activeResponseId || record.responses[0].id;
|
|
249
|
-
|
|
250
|
-
// 确保每个响应都有名称
|
|
251
|
-
initialResponses = initialResponses.map((resp, index) => ({
|
|
252
|
-
id: resp.id || generateResponseId(),
|
|
253
|
-
name: resp.name || `响应 ${index + 1}`,
|
|
254
|
-
description: resp.description || '',
|
|
255
|
-
content: resp.content || '{}'
|
|
256
|
-
}));
|
|
277
|
+
responses = record.responses;
|
|
278
|
+
activeResponseId = record.activeResponseId || responses[0].id;
|
|
257
279
|
} else if (record.responseContent) {
|
|
258
|
-
//
|
|
259
|
-
console.log('创建默认响应,使用responseContent字段');
|
|
280
|
+
// 兼容旧数据格式,创建默认响应
|
|
260
281
|
const defaultResponseId = generateResponseId();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
];
|
|
269
|
-
initialResponseId = defaultResponseId;
|
|
282
|
+
responses = [{
|
|
283
|
+
id: defaultResponseId,
|
|
284
|
+
name: '默认响应',
|
|
285
|
+
description: '',
|
|
286
|
+
content: record.responseContent
|
|
287
|
+
}];
|
|
288
|
+
activeResponseId = defaultResponseId;
|
|
270
289
|
}
|
|
271
290
|
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
// 处理自定义请求头
|
|
292
|
+
let headerItems = [];
|
|
293
|
+
if (record.headers && typeof record.headers === 'object') {
|
|
294
|
+
headerItems = Object.entries(record.headers).map(([headerName, headerValue]) => ({
|
|
295
|
+
headerName,
|
|
296
|
+
headerValue
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
274
299
|
|
|
300
|
+
// 设置表单值
|
|
275
301
|
form.setFieldsValue({
|
|
276
302
|
name: record.name,
|
|
303
|
+
group: record.group || '',
|
|
277
304
|
pattern: record.urlPattern,
|
|
278
305
|
proxyType: record.proxyType || 'response',
|
|
279
|
-
statusCode: record.httpStatus
|
|
306
|
+
statusCode: (record.httpStatus || record.statusCode || 200).toString(),
|
|
280
307
|
contentType: record.contentType || 'application/json; charset=utf-8',
|
|
281
|
-
responses
|
|
282
|
-
activeResponseId
|
|
283
|
-
httpMethod: record.httpMethod || 'ALL',
|
|
308
|
+
responses,
|
|
309
|
+
activeResponseId,
|
|
310
|
+
httpMethod: record.httpMethod || record.method || 'ALL',
|
|
284
311
|
targetUrl: record.targetUrl || '',
|
|
285
|
-
headerItems
|
|
286
|
-
paramMatchers:
|
|
312
|
+
headerItems,
|
|
313
|
+
paramMatchers: record.paramMatchers || [],
|
|
314
|
+
responseDelay: record.responseDelay ? record.responseDelay.toString() : '0'
|
|
287
315
|
});
|
|
316
|
+
|
|
317
|
+
setCurrentResponseId(activeResponseId);
|
|
318
|
+
setEditingInterface(record);
|
|
288
319
|
setModalVisible(true);
|
|
289
320
|
};
|
|
290
321
|
|
|
@@ -308,12 +339,15 @@ const InterfaceManagement = () => {
|
|
|
308
339
|
|
|
309
340
|
const handleToggleActive = async (id, currentActive) => {
|
|
310
341
|
try {
|
|
311
|
-
const response = await axios.patch(`/cgi-bin/interfaces
|
|
342
|
+
const response = await axios.patch(`/cgi-bin/interfaces/${id}`, {
|
|
312
343
|
active: !currentActive
|
|
313
344
|
});
|
|
314
345
|
if (response.data && response.data.code === 0) {
|
|
315
346
|
message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
|
|
316
347
|
fetchInterfaces();
|
|
348
|
+
|
|
349
|
+
// 刷新规则缓存
|
|
350
|
+
refreshCacheAfterUpdate();
|
|
317
351
|
} else {
|
|
318
352
|
throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
|
|
319
353
|
}
|
|
@@ -325,174 +359,81 @@ const InterfaceManagement = () => {
|
|
|
325
359
|
|
|
326
360
|
const handleSubmit = async () => {
|
|
327
361
|
try {
|
|
362
|
+
// 表单验证
|
|
328
363
|
const values = await form.validateFields();
|
|
329
|
-
console.log('
|
|
330
|
-
console.log('表单提交,原始值:', JSON.stringify(values, null, 2));
|
|
331
|
-
|
|
332
|
-
// 获取当前激活的响应ID和所有响应
|
|
333
|
-
const activeResponseId = values.activeResponseId;
|
|
334
|
-
let responses = values.responses || [];
|
|
335
|
-
|
|
336
|
-
console.log('提取的原始响应数据:', responses);
|
|
337
|
-
console.log('响应数据类型:', typeof responses);
|
|
338
|
-
console.log('是否为数组:', Array.isArray(responses));
|
|
364
|
+
console.log('表单提交数据:', values);
|
|
339
365
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
if (!Array.isArray(responses)) {
|
|
347
|
-
responses = [];
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
responses = [];
|
|
366
|
+
// 处理自定义请求头
|
|
367
|
+
let headers = {};
|
|
368
|
+
if (values.headerItems && Array.isArray(values.headerItems)) {
|
|
369
|
+
values.headerItems.forEach(item => {
|
|
370
|
+
if (item && item.headerName) {
|
|
371
|
+
headers[item.headerName] = item.headerValue;
|
|
351
372
|
}
|
|
352
|
-
} catch (e) {
|
|
353
|
-
console.error('解析响应数据失败:', e);
|
|
354
|
-
responses = [];
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
console.log('处理前的响应数据:', JSON.stringify(responses, null, 2));
|
|
359
|
-
|
|
360
|
-
// 确保每个响应都有名称、描述和内容
|
|
361
|
-
const cleanedResponses = responses.map((resp, index) => {
|
|
362
|
-
console.log(`处理响应 ${index}:`, resp);
|
|
363
|
-
const cleaned = {
|
|
364
|
-
id: resp.id || generateResponseId(),
|
|
365
|
-
name: resp.name || `响应 ${index + 1}`,
|
|
366
|
-
description: resp.description || '',
|
|
367
|
-
content: resp.content || '{}'
|
|
368
|
-
};
|
|
369
|
-
console.log(`处理后的响应 ${index}:`, cleaned);
|
|
370
|
-
return cleaned;
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// 如果没有响应,创建一个默认响应
|
|
374
|
-
if (cleanedResponses.length === 0) {
|
|
375
|
-
console.log('没有响应数据,创建默认响应');
|
|
376
|
-
const defaultId = generateResponseId();
|
|
377
|
-
cleanedResponses.push({
|
|
378
|
-
id: defaultId,
|
|
379
|
-
name: '默认响应',
|
|
380
|
-
description: '',
|
|
381
|
-
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
|
|
382
373
|
});
|
|
383
|
-
setCurrentResponseId(defaultId);
|
|
384
374
|
}
|
|
385
375
|
|
|
386
|
-
//
|
|
387
|
-
const validActiveResponseId = activeResponseId && cleanedResponses.some(r => r.id === activeResponseId)
|
|
388
|
-
? activeResponseId
|
|
389
|
-
: cleanedResponses[0].id;
|
|
390
|
-
|
|
391
|
-
// 获取激活的响应
|
|
392
|
-
const activeResponse = cleanedResponses.find(r => r.id === validActiveResponseId) || cleanedResponses[0];
|
|
393
|
-
|
|
394
|
-
console.log('=== 清理后的响应数据 ===');
|
|
395
|
-
console.log('清理后的响应数据:', JSON.stringify(cleanedResponses, null, 2));
|
|
396
|
-
console.log('激活的响应ID:', validActiveResponseId);
|
|
397
|
-
console.log('激活的响应:', activeResponse);
|
|
398
|
-
|
|
399
|
-
// 检查responseBody是否是有效的JSON
|
|
400
|
-
if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
|
|
401
|
-
cleanedResponses.forEach(response => {
|
|
402
|
-
try {
|
|
403
|
-
if (response.content) {
|
|
404
|
-
JSON.parse(response.content);
|
|
405
|
-
}
|
|
406
|
-
} catch (e) {
|
|
407
|
-
message.error(`响应 "${response.name}" 不是有效的JSON格式`);
|
|
408
|
-
throw new Error(`响应 "${response.name}" 不是有效的JSON格式`);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// 处理自定义请求头,将表单项数组转换为对象格式
|
|
414
|
-
let customHeaders = {};
|
|
415
|
-
|
|
416
|
-
if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
|
|
417
|
-
if (values.headerItems && values.headerItems.length > 0) {
|
|
418
|
-
values.headerItems.forEach(item => {
|
|
419
|
-
if (item && item.headerName && item.headerName.trim()) {
|
|
420
|
-
customHeaders[item.headerName.trim()] = item.headerValue || '';
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// 处理参数匹配规则,清理无效的规则
|
|
427
|
-
let paramMatchers = [];
|
|
428
|
-
if (values.proxyType === 'response' && values.paramMatchers && values.paramMatchers.length > 0) {
|
|
429
|
-
paramMatchers = values.paramMatchers
|
|
430
|
-
.filter(item => item && item.paramPath && item.paramPath.trim() && item.paramValue !== undefined)
|
|
431
|
-
.map(item => ({
|
|
432
|
-
paramPath: item.paramPath.trim(),
|
|
433
|
-
paramValue: item.paramValue,
|
|
434
|
-
matchType: item.matchType || 'exact'
|
|
435
|
-
}));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
console.log('=== 参数匹配规则处理 ===');
|
|
439
|
-
console.log('原始参数匹配规则:', values.paramMatchers);
|
|
440
|
-
console.log('清理后的参数匹配规则:', paramMatchers);
|
|
441
|
-
|
|
376
|
+
// 构建接口数据
|
|
442
377
|
const interfaceData = {
|
|
443
378
|
name: values.name,
|
|
444
|
-
|
|
379
|
+
group: values.group,
|
|
445
380
|
urlPattern: values.pattern,
|
|
446
381
|
proxyType: values.proxyType,
|
|
447
|
-
|
|
448
|
-
responses:
|
|
449
|
-
activeResponseId:
|
|
450
|
-
// 兼容性保留:将当前活跃响应的内容保存到 responseContent
|
|
451
|
-
responseContent: activeResponse ? activeResponse.content : '',
|
|
452
|
-
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
453
|
-
customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
|
|
454
|
-
paramMatchers: paramMatchers, // 添加参数匹配规则
|
|
382
|
+
featureId: selectedFeatureId,
|
|
383
|
+
responses: values.responses,
|
|
384
|
+
activeResponseId: values.activeResponseId,
|
|
455
385
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
456
386
|
contentType: values.contentType,
|
|
457
|
-
responseDelay: 0,
|
|
387
|
+
responseDelay: parseInt(values.responseDelay, 10) || 0,
|
|
458
388
|
httpMethod: values.httpMethod,
|
|
459
389
|
active: true
|
|
460
390
|
};
|
|
461
391
|
|
|
462
|
-
|
|
463
|
-
|
|
392
|
+
// 根据代理类型添加不同字段
|
|
393
|
+
if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
|
|
394
|
+
interfaceData.targetUrl = values.targetUrl;
|
|
395
|
+
interfaceData.headers = headers;
|
|
396
|
+
}
|
|
464
397
|
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// 创建新接口
|
|
472
|
-
console.log('执行接口创建');
|
|
473
|
-
response = await axios.post('/cgi-bin/interfaces', interfaceData);
|
|
398
|
+
// 添加参数匹配规则
|
|
399
|
+
if (values.paramMatchers && Array.isArray(values.paramMatchers) && values.paramMatchers.length > 0) {
|
|
400
|
+
// 过滤掉空的匹配规则
|
|
401
|
+
interfaceData.paramMatchers = values.paramMatchers.filter(matcher =>
|
|
402
|
+
matcher && matcher.paramPath && matcher.paramValue
|
|
403
|
+
);
|
|
474
404
|
}
|
|
475
|
-
|
|
476
|
-
console.log('
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
405
|
+
|
|
406
|
+
console.log('提交接口数据:', interfaceData);
|
|
407
|
+
|
|
408
|
+
if (editingInterface) {
|
|
409
|
+
// 更新接口
|
|
410
|
+
const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
|
|
411
|
+
if (response.data && response.data.code === 0) {
|
|
412
|
+
message.success('接口更新成功');
|
|
413
|
+
setModalVisible(false);
|
|
414
|
+
fetchInterfaces();
|
|
415
|
+
|
|
416
|
+
// 刷新规则缓存
|
|
417
|
+
refreshCacheAfterUpdate();
|
|
418
|
+
} else {
|
|
419
|
+
throw new Error(response.data?.message || '接口更新失败');
|
|
420
|
+
}
|
|
486
421
|
} else {
|
|
487
|
-
|
|
422
|
+
// 创建接口
|
|
423
|
+
const response = await axios.post('/cgi-bin/interfaces', interfaceData);
|
|
424
|
+
if (response.data && response.data.code === 0) {
|
|
425
|
+
message.success('接口创建成功');
|
|
426
|
+
setModalVisible(false);
|
|
427
|
+
fetchInterfaces();
|
|
428
|
+
|
|
429
|
+
// 刷新规则缓存
|
|
430
|
+
refreshCacheAfterUpdate();
|
|
431
|
+
} else {
|
|
432
|
+
throw new Error(response.data?.message || '接口创建失败');
|
|
433
|
+
}
|
|
488
434
|
}
|
|
489
435
|
} catch (error) {
|
|
490
|
-
|
|
491
|
-
// 已经显示了错误信息,不需要再显示
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
console.error('=== 接口操作失败 ===');
|
|
495
|
-
console.error('操作失败:', error);
|
|
436
|
+
console.error('表单提交失败:', error);
|
|
496
437
|
message.error(error.response?.data?.message || error.message || '操作失败');
|
|
497
438
|
}
|
|
498
439
|
};
|
|
@@ -568,9 +509,7 @@ const InterfaceManagement = () => {
|
|
|
568
509
|
}
|
|
569
510
|
};
|
|
570
511
|
|
|
571
|
-
const filteredInterfaces =
|
|
572
|
-
!selectedFeatureId || item.featureId === selectedFeatureId
|
|
573
|
-
);
|
|
512
|
+
const filteredInterfaces = getFilteredInterfaces();
|
|
574
513
|
|
|
575
514
|
// 处理表格变化(排序、分页)
|
|
576
515
|
const handleTableChange = (pagination, filters, sorter) => {
|
|
@@ -620,15 +559,11 @@ const InterfaceManagement = () => {
|
|
|
620
559
|
dataIndex: 'active',
|
|
621
560
|
key: 'active',
|
|
622
561
|
width: 80,
|
|
623
|
-
sorter: (a, b) => {
|
|
624
|
-
// true排在前面,false排在后面
|
|
625
|
-
return Number(b.active) - Number(a.active);
|
|
626
|
-
},
|
|
627
|
-
sortDirections: ['descend', 'ascend'],
|
|
628
562
|
render: (active, record) => (
|
|
629
563
|
<Switch
|
|
630
|
-
checked={active}
|
|
564
|
+
checked={active !== false}
|
|
631
565
|
onChange={() => handleToggleActive(record.id, active)}
|
|
566
|
+
size="small"
|
|
632
567
|
/>
|
|
633
568
|
),
|
|
634
569
|
},
|
|
@@ -636,113 +571,115 @@ const InterfaceManagement = () => {
|
|
|
636
571
|
title: '名称',
|
|
637
572
|
dataIndex: 'name',
|
|
638
573
|
key: 'name',
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
574
|
+
sorter: (a, b) => a.name.localeCompare(b.name),
|
|
575
|
+
render: (text) => <span className="interface-name">{text}</span>,
|
|
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
|
+
},
|
|
642
597
|
},
|
|
643
598
|
{
|
|
644
599
|
title: 'URL匹配规则',
|
|
645
600
|
dataIndex: 'urlPattern',
|
|
646
601
|
key: 'urlPattern',
|
|
647
602
|
ellipsis: true,
|
|
648
|
-
|
|
649
|
-
sortDirections: ['ascend', 'descend'],
|
|
603
|
+
render: (text) => <span className="url-pattern">{text}</span>,
|
|
650
604
|
},
|
|
651
605
|
{
|
|
652
606
|
title: '处理方式',
|
|
653
607
|
dataIndex: 'proxyType',
|
|
654
608
|
key: 'proxyType',
|
|
655
609
|
width: 120,
|
|
656
|
-
sorter: (a, b) => {
|
|
657
|
-
const aType = a.proxyType || 'response';
|
|
658
|
-
const bType = b.proxyType || 'response';
|
|
659
|
-
return aType.localeCompare(bType);
|
|
660
|
-
},
|
|
661
|
-
sortDirections: ['ascend', 'descend'],
|
|
662
610
|
render: (text) => {
|
|
663
|
-
const
|
|
664
|
-
return
|
|
665
|
-
|
|
611
|
+
const proxyType = proxyTypes.find(item => item.value === text);
|
|
612
|
+
return (
|
|
613
|
+
<Badge
|
|
614
|
+
color={proxyType?.color || '#999'}
|
|
615
|
+
text={proxyType?.label || text}
|
|
616
|
+
/>
|
|
617
|
+
);
|
|
618
|
+
},
|
|
666
619
|
},
|
|
667
620
|
{
|
|
668
621
|
title: '当前响应',
|
|
669
|
-
dataIndex: '
|
|
622
|
+
dataIndex: 'activeResponseId',
|
|
670
623
|
key: 'currentResponse',
|
|
671
|
-
width:
|
|
672
|
-
render: (
|
|
673
|
-
//
|
|
674
|
-
if (record.
|
|
675
|
-
return
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// 如果没有响应数据
|
|
679
|
-
if (!responses || !Array.isArray(responses) || responses.length === 0) {
|
|
680
|
-
return <span style={{ color: '#999' }}>无响应数据</span>;
|
|
624
|
+
width: 180,
|
|
625
|
+
render: (activeResponseId, record) => {
|
|
626
|
+
// 如果没有响应数据,返回空
|
|
627
|
+
if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
|
|
628
|
+
return <span className="no-response">无响应数据</span>;
|
|
681
629
|
}
|
|
682
630
|
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
return (
|
|
686
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
687
|
-
<span>{responses[0].name || '默认响应'}</span>
|
|
688
|
-
<Badge count="1" style={{ backgroundColor: '#52c41a' }} />
|
|
689
|
-
</div>
|
|
690
|
-
);
|
|
691
|
-
}
|
|
631
|
+
// 找到当前激活的响应
|
|
632
|
+
const activeResponse = record.responses.find(resp => resp.id === activeResponseId);
|
|
692
633
|
|
|
693
|
-
//
|
|
694
|
-
const
|
|
634
|
+
// 如果没有找到激活的响应,使用第一个
|
|
635
|
+
const currentResponse = activeResponse || record.responses[0];
|
|
695
636
|
|
|
696
637
|
return (
|
|
697
|
-
<
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
value={resp.id}
|
|
710
|
-
label={resp.name || '未命名'}
|
|
711
|
-
>
|
|
712
|
-
<Tooltip title={resp.description || resp.name}>
|
|
713
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
714
|
-
<span>{resp.name || '未命名'}</span>
|
|
715
|
-
{resp.id === (record.activeResponseId || activeResponse.id) && (
|
|
716
|
-
<Badge status="processing" />
|
|
717
|
-
)}
|
|
718
|
-
</div>
|
|
719
|
-
</Tooltip>
|
|
720
|
-
</Option>
|
|
721
|
-
))}
|
|
722
|
-
</Select>
|
|
723
|
-
<Badge
|
|
724
|
-
count={responses.length}
|
|
725
|
-
style={{ backgroundColor: '#1890ff' }}
|
|
726
|
-
title={`共${responses.length}个响应`}
|
|
727
|
-
/>
|
|
728
|
-
</div>
|
|
638
|
+
<Select
|
|
639
|
+
value={activeResponseId || record.responses[0]?.id}
|
|
640
|
+
style={{ width: '100%' }}
|
|
641
|
+
onChange={(value) => handleResponseSwitch(record.id, value)}
|
|
642
|
+
disabled={record.proxyType !== 'response'}
|
|
643
|
+
>
|
|
644
|
+
{record.responses.map(resp => (
|
|
645
|
+
<Option key={resp.id} value={resp.id}>
|
|
646
|
+
{resp.name}
|
|
647
|
+
</Option>
|
|
648
|
+
))}
|
|
649
|
+
</Select>
|
|
729
650
|
);
|
|
730
|
-
}
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
title: '延迟(毫秒)',
|
|
655
|
+
dataIndex: 'responseDelay',
|
|
656
|
+
key: 'responseDelay',
|
|
657
|
+
width: 100,
|
|
658
|
+
sorter: (a, b) => (a.responseDelay || 0) - (b.responseDelay || 0),
|
|
659
|
+
render: (delay) => {
|
|
660
|
+
const delayValue = parseInt(delay, 10) || 0;
|
|
661
|
+
return (
|
|
662
|
+
<span className={delayValue > 0 ? 'delay-active' : 'delay-inactive'}>
|
|
663
|
+
{delayValue > 0 ? delayValue : '无延迟'}
|
|
664
|
+
</span>
|
|
665
|
+
);
|
|
666
|
+
},
|
|
731
667
|
},
|
|
732
668
|
{
|
|
733
669
|
title: '状态码',
|
|
734
670
|
dataIndex: 'httpStatus',
|
|
735
671
|
key: 'httpStatus',
|
|
736
672
|
width: 100,
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
673
|
+
render: (status) => {
|
|
674
|
+
const statusCode = status || 200;
|
|
675
|
+
let statusClass = 'status-success';
|
|
676
|
+
if (statusCode >= 400) {
|
|
677
|
+
statusClass = 'status-error';
|
|
678
|
+
} else if (statusCode >= 300) {
|
|
679
|
+
statusClass = 'status-warning';
|
|
680
|
+
}
|
|
681
|
+
return <span className={statusClass}>{statusCode}</span>;
|
|
741
682
|
},
|
|
742
|
-
sortDirections: ['ascend', 'descend'],
|
|
743
|
-
render: (text, record) => {
|
|
744
|
-
return record.proxyType === 'response' ? text : '-';
|
|
745
|
-
}
|
|
746
683
|
},
|
|
747
684
|
{
|
|
748
685
|
title: '内容类型',
|
|
@@ -906,6 +843,28 @@ const InterfaceManagement = () => {
|
|
|
906
843
|
const visibleColumns = tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key);
|
|
907
844
|
const columns = allColumns.filter(col => visibleColumns.includes(col.key));
|
|
908
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
|
+
|
|
909
868
|
return (
|
|
910
869
|
<AppLayout>
|
|
911
870
|
<div className="interface-management-container">
|
|
@@ -959,6 +918,77 @@ const InterfaceManagement = () => {
|
|
|
959
918
|
/>
|
|
960
919
|
)}
|
|
961
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
|
+
|
|
962
992
|
{/* 接口列表状态栏 */}
|
|
963
993
|
{filteredInterfaces.length > 0 && !interfacesLoading && (
|
|
964
994
|
<div style={{
|
|
@@ -974,6 +1004,11 @@ const InterfaceManagement = () => {
|
|
|
974
1004
|
}}>
|
|
975
1005
|
<span>
|
|
976
1006
|
当前功能模块:<strong>{selectedFeature?.name}</strong>
|
|
1007
|
+
{selectedGroup && (
|
|
1008
|
+
<span style={{ marginLeft: 8 }}>
|
|
1009
|
+
| 分组:<Tag color="blue">{selectedGroup}</Tag>
|
|
1010
|
+
</span>
|
|
1011
|
+
)}
|
|
977
1012
|
</span>
|
|
978
1013
|
<span>
|
|
979
1014
|
共 {filteredInterfaces.length} 个接口,每页显示 {tableConfig.pageSize} 个
|
|
@@ -1088,6 +1123,7 @@ const InterfaceManagement = () => {
|
|
|
1088
1123
|
layout="vertical"
|
|
1089
1124
|
initialValues={{
|
|
1090
1125
|
name: '',
|
|
1126
|
+
group: '',
|
|
1091
1127
|
pattern: '',
|
|
1092
1128
|
proxyType: 'response',
|
|
1093
1129
|
statusCode: '200',
|
|
@@ -1096,10 +1132,11 @@ const InterfaceManagement = () => {
|
|
|
1096
1132
|
httpMethod: 'ALL',
|
|
1097
1133
|
targetUrl: '',
|
|
1098
1134
|
headerItems: [],
|
|
1099
|
-
paramMatchers: []
|
|
1135
|
+
paramMatchers: [],
|
|
1136
|
+
responseDelay: '0'
|
|
1100
1137
|
}}
|
|
1101
1138
|
>
|
|
1102
|
-
{/*
|
|
1139
|
+
{/* 基础信息表单项 */}
|
|
1103
1140
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1104
1141
|
<div style={{ flex: 1 }}>
|
|
1105
1142
|
<Form.Item
|
|
@@ -1110,6 +1147,34 @@ const InterfaceManagement = () => {
|
|
|
1110
1147
|
<Input placeholder="请输入接口名称" />
|
|
1111
1148
|
</Form.Item>
|
|
1112
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
|
+
mode="tags"
|
|
1161
|
+
tokenSeparators={[',']}
|
|
1162
|
+
onInputKeyDown={(e) => {
|
|
1163
|
+
// 防止回车键提交表单
|
|
1164
|
+
if (e.key === 'Enter') {
|
|
1165
|
+
e.stopPropagation();
|
|
1166
|
+
}
|
|
1167
|
+
}}
|
|
1168
|
+
>
|
|
1169
|
+
{Array.from(new Set(interfaces.map(item => item.group).filter(Boolean))).map(group => (
|
|
1170
|
+
<Option key={group} value={group}>{group}</Option>
|
|
1171
|
+
))}
|
|
1172
|
+
</Select>
|
|
1173
|
+
</Form.Item>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
|
|
1177
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1113
1178
|
<div style={{ flex: 1 }}>
|
|
1114
1179
|
<Form.Item
|
|
1115
1180
|
name="pattern"
|
|
@@ -1189,9 +1254,6 @@ const InterfaceManagement = () => {
|
|
|
1189
1254
|
/>
|
|
1190
1255
|
</Form.Item>
|
|
1191
1256
|
</div>
|
|
1192
|
-
</div>
|
|
1193
|
-
|
|
1194
|
-
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1195
1257
|
<div style={{ flex: 1 }}>
|
|
1196
1258
|
<Form.Item
|
|
1197
1259
|
name="proxyType"
|
|
@@ -1219,7 +1281,7 @@ const InterfaceManagement = () => {
|
|
|
1219
1281
|
const patternInput = document.querySelector('input[placeholder="根据选择的处理方式输入相应格式的URL"]');
|
|
1220
1282
|
if (patternInput) {
|
|
1221
1283
|
if (value === 'response') {
|
|
1222
|
-
patternInput.placeholder = "例如:/api/users,/api/users/*,/api
|
|
1284
|
+
patternInput.placeholder = "例如:/api/users,/api/users/*,/api\\/users\\/\\d+/";
|
|
1223
1285
|
} else if (value === 'redirect') {
|
|
1224
1286
|
patternInput.placeholder = "例如:https://example.com/api";
|
|
1225
1287
|
} else if (value === 'url_redirect') {
|
|
@@ -1233,6 +1295,9 @@ const InterfaceManagement = () => {
|
|
|
1233
1295
|
</Select>
|
|
1234
1296
|
</Form.Item>
|
|
1235
1297
|
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
|
|
1300
|
+
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
1236
1301
|
<div style={{ flex: 1 }}>
|
|
1237
1302
|
<Form.Item
|
|
1238
1303
|
name="httpMethod"
|
|
@@ -1246,6 +1311,9 @@ const InterfaceManagement = () => {
|
|
|
1246
1311
|
</Select>
|
|
1247
1312
|
</Form.Item>
|
|
1248
1313
|
</div>
|
|
1314
|
+
<div style={{ flex: 1 }}>
|
|
1315
|
+
{/* 这里可以留空或添加其他字段 */}
|
|
1316
|
+
</div>
|
|
1249
1317
|
</div>
|
|
1250
1318
|
|
|
1251
1319
|
{/* 根据proxyType显示不同的表单项 */}
|
|
@@ -1283,6 +1351,29 @@ const InterfaceManagement = () => {
|
|
|
1283
1351
|
</Select>
|
|
1284
1352
|
</Form.Item>
|
|
1285
1353
|
</div>
|
|
1354
|
+
<div style={{ flex: 1 }}>
|
|
1355
|
+
<Form.Item
|
|
1356
|
+
name="responseDelay"
|
|
1357
|
+
label="延迟响应(毫秒)"
|
|
1358
|
+
tooltip="设置接口响应延迟时间,单位为毫秒"
|
|
1359
|
+
rules={[
|
|
1360
|
+
{
|
|
1361
|
+
pattern: /^\d*$/,
|
|
1362
|
+
message: '请输入有效的数字'
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
validator: (_, value) => {
|
|
1366
|
+
if (!value || parseInt(value, 10) <= 60000) {
|
|
1367
|
+
return Promise.resolve();
|
|
1368
|
+
}
|
|
1369
|
+
return Promise.reject(new Error('延迟时间不能超过60秒(60000毫秒)'));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
]}
|
|
1373
|
+
>
|
|
1374
|
+
<Input placeholder="例如:500" />
|
|
1375
|
+
</Form.Item>
|
|
1376
|
+
</div>
|
|
1286
1377
|
</div>
|
|
1287
1378
|
|
|
1288
1379
|
{/* 请求入参匹配设置 */}
|