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.
@@ -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
- // 清理并验证JSON
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
- // 验证重定向URL
224
- if ((values.proxyType === 'redirect' || values.proxyType === 'url_redirect') && !values.targetUrl) {
225
- message.error('重定向目标URL不能为空');
226
- return;
227
- }
256
+ // 处理自定义请求头,将表单项数组转换为对象格式
257
+ let customHeaders = {};
228
258
 
229
- // 验证重定向URL的格式
230
- if ((values.proxyType === 'redirect' || values.proxyType === 'url_redirect') && !isValidUrl(values.targetUrl)) {
231
- message.error('重定向目标URL格式不正确,请输入完整的URL,包括http://或https://');
232
- return;
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
- const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
251
- if (response.data && response.data.code === 0) {
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
- const response = await axios.post('/cgi-bin/interfaces', interfaceData);
258
- if (response.data && response.data.code === 0) {
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
- setModalVisible(false);
266
- fetchInterfaces();
267
- } catch (error) {
268
- if (error.errorFields) {
269
- return; // 表单验证错误
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(values.testUrl, editingInterface.urlPattern, editingInterface.proxyType)) {
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: values.testUrl,
404
+ url: testUrl,
355
405
  interfaceId: editingInterface.id
356
406
  });
357
-
358
- if (response.data && response.data.code === 0 && response.data.data) {
407
+
408
+ if (response.data && response.data.code === 0) {
409
+ // 设置测试结果
359
410
  setTestResult({
360
411
  success: true,
361
- statusCode: response.data.data.statusCode,
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: values.testUrl,
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
- <Form.Item
870
- name="targetUrl"
871
- label="重定向目标URL"
872
- rules={[
873
- { required: true, message: '请输入重定向目标URL' },
874
- {
875
- validator: (_, value) => {
876
- if (!value) return Promise.resolve();
877
-
878
- try {
879
- new URL(value);
880
- return Promise.resolve();
881
- } catch (e) {
882
- return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
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
- tooltip={proxyType === 'redirect' ?
888
- "重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
889
- "URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
890
- }
891
- >
892
- <Input
893
- placeholder={proxyType === 'redirect' ?
894
- "例如:https://example.com/api" :
895
- "例如:https://example.com/api/users"
896
- }
897
- addonBefore={
898
- <Select
899
- defaultValue="https://"
900
- className="select-before"
901
- style={{ width: 100 }}
902
- onChange={(value) => {
903
- const currentUrl = form.getFieldValue('targetUrl') || '';
904
- const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
905
- form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
906
- }}
907
- >
908
- <Option value="http://">http://</Option>
909
- <Option value="https://">https://</Option>
910
- </Select>
911
- }
912
- />
913
- </Form.Item>
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">
@@ -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
  }