whistle.mock-plugins 1.0.4 → 1.0.6-beta.0
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/cgi-bin/interfaces.js +3 -0
- package/app/cgi-bin/test-interface.js +279 -230
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +2 -0
- package/lib/rules-server.js +292 -133
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +343 -91
- package/ui/src/pages/MockData.js +32 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { useParams, useHistory } from 'react-router-dom';
|
|
3
3
|
import AppLayout from '../components/AppLayout';
|
|
4
|
-
import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs } from 'antd';
|
|
5
|
-
import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs, Card, Divider, Badge, Tooltip, Row, Col } from 'antd';
|
|
5
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined, CopyOutlined, CodeOutlined, CheckCircleOutlined, CloseCircleOutlined, MenuOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
|
6
6
|
import '../styles/interface-management.css';
|
|
7
7
|
import axios from 'axios';
|
|
8
8
|
|
|
@@ -137,6 +137,16 @@ const InterfaceManagement = () => {
|
|
|
137
137
|
|
|
138
138
|
const handleEditInterface = (record) => {
|
|
139
139
|
setEditingInterface(record);
|
|
140
|
+
|
|
141
|
+
// 将自定义请求头转换为数组格式,用于动态表单项
|
|
142
|
+
let headersArray = [];
|
|
143
|
+
if (record.customHeaders && typeof record.customHeaders === 'object') {
|
|
144
|
+
headersArray = Object.entries(record.customHeaders).map(([key, value]) => ({
|
|
145
|
+
headerName: key,
|
|
146
|
+
headerValue: value
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
140
150
|
form.setFieldsValue({
|
|
141
151
|
name: record.name,
|
|
142
152
|
pattern: record.urlPattern,
|
|
@@ -146,6 +156,7 @@ const InterfaceManagement = () => {
|
|
|
146
156
|
responseBody: record.responseContent || '',
|
|
147
157
|
httpMethod: record.httpMethod || 'ALL',
|
|
148
158
|
targetUrl: record.targetUrl || '',
|
|
159
|
+
headerItems: headersArray // 使用数组存储表单项
|
|
149
160
|
});
|
|
150
161
|
setModalVisible(true);
|
|
151
162
|
};
|
|
@@ -156,6 +167,9 @@ const InterfaceManagement = () => {
|
|
|
156
167
|
if (response.data && response.data.code === 0) {
|
|
157
168
|
message.success('接口删除成功');
|
|
158
169
|
fetchInterfaces();
|
|
170
|
+
|
|
171
|
+
// 刷新规则缓存
|
|
172
|
+
refreshCacheAfterUpdate();
|
|
159
173
|
} else {
|
|
160
174
|
throw new Error(response.data?.message || '接口删除失败');
|
|
161
175
|
}
|
|
@@ -203,33 +217,53 @@ const InterfaceManagement = () => {
|
|
|
203
217
|
}
|
|
204
218
|
};
|
|
205
219
|
|
|
220
|
+
// 缓存刷新服务
|
|
221
|
+
const flushCache = async () => {
|
|
222
|
+
try {
|
|
223
|
+
const response = await axios.get('/_flush_cache');
|
|
224
|
+
return response.data;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('刷新缓存失败:', error);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// 更新成功后刷新缓存
|
|
232
|
+
const refreshCacheAfterUpdate = async () => {
|
|
233
|
+
try {
|
|
234
|
+
await flushCache();
|
|
235
|
+
// 这里不需要显示提示,因为主要操作会有自己的提示
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// 如果刷新缓存失败,记录错误但不影响用户体验
|
|
238
|
+
console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
206
242
|
const handleSubmit = async () => {
|
|
207
243
|
try {
|
|
208
244
|
const values = await form.validateFields();
|
|
209
245
|
|
|
210
|
-
//
|
|
211
|
-
if (values.proxyType === 'response' && values.contentType.includes('json')) {
|
|
246
|
+
// 检查responseBody是否是有效的JSON
|
|
247
|
+
if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
|
|
212
248
|
try {
|
|
213
|
-
|
|
214
|
-
const cleanedJson = cleanJsonResponse(values.responseBody);
|
|
215
|
-
JSON.parse(cleanedJson);
|
|
216
|
-
values.responseBody = cleanedJson;
|
|
249
|
+
JSON.parse(values.responseBody);
|
|
217
250
|
} catch (e) {
|
|
218
|
-
message.error('JSON
|
|
251
|
+
message.error('响应体不是有效的JSON格式');
|
|
219
252
|
return;
|
|
220
253
|
}
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
message.error('重定向目标URL不能为空');
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
256
|
+
// 处理自定义请求头,将表单项数组转换为对象格式
|
|
257
|
+
let customHeaders = {};
|
|
228
258
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
|
|
260
|
+
if (values.headerItems && values.headerItems.length > 0) {
|
|
261
|
+
values.headerItems.forEach(item => {
|
|
262
|
+
if (item && item.headerName && item.headerName.trim()) {
|
|
263
|
+
customHeaders[item.headerName.trim()] = item.headerValue || '';
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
233
267
|
}
|
|
234
268
|
|
|
235
269
|
const interfaceData = {
|
|
@@ -239,6 +273,7 @@ const InterfaceManagement = () => {
|
|
|
239
273
|
proxyType: values.proxyType,
|
|
240
274
|
responseContent: values.proxyType === 'response' ? values.responseBody : '',
|
|
241
275
|
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
276
|
+
customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
|
|
242
277
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
243
278
|
contentType: values.contentType,
|
|
244
279
|
responseDelay: 0,
|
|
@@ -246,30 +281,28 @@ const InterfaceManagement = () => {
|
|
|
246
281
|
active: true
|
|
247
282
|
};
|
|
248
283
|
|
|
284
|
+
let response;
|
|
249
285
|
if (editingInterface) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
message.success('接口更新成功');
|
|
253
|
-
} else {
|
|
254
|
-
throw new Error(response.data?.message || '接口更新失败');
|
|
255
|
-
}
|
|
286
|
+
// 更新现有接口
|
|
287
|
+
response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
|
|
256
288
|
} else {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
message.success('接口添加成功');
|
|
260
|
-
} else {
|
|
261
|
-
throw new Error(response.data?.message || '接口添加失败');
|
|
262
|
-
}
|
|
289
|
+
// 创建新接口
|
|
290
|
+
response = await axios.post('/cgi-bin/interfaces', interfaceData);
|
|
263
291
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
292
|
+
|
|
293
|
+
if (response.data && response.data.code === 0) {
|
|
294
|
+
message.success(editingInterface ? '接口更新成功' : '接口创建成功');
|
|
295
|
+
setModalVisible(false);
|
|
296
|
+
fetchInterfaces();
|
|
297
|
+
|
|
298
|
+
// 刷新规则缓存
|
|
299
|
+
refreshCacheAfterUpdate();
|
|
300
|
+
} else {
|
|
301
|
+
throw new Error(response.data?.message || '操作失败');
|
|
270
302
|
}
|
|
303
|
+
} catch (error) {
|
|
271
304
|
console.error('操作失败:', error);
|
|
272
|
-
message.error(error.response?.data?.message || error.message || '
|
|
305
|
+
message.error(error.response?.data?.message || error.message || '操作失败');
|
|
273
306
|
}
|
|
274
307
|
};
|
|
275
308
|
|
|
@@ -277,10 +310,11 @@ const InterfaceManagement = () => {
|
|
|
277
310
|
setModalVisible(false);
|
|
278
311
|
};
|
|
279
312
|
|
|
280
|
-
const handleTestInterface = (record) => {
|
|
313
|
+
const handleTestInterface = async (record) => {
|
|
281
314
|
setEditingInterface(record);
|
|
282
315
|
setTestUrl('');
|
|
283
316
|
setTestResult(null);
|
|
317
|
+
setTestLoading(false);
|
|
284
318
|
setTestModalVisible(true);
|
|
285
319
|
testForm.resetFields();
|
|
286
320
|
};
|
|
@@ -327,11 +361,26 @@ const InterfaceManagement = () => {
|
|
|
327
361
|
const handleTestSubmit = async () => {
|
|
328
362
|
try {
|
|
329
363
|
const values = await testForm.validateFields();
|
|
364
|
+
const { testUrl } = values;
|
|
365
|
+
|
|
366
|
+
if (!testUrl) {
|
|
367
|
+
message.error('请输入测试URL');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
330
371
|
setTestLoading(true);
|
|
331
372
|
|
|
373
|
+
try {
|
|
374
|
+
// 测试前刷新缓存,确保使用最新的接口定义
|
|
375
|
+
await refreshCacheAfterUpdate();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// 即使刷新缓存失败,也继续测试
|
|
378
|
+
console.warn('刷新缓存失败,将使用现有的缓存数据:', error);
|
|
379
|
+
}
|
|
380
|
+
|
|
332
381
|
try {
|
|
333
382
|
// 验证测试 URL 是否匹配当前接口的匹配规则
|
|
334
|
-
if (!isUrlMatchPattern(
|
|
383
|
+
if (!isUrlMatchPattern(testUrl, editingInterface.urlPattern, editingInterface.proxyType)) {
|
|
335
384
|
let errorMessage = '测试URL与接口匹配规则不匹配';
|
|
336
385
|
|
|
337
386
|
// 根据不同的代理类型提供更具体的错误信息
|
|
@@ -346,27 +395,26 @@ const InterfaceManagement = () => {
|
|
|
346
395
|
error: errorMessage
|
|
347
396
|
});
|
|
348
397
|
message.error(errorMessage);
|
|
398
|
+
setTestLoading(false);
|
|
349
399
|
return;
|
|
350
400
|
}
|
|
351
401
|
|
|
352
402
|
// 调用实际测试接口
|
|
353
403
|
const response = await axios.post(`/cgi-bin/test-interface`, {
|
|
354
|
-
url:
|
|
404
|
+
url: testUrl,
|
|
355
405
|
interfaceId: editingInterface.id
|
|
356
406
|
});
|
|
357
|
-
|
|
358
|
-
if (response.data && response.data.code === 0
|
|
407
|
+
|
|
408
|
+
if (response.data && response.data.code === 0) {
|
|
409
|
+
// 设置测试结果
|
|
359
410
|
setTestResult({
|
|
360
411
|
success: true,
|
|
361
|
-
|
|
362
|
-
contentType: response.data.data.contentType,
|
|
363
|
-
responseBody: response.data.data.responseBody,
|
|
412
|
+
...response.data.data,
|
|
364
413
|
matchedRule: editingInterface.urlPattern,
|
|
365
414
|
httpMethod: editingInterface.httpMethod,
|
|
366
|
-
requestUrl:
|
|
415
|
+
requestUrl: testUrl,
|
|
367
416
|
mockInfo: response.data.data.mockInfo
|
|
368
417
|
});
|
|
369
|
-
message.success('测试成功');
|
|
370
418
|
} else {
|
|
371
419
|
throw new Error(response.data?.message || '测试响应格式不正确');
|
|
372
420
|
}
|
|
@@ -383,6 +431,8 @@ const InterfaceManagement = () => {
|
|
|
383
431
|
if (error.errorFields) {
|
|
384
432
|
return; // 表单验证错误
|
|
385
433
|
}
|
|
434
|
+
console.error('测试失败:', error);
|
|
435
|
+
message.error(error.message || '测试失败');
|
|
386
436
|
setTestLoading(false);
|
|
387
437
|
}
|
|
388
438
|
};
|
|
@@ -516,6 +566,47 @@ const InterfaceManagement = () => {
|
|
|
516
566
|
return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
|
|
517
567
|
}
|
|
518
568
|
},
|
|
569
|
+
{
|
|
570
|
+
title: '自定义头',
|
|
571
|
+
dataIndex: 'customHeaders',
|
|
572
|
+
key: 'customHeaders',
|
|
573
|
+
width: 100,
|
|
574
|
+
render: (_, record) => {
|
|
575
|
+
if (record.proxyType !== 'redirect' && record.proxyType !== 'url_redirect') {
|
|
576
|
+
return '-';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const headers = record.customHeaders || {};
|
|
580
|
+
const count = Object.keys(headers).length;
|
|
581
|
+
|
|
582
|
+
if (count === 0) {
|
|
583
|
+
return '-';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 检查是否包含随机值
|
|
587
|
+
const hasRandomValue = Object.values(headers).some(v => v && v.startsWith('@'));
|
|
588
|
+
|
|
589
|
+
return (
|
|
590
|
+
<Tooltip title={
|
|
591
|
+
<div>
|
|
592
|
+
{Object.entries(headers).map(([key, value]) => (
|
|
593
|
+
<div key={key}>
|
|
594
|
+
{key}: {value}
|
|
595
|
+
{value && value.startsWith('@') && (
|
|
596
|
+
<span style={{ color: '#52c41a' }}> (随机)</span>
|
|
597
|
+
)}
|
|
598
|
+
</div>
|
|
599
|
+
))}
|
|
600
|
+
</div>
|
|
601
|
+
}>
|
|
602
|
+
<span style={{ color: '#1890ff' }}>
|
|
603
|
+
{count}个
|
|
604
|
+
{hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
|
|
605
|
+
</span>
|
|
606
|
+
</Tooltip>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
},
|
|
519
610
|
{
|
|
520
611
|
title: '请求方法',
|
|
521
612
|
dataIndex: 'httpMethod',
|
|
@@ -640,7 +731,8 @@ const InterfaceManagement = () => {
|
|
|
640
731
|
contentType: 'application/json; charset=utf-8',
|
|
641
732
|
responseBody: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
|
|
642
733
|
httpMethod: 'ALL',
|
|
643
|
-
targetUrl: ''
|
|
734
|
+
targetUrl: '',
|
|
735
|
+
headerItems: []
|
|
644
736
|
}}
|
|
645
737
|
>
|
|
646
738
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
|
|
@@ -866,51 +958,183 @@ const InterfaceManagement = () => {
|
|
|
866
958
|
|
|
867
959
|
if (proxyType === 'redirect' || proxyType === 'url_redirect') {
|
|
868
960
|
return (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
{
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
961
|
+
<>
|
|
962
|
+
<Form.Item
|
|
963
|
+
name="targetUrl"
|
|
964
|
+
label="重定向目标URL"
|
|
965
|
+
rules={[
|
|
966
|
+
{ required: true, message: '请输入重定向目标URL' },
|
|
967
|
+
{
|
|
968
|
+
validator: (_, value) => {
|
|
969
|
+
if (!value) return Promise.resolve();
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
new URL(value);
|
|
973
|
+
return Promise.resolve();
|
|
974
|
+
} catch (e) {
|
|
975
|
+
return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
|
|
976
|
+
}
|
|
883
977
|
}
|
|
884
978
|
}
|
|
979
|
+
]}
|
|
980
|
+
tooltip={proxyType === 'redirect' ?
|
|
981
|
+
"重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
|
|
982
|
+
"URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
|
|
885
983
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
984
|
+
>
|
|
985
|
+
<Input
|
|
986
|
+
placeholder={proxyType === 'redirect' ?
|
|
987
|
+
"例如:https://example.com/api" :
|
|
988
|
+
"例如:https://example.com/api/users"
|
|
989
|
+
}
|
|
990
|
+
addonBefore={
|
|
991
|
+
<Select
|
|
992
|
+
defaultValue="https://"
|
|
993
|
+
className="select-before"
|
|
994
|
+
style={{ width: 100 }}
|
|
995
|
+
onChange={(value) => {
|
|
996
|
+
const currentUrl = form.getFieldValue('targetUrl') || '';
|
|
997
|
+
const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
|
|
998
|
+
form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
|
|
999
|
+
}}
|
|
1000
|
+
>
|
|
1001
|
+
<Option value="http://">http://</Option>
|
|
1002
|
+
<Option value="https://">https://</Option>
|
|
1003
|
+
</Select>
|
|
1004
|
+
}
|
|
1005
|
+
/>
|
|
1006
|
+
</Form.Item>
|
|
1007
|
+
|
|
1008
|
+
<Form.Item
|
|
1009
|
+
label="自定义请求头"
|
|
1010
|
+
tooltip="为请求添加或替换HTTP请求头"
|
|
1011
|
+
className="custom-headers-container"
|
|
1012
|
+
>
|
|
1013
|
+
<Form.List name="headerItems">
|
|
1014
|
+
{(fields, { add, remove }) => (
|
|
1015
|
+
<>
|
|
1016
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
1017
|
+
<Row key={key} gutter={8} style={{ marginBottom: 8 }}>
|
|
1018
|
+
<Col span={10}>
|
|
1019
|
+
<Form.Item
|
|
1020
|
+
{...restField}
|
|
1021
|
+
name={[name, 'headerName']}
|
|
1022
|
+
noStyle
|
|
1023
|
+
rules={[
|
|
1024
|
+
{
|
|
1025
|
+
required: true,
|
|
1026
|
+
message: '请输入请求头名称'
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
pattern: /^[^:]+$/,
|
|
1030
|
+
message: '请求头名称不能包含冒号'
|
|
1031
|
+
}
|
|
1032
|
+
]}
|
|
1033
|
+
>
|
|
1034
|
+
<Input placeholder="Header-Name" />
|
|
1035
|
+
</Form.Item>
|
|
1036
|
+
</Col>
|
|
1037
|
+
<Col span={12}>
|
|
1038
|
+
<Form.Item
|
|
1039
|
+
{...restField}
|
|
1040
|
+
name={[name, 'headerValue']}
|
|
1041
|
+
noStyle
|
|
1042
|
+
rules={[
|
|
1043
|
+
{
|
|
1044
|
+
validator: (_, value) => {
|
|
1045
|
+
if (!value) return Promise.resolve();
|
|
1046
|
+
|
|
1047
|
+
// 检查是否是随机数格式 @xxxx-xxx
|
|
1048
|
+
if (value.startsWith('@')) {
|
|
1049
|
+
const randomPattern = value.substring(1);
|
|
1050
|
+
// 验证格式: 只能包含x和-,且首尾不能是-
|
|
1051
|
+
if (!/^[x][-x]*[x]$/.test(randomPattern) ||
|
|
1052
|
+
randomPattern.startsWith('-') ||
|
|
1053
|
+
randomPattern.endsWith('-')) {
|
|
1054
|
+
return Promise.reject(
|
|
1055
|
+
new Error('随机数格式错误,应为@xxxx-xxx格式,只能包含x和-,且首尾不能是-')
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return Promise.resolve();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
]}
|
|
1063
|
+
>
|
|
1064
|
+
<Input placeholder="Header-Value" />
|
|
1065
|
+
</Form.Item>
|
|
1066
|
+
</Col>
|
|
1067
|
+
<Col span={2}>
|
|
1068
|
+
<Button
|
|
1069
|
+
type="text"
|
|
1070
|
+
icon={<MinusCircleOutlined />}
|
|
1071
|
+
onClick={() => remove(name)}
|
|
1072
|
+
style={{ color: '#ff4d4f' }}
|
|
1073
|
+
/>
|
|
1074
|
+
</Col>
|
|
1075
|
+
</Row>
|
|
1076
|
+
))}
|
|
1077
|
+
|
|
1078
|
+
<Form.Item>
|
|
1079
|
+
<Button
|
|
1080
|
+
type="dashed"
|
|
1081
|
+
onClick={() => add()}
|
|
1082
|
+
block
|
|
1083
|
+
icon={<PlusOutlined />}
|
|
1084
|
+
>
|
|
1085
|
+
添加请求头
|
|
1086
|
+
</Button>
|
|
1087
|
+
</Form.Item>
|
|
1088
|
+
|
|
1089
|
+
<div style={{ marginTop: 8 }}>
|
|
1090
|
+
<Button
|
|
1091
|
+
type="link"
|
|
1092
|
+
onClick={() => {
|
|
1093
|
+
add({ headerName: 'Content-Type', headerValue: 'application/json' });
|
|
1094
|
+
}}
|
|
1095
|
+
>
|
|
1096
|
+
添加 Content-Type
|
|
1097
|
+
</Button>
|
|
1098
|
+
<Button
|
|
1099
|
+
type="link"
|
|
1100
|
+
onClick={() => {
|
|
1101
|
+
add({ headerName: 'Authorization', headerValue: 'Bearer ' });
|
|
1102
|
+
}}
|
|
1103
|
+
>
|
|
1104
|
+
添加 Authorization
|
|
1105
|
+
</Button>
|
|
1106
|
+
<Button
|
|
1107
|
+
type="link"
|
|
1108
|
+
onClick={() => {
|
|
1109
|
+
add({ headerName: 'User-Agent', headerValue: 'Mozilla/5.0' });
|
|
1110
|
+
}}
|
|
1111
|
+
>
|
|
1112
|
+
添加 User-Agent
|
|
1113
|
+
</Button>
|
|
1114
|
+
<Button
|
|
1115
|
+
type="link"
|
|
1116
|
+
onClick={() => {
|
|
1117
|
+
add({ headerName: 'X-Random-ID', headerValue: '@xxxx-xxxx' });
|
|
1118
|
+
}}
|
|
1119
|
+
>
|
|
1120
|
+
添加随机ID
|
|
1121
|
+
</Button>
|
|
1122
|
+
</div>
|
|
1123
|
+
|
|
1124
|
+
<div style={{ marginTop: 8, background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
|
|
1125
|
+
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>随机数格式说明:</div>
|
|
1126
|
+
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
|
1127
|
+
<li>使用 <code>@</code> 开头表示这是一个随机数值</li>
|
|
1128
|
+
<li>格式示例:<code>@xxxx-xxxx</code> 将生成如 <code>a1b2-c3d4</code> 的随机值</li>
|
|
1129
|
+
<li>每个 <code>x</code> 将替换为随机字母或数字</li>
|
|
1130
|
+
<li><code>-</code> 将保留在输出中作为分隔符</li>
|
|
1131
|
+
</ul>
|
|
1132
|
+
</div>
|
|
1133
|
+
</>
|
|
1134
|
+
)}
|
|
1135
|
+
</Form.List>
|
|
1136
|
+
</Form.Item>
|
|
1137
|
+
</>
|
|
914
1138
|
);
|
|
915
1139
|
}
|
|
916
1140
|
|
|
@@ -1046,6 +1270,34 @@ const InterfaceManagement = () => {
|
|
|
1046
1270
|
</div>
|
|
1047
1271
|
)}
|
|
1048
1272
|
|
|
1273
|
+
{(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') &&
|
|
1274
|
+
testResult.formattedHeaders && (
|
|
1275
|
+
<div className="result-item">
|
|
1276
|
+
<span className="label">自定义请求头:</span>
|
|
1277
|
+
<div className="value" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
|
1278
|
+
{
|
|
1279
|
+
editingInterface.customHeaders &&
|
|
1280
|
+
Object.entries(editingInterface.customHeaders)
|
|
1281
|
+
.map(([key, value]) => {
|
|
1282
|
+
const generatedValue = testResult.customHeaders[key];
|
|
1283
|
+
return (
|
|
1284
|
+
<div key={key}>
|
|
1285
|
+
{key}: {value.startsWith('@') ? (
|
|
1286
|
+
<span>
|
|
1287
|
+
<span style={{ color: '#d9d9d9' }}>{value}</span>
|
|
1288
|
+
{' '} → {' '}
|
|
1289
|
+
<span style={{ color: '#52c41a', fontWeight: 'bold' }}>{generatedValue}</span>
|
|
1290
|
+
<span style={{ color: '#8c8c8c', fontSize: '12px' }}> (随机生成)</span>
|
|
1291
|
+
</span>
|
|
1292
|
+
) : generatedValue}
|
|
1293
|
+
</div>
|
|
1294
|
+
);
|
|
1295
|
+
})
|
|
1296
|
+
}
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
)}
|
|
1300
|
+
|
|
1049
1301
|
{editingInterface.proxyType === 'response' && (
|
|
1050
1302
|
<>
|
|
1051
1303
|
<div className="result-item">
|
package/ui/src/pages/MockData.js
CHANGED
|
@@ -4,10 +4,22 @@ import AppLayout from '../components/AppLayout';
|
|
|
4
4
|
import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge } from 'antd';
|
|
5
5
|
import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined } from '@ant-design/icons';
|
|
6
6
|
import '../styles/mock-data.css';
|
|
7
|
+
import axios from 'axios';
|
|
7
8
|
|
|
8
9
|
const { Text, Title, Paragraph } = Typography;
|
|
9
10
|
const { TextArea } = Input;
|
|
10
11
|
|
|
12
|
+
// 刷新缓存服务
|
|
13
|
+
const flushCache = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const response = await axios.get(`/_flush_cache`);
|
|
16
|
+
return response.data;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('刷新缓存失败:', error);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
11
23
|
const MockData = () => {
|
|
12
24
|
const history = useHistory();
|
|
13
25
|
const [mockFeatures, setMockFeatures] = useState([]);
|
|
@@ -69,6 +81,17 @@ const MockData = () => {
|
|
|
69
81
|
setShowModal(false);
|
|
70
82
|
};
|
|
71
83
|
|
|
84
|
+
// 更新成功后刷新缓存
|
|
85
|
+
const refreshCacheAfterUpdate = async () => {
|
|
86
|
+
try {
|
|
87
|
+
await flushCache();
|
|
88
|
+
// 不显示提示,避免干扰主操作的反馈
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// 记录错误但不影响用户体验
|
|
91
|
+
console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
72
95
|
const handleSubmit = async (values) => {
|
|
73
96
|
try {
|
|
74
97
|
const featureData = {
|
|
@@ -96,6 +119,9 @@ const MockData = () => {
|
|
|
96
119
|
message.success(currentFeature ? '功能模块更新成功' : '功能模块创建成功');
|
|
97
120
|
fetchFeatures();
|
|
98
121
|
closeModal();
|
|
122
|
+
|
|
123
|
+
// 刷新缓存
|
|
124
|
+
refreshCacheAfterUpdate();
|
|
99
125
|
} else {
|
|
100
126
|
message.error('操作失败: ' + result.message);
|
|
101
127
|
}
|
|
@@ -127,6 +153,9 @@ const MockData = () => {
|
|
|
127
153
|
setMockFeatures(mockFeatures.map(feature =>
|
|
128
154
|
feature.id === id ? { ...feature, active: !currentActive } : feature
|
|
129
155
|
));
|
|
156
|
+
|
|
157
|
+
// 刷新缓存 - 功能启用状态变化是最关键的需要刷新缓存的操作
|
|
158
|
+
refreshCacheAfterUpdate();
|
|
130
159
|
} else {
|
|
131
160
|
message.error(`${currentActive ? '禁用' : '启用'}功能模块失败: ${result.message}`);
|
|
132
161
|
}
|
|
@@ -156,6 +185,9 @@ const MockData = () => {
|
|
|
156
185
|
message.success('功能模块已成功删除');
|
|
157
186
|
// 更新本地状态
|
|
158
187
|
setMockFeatures(mockFeatures.filter(f => f.id !== id));
|
|
188
|
+
|
|
189
|
+
// 刷新缓存
|
|
190
|
+
refreshCacheAfterUpdate();
|
|
159
191
|
} else {
|
|
160
192
|
message.error('删除失败: ' + result.message);
|
|
161
193
|
}
|