whistle.mock-plugins 1.2.2-beta.18 → 1.2.2-beta.22

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.
@@ -0,0 +1,51 @@
1
+ const express = require('express');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const storage = require('../storage');
5
+
6
+ const DATA_DIR = storage.DATA_DIR;
7
+ const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json');
8
+
9
+ fs.ensureDirSync(DATA_DIR);
10
+
11
+ const DEFAULT_SETTINGS = {
12
+ interfaceParamMatcherEnabled: true,
13
+ responseParamMatcherEnabled: true,
14
+ };
15
+
16
+ const getSettings = () => {
17
+ try {
18
+ if (!fs.existsSync(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
19
+ return { ...DEFAULT_SETTINGS, ...fs.readJsonSync(SETTINGS_FILE) };
20
+ } catch (err) {
21
+ return { ...DEFAULT_SETTINGS };
22
+ }
23
+ };
24
+
25
+ const saveSettings = (settings) => {
26
+ fs.writeJsonSync(SETTINGS_FILE, settings, { spaces: 2 });
27
+ };
28
+
29
+ const initRouter = () => {
30
+ const router = express.Router();
31
+ router.use(express.json());
32
+
33
+ router.get('/settings', (req, res) => {
34
+ res.json(getSettings());
35
+ });
36
+
37
+ router.put('/settings', (req, res) => {
38
+ try {
39
+ const current = getSettings();
40
+ const updated = { ...current, ...req.body };
41
+ saveSettings(updated);
42
+ res.json(updated);
43
+ } catch (err) {
44
+ res.status(500).json({ error: '保存设置失败' });
45
+ }
46
+ });
47
+
48
+ return router;
49
+ };
50
+
51
+ module.exports = { initRouter, getSettings };
@@ -1,7 +1,24 @@
1
1
  const fs = require('fs');
2
+ const fse = require('fs-extra');
2
3
  const path = require('path');
3
4
  const url = require('url');
4
5
  const Mock = require('mockjs');
6
+ const storage = require('./storage');
7
+
8
+ const SETTINGS_FILE = path.join(storage.DATA_DIR, 'settings.json');
9
+
10
+ const getSettings = () => {
11
+ try {
12
+ if (!fse.existsSync(SETTINGS_FILE)) return { interfaceParamMatcherEnabled: true, responseParamMatcherEnabled: true };
13
+ const s = fse.readJsonSync(SETTINGS_FILE);
14
+ return {
15
+ interfaceParamMatcherEnabled: s.interfaceParamMatcherEnabled !== false,
16
+ responseParamMatcherEnabled: s.responseParamMatcherEnabled !== false,
17
+ };
18
+ } catch (e) {
19
+ return { interfaceParamMatcherEnabled: true, responseParamMatcherEnabled: true };
20
+ }
21
+ };
5
22
 
6
23
  // 获取嵌套对象属性值的工具函数
7
24
  const getNestedValue = (obj, path) => {
@@ -268,29 +285,34 @@ const ruleManager = {
268
285
  return null;
269
286
  }
270
287
 
271
- // 在URL匹配的接口中,查找参数也匹配的接口
272
- for (const intf of urlMatchedInterfaces) {
273
- // 检查参数匹配
274
- if (isParamMatch(req, intf.paramMatchers, this.log)) {
275
- this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
276
- return intf;
277
- } else {
278
- this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
288
+ // 在URL匹配的接口中,查找参数也匹配的接口(受接口入参匹配开关控制)
289
+ const settings = getSettings();
290
+ if (settings.interfaceParamMatcherEnabled) {
291
+ for (const intf of urlMatchedInterfaces) {
292
+ if (isParamMatch(req, intf.paramMatchers, this.log)) {
293
+ this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
294
+ return intf;
295
+ } else {
296
+ this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
297
+ }
279
298
  }
299
+
300
+ // 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
301
+ const fallbackInterface = urlMatchedInterfaces.find(intf =>
302
+ !intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
303
+ );
304
+
305
+ if (fallbackInterface) {
306
+ this.log(`[规则处理器] 使用回退接口: ${fallbackInterface.name} (无参数匹配规则)`);
307
+ return fallbackInterface;
308
+ }
309
+
310
+ this.log(`[规则处理器] 所有URL匹配的接口都有参数匹配规则且都不满足,未找到合适的接口`);
311
+ return null;
312
+ } else {
313
+ this.log('[规则处理器] 接口入参匹配已关闭,直接返回第一个URL匹配的接口');
314
+ return urlMatchedInterfaces[0];
280
315
  }
281
-
282
- // 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
283
- const fallbackInterface = urlMatchedInterfaces.find(intf =>
284
- !intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
285
- );
286
-
287
- if (fallbackInterface) {
288
- this.log(`[规则处理器] 使用回退接口: ${fallbackInterface.name} (无参数匹配规则)`);
289
- return fallbackInterface;
290
- }
291
-
292
- this.log(`[规则处理器] 所有URL匹配的接口都有参数匹配规则且都不满足,未找到合适的接口`);
293
- return null;
294
316
  },
295
317
 
296
318
  /**
@@ -756,21 +778,48 @@ const ruleManager = {
756
778
  // 检查是否有多响应配置
757
779
  if (config.responses && Array.isArray(config.responses) && config.responses.length > 0) {
758
780
  let selectedResponse = null;
781
+ const settings = getSettings();
759
782
 
760
- // 1. 按顺序查找第一个入参匹配的响应
761
- for (const resp of config.responses) {
762
- if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) {
763
- if (isParamMatch(req, resp.paramMatchers, this.log)) {
764
- selectedResponse = resp;
765
- this.log(`[规则处理器] 响应级入参匹配成功,使用响应: ${resp.name || '未命名'}`);
766
- break;
767
- } else {
768
- this.log(`[规则处理器] 响应 "${resp.name || '未命名'}" 入参不匹配,继续检查下一个`);
783
+ // 1. 优先检查接口级 paramMatchers 中带 targetResponseId 的规则(受开关控制)
784
+ if (settings.interfaceParamMatcherEnabled) {
785
+ const interfaceMatchers = config.paramMatchers;
786
+ if (interfaceMatchers && Array.isArray(interfaceMatchers) && interfaceMatchers.length > 0) {
787
+ for (const matcher of interfaceMatchers) {
788
+ if (!matcher.targetResponseId) continue;
789
+ if (isParamMatch(req, [matcher], this.log)) {
790
+ const target = config.responses.find(r => r.id === matcher.targetResponseId);
791
+ if (target) {
792
+ selectedResponse = target;
793
+ this.log(`[规则处理器] 接口级规则指定响应匹配成功,使用响应: ${target.name || '未命名'}`);
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ }
799
+ } else {
800
+ this.log('[规则处理器] 接口入参匹配已关闭,跳过接口级匹配');
801
+ }
802
+
803
+ // 2. 按顺序查找第一个响应级入参匹配的响应(受开关控制)
804
+ if (!selectedResponse) {
805
+ if (settings.responseParamMatcherEnabled) {
806
+ for (const resp of config.responses) {
807
+ if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) {
808
+ if (isParamMatch(req, resp.paramMatchers, this.log)) {
809
+ selectedResponse = resp;
810
+ this.log(`[规则处理器] 响应级入参匹配成功,使用响应: ${resp.name || '未命名'}`);
811
+ break;
812
+ } else {
813
+ this.log(`[规则处理器] 响应 "${resp.name || '未命名'}" 入参不匹配,继续检查下一个`);
814
+ }
815
+ }
769
816
  }
817
+ } else {
818
+ this.log('[规则处理器] 响应入参匹配已关闭,跳过响应级匹配');
770
819
  }
771
820
  }
772
821
 
773
- // 2. 无匹配则回退到 activeResponseId 指定的默认响应
822
+ // 3. 无匹配则回退到 activeResponseId 指定的默认响应
774
823
  if (!selectedResponse) {
775
824
  const activeResponseId = config.activeResponseId;
776
825
  selectedResponse = (activeResponseId && config.responses.find(r => r.id === activeResponseId))
package/lib/server.js CHANGED
@@ -270,6 +270,10 @@ const app = express();
270
270
  app.use(express.json({limit: '100mb'}));
271
271
  app.use(express.urlencoded({ extended: true, limit: '100mb'}));
272
272
 
273
+ // 注册设置路由
274
+ const settingsRouter = require('./routes/settings');
275
+ app.use('/api', settingsRouter.initRouter());
276
+
273
277
  // 加载功能和接口配置
274
278
  const loadConfigurations = () => {
275
279
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.2.2-beta.18",
3
+ "version": "1.2.2-beta.22",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -42,6 +42,20 @@ const ResponseContentEditor = ({
42
42
  const [isTextareaFullscreen, setIsTextareaFullscreen] = useState(false);
43
43
  const [showSorting, setShowSorting] = useState(false);
44
44
 
45
+ const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(() => {
46
+ const v = localStorage.getItem('responseParamMatcherEnabled');
47
+ return v === null ? true : v !== 'false';
48
+ });
49
+
50
+ useEffect(() => {
51
+ const handleStorage = () => {
52
+ const v = localStorage.getItem('responseParamMatcherEnabled');
53
+ setResponseParamMatcherEnabled(v === null ? true : v !== 'false');
54
+ };
55
+ window.addEventListener('storage', handleStorage);
56
+ return () => window.removeEventListener('storage', handleStorage);
57
+ }, []);
58
+
45
59
  // 查找激活的响应
46
60
  const activeResponseIndex = responses.findIndex(r => r.id === activeResponseId);
47
61
  const activeResponse = activeResponseIndex >= 0 ? responses[activeResponseIndex] : null;
@@ -289,10 +303,8 @@ const ResponseContentEditor = ({
289
303
  <Space size={6}>
290
304
  <FilterOutlined style={{ color: '#1890ff' }} />
291
305
  <span style={{ fontWeight: 'bold', color: '#333' }}>入参匹配规则</span>
292
- {matchers.length > 0 ? (
306
+ {matchers.length > 0 && (
293
307
  <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>{matchers.length}条</Tag>
294
- ) : (
295
- <Tag color="default" style={{ fontSize: '11px', padding: '0 4px' }}>无规则(默认响应)</Tag>
296
308
  )}
297
309
  <Tooltip title="满足所有规则时返回此响应;无规则的响应作为默认兜底。响应按列表顺序依次检查,第一个匹配的响应被返回。">
298
310
  <QuestionCircleOutlined style={{ color: '#bbb', cursor: 'help', fontSize: '12px' }} />
@@ -533,48 +545,12 @@ const ResponseContentEditor = ({
533
545
  placeholder="选择要编辑的响应"
534
546
  value={activeResponseId}
535
547
  onChange={handleResponseSelect}
536
- optionLabelProp="label"
537
548
  >
538
- {responses.map((resp, index) => {
539
- const matcherCount = (resp.paramMatchers || []).filter(m => m.paramPath).length;
540
- const isDefault = resp.id === getFieldValue('activeResponseId');
541
- const labelContent = (
542
- <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
543
- <CheckCircleOutlined style={{ color: '#52c41a' }} />
544
- <span>{resp.name || `响应 ${index + 1}`}</span>
545
- {isDefault && <Tag color="green" style={{ marginLeft: 2, fontSize: '11px', padding: '0 4px' }}>默认</Tag>}
546
- {matcherCount > 0 && <Tag color="blue" style={{ marginLeft: 2, fontSize: '11px', padding: '0 4px' }}>{matcherCount}条规则</Tag>}
547
- </div>
548
- );
549
- return (
550
- <Option
551
- key={resp.id}
552
- value={resp.id}
553
- label={labelContent}
554
- >
555
- <div style={{ padding: '4px 0' }}>
556
- <div style={{
557
- display: 'flex',
558
- alignItems: 'center',
559
- justifyContent: 'space-between',
560
- marginBottom: '4px'
561
- }}>
562
- <span style={{ fontWeight: 'bold' }}>
563
- {resp.name || `响应 ${index + 1}`}
564
- </span>
565
- <Space size={4}>
566
- {isDefault && <Badge status="processing" text="默认" />}
567
- {matcherCount > 0 && (
568
- <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>
569
- {matcherCount}条规则
570
- </Tag>
571
- )}
572
- </Space>
573
- </div>
574
- </div>
575
- </Option>
576
- );
577
- })}
549
+ {responses.map((resp, index) => (
550
+ <Option key={resp.id} value={resp.id}>
551
+ {resp.name || `响应 ${index + 1}`}
552
+ </Option>
553
+ ))}
578
554
  </Select>
579
555
  <Tooltip title="编辑响应名称">
580
556
  <Button
@@ -616,7 +592,7 @@ const ResponseContentEditor = ({
616
592
  </div>
617
593
 
618
594
  {/* 响应级入参匹配规则区域 */}
619
- {activeResponse && renderParamMatchers()}
595
+ {activeResponse && responseParamMatcherEnabled && renderParamMatchers()}
620
596
 
621
597
  {/* 响应内容编辑区域 */}
622
598
  {activeResponse && (
@@ -97,6 +97,12 @@ const InterfaceManagement = () => {
97
97
  // 接口表单显示模式(modal / drawer)
98
98
  const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
99
99
 
100
+ // 接口入参匹配显示开关
101
+ const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
102
+
103
+ // 响应入参匹配显示开关
104
+ const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
105
+
100
106
  // 表单实例
101
107
  const [form] = Form.useForm();
102
108
 
@@ -170,6 +176,19 @@ const InterfaceManagement = () => {
170
176
  };
171
177
  }, []);
172
178
 
179
+ // 监听接口入参匹配 / 响应入参匹配开关变化
180
+ useEffect(() => {
181
+ const readSwitches = () => {
182
+ const v1 = localStorage.getItem('interfaceParamMatcherEnabled');
183
+ setInterfaceParamMatcherEnabled(v1 === null ? true : v1 !== 'false');
184
+ const v2 = localStorage.getItem('responseParamMatcherEnabled');
185
+ setResponseParamMatcherEnabled(v2 === null ? true : v2 !== 'false');
186
+ };
187
+ readSwitches();
188
+ window.addEventListener('storage', readSwitches);
189
+ return () => window.removeEventListener('storage', readSwitches);
190
+ }, []);
191
+
173
192
  // 获取当前选中的功能模块
174
193
  const selectedFeature = features.find(f => f.id === selectedFeatureId);
175
194
 
@@ -178,9 +197,12 @@ const InterfaceManagement = () => {
178
197
  setFeaturesLoading(true);
179
198
  const response = await axios.get('/cgi-bin/features');
180
199
  if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
181
- setFeatures(response.data.data);
182
- // 如果URL中有featureId参数,使用它
183
- const initialFeatureId = featureId || (response.data.data[0]?.id);
200
+ const featureList = response.data.data;
201
+ setFeatures(featureList);
202
+ // 优先级:URL 参数 > localStorage 上次选择 > 第一个功能模块
203
+ const lastSelected = localStorage.getItem('selectedFeatureId');
204
+ const validLastSelected = lastSelected && featureList.find(f => f.id === lastSelected) ? lastSelected : null;
205
+ const initialFeatureId = featureId || validLastSelected || featureList[0]?.id;
184
206
  if (initialFeatureId) {
185
207
  setSelectedFeatureId(initialFeatureId);
186
208
  }
@@ -632,6 +654,7 @@ const InterfaceManagement = () => {
632
654
 
633
655
  const handleSelectFeature = (featureId) => {
634
656
  setSelectedFeatureId(featureId);
657
+ localStorage.setItem('selectedFeatureId', featureId);
635
658
  // 切换功能模块时重置分页到第一页,但保留其他配置
636
659
  saveTableConfig({ current: 1 });
637
660
  };
@@ -873,27 +896,11 @@ const InterfaceManagement = () => {
873
896
  style={{ width: '100%' }}
874
897
  onChange={(value) => handleResponseSwitch(record.id, value)}
875
898
  disabled={record.proxyType !== 'response'}
876
- optionLabelProp="label"
877
899
  >
878
900
  {record.responses.map(resp => {
879
- const matcherCount = (resp.paramMatchers || []).filter(m => m.paramPath).length;
880
- const isDefault = resp.id === activeResponseId;
881
- const labelContent = (
882
- <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
883
- <span>{resp.name}</span>
884
- {isDefault && <Tag color="green" style={{ fontSize: '11px', padding: '0 4px', marginLeft: 2 }}>默认</Tag>}
885
- {matcherCount > 0 && <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px', marginLeft: 2 }}>{matcherCount}条规则</Tag>}
886
- </span>
887
- );
888
901
  return (
889
- <Option key={resp.id} value={resp.id} label={labelContent}>
890
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
891
- <span>{resp.name}</span>
892
- <Space size={4}>
893
- {isDefault && <Tag color="green" style={{ fontSize: '11px', padding: '0 4px' }}>默认</Tag>}
894
- {matcherCount > 0 && <Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>{matcherCount}条规则</Tag>}
895
- </Space>
896
- </div>
902
+ <Option key={resp.id} value={resp.id}>
903
+ {resp.name}
897
904
  </Option>
898
905
  );
899
906
  })}
@@ -1023,13 +1030,18 @@ const InterfaceManagement = () => {
1023
1030
  return '-';
1024
1031
  }
1025
1032
 
1033
+ const showInterface = interfaceParamMatcherEnabled;
1034
+ const showResponse = responseParamMatcherEnabled;
1035
+
1026
1036
  const interfaceMatchers = record.paramMatchers || [];
1027
- const interfaceCount = interfaceMatchers.filter(m => m.paramPath).length;
1037
+ const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
1028
1038
 
1029
1039
  // 统计所有响应级规则
1030
- const responsesWithMatchers = (record.responses || []).filter(resp =>
1031
- Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
1032
- );
1040
+ const responsesWithMatchers = showResponse
1041
+ ? (record.responses || []).filter(resp =>
1042
+ Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
1043
+ )
1044
+ : [];
1033
1045
  const responseMatcherTotal = responsesWithMatchers.reduce(
1034
1046
  (sum, resp) => sum + resp.paramMatchers.filter(m => m.paramPath).length, 0
1035
1047
  );
@@ -1619,7 +1631,8 @@ const InterfaceManagement = () => {
1619
1631
  </Form.Item>
1620
1632
  </div>
1621
1633
  </div>
1622
- <Form.Item label="接口匹配条件" tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。若需要同一接口内不同入参返回不同响应,请在下方「响应内容编辑」中为每个响应单独配置匹配规则。">
1634
+ {interfaceParamMatcherEnabled && (
1635
+ <Form.Item label="接口匹配条件" tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。">
1623
1636
  <Form.List name="paramMatchers">
1624
1637
  {(fields, { add, remove }) => (<>
1625
1638
  {fields.map(({ key, name, ...restField }) => (
@@ -1630,9 +1643,23 @@ const InterfaceManagement = () => {
1630
1643
  <Form.Item {...restField} name={[name, 'paramValue']} rules={[{ required: true, message: '请输入期望值' }]} style={{ flex: 1, margin: 0 }}>
1631
1644
  <Input placeholder="期望值(如:123 或 admin)" />
1632
1645
  </Form.Item>
1633
- <Form.Item {...restField} name={[name, 'matchType']} style={{ width: 100, margin: 0 }} initialValue="exact">
1646
+ <Form.Item {...restField} name={[name, 'matchType']} style={{ width: 90, margin: 0 }} initialValue="exact">
1634
1647
  <Select><Option value="exact">精确匹配</Option><Option value="contains">包含</Option><Option value="regex">正则</Option></Select>
1635
1648
  </Form.Item>
1649
+ <Form.Item noStyle shouldUpdate>
1650
+ {({ getFieldValue }) => {
1651
+ const responses = getFieldValue('responses') || [];
1652
+ return (
1653
+ <Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 130, margin: 0 }}>
1654
+ <Select allowClear placeholder="指定响应(可选)">
1655
+ {responses.filter(r => r && r.id).map(r => (
1656
+ <Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
1657
+ ))}
1658
+ </Select>
1659
+ </Form.Item>
1660
+ );
1661
+ }}
1662
+ </Form.Item>
1636
1663
  <Button type="text" danger icon={<DeleteOutlined />} onClick={() => remove(name)} />
1637
1664
  </div>
1638
1665
  ))}
@@ -1640,6 +1667,7 @@ const InterfaceManagement = () => {
1640
1667
  </>)}
1641
1668
  </Form.List>
1642
1669
  </Form.Item>
1670
+ )}
1643
1671
  <Form.Item name="responses" initialValue={[]} hidden><Input /></Form.Item>
1644
1672
  <Form.Item name="activeResponseId" hidden><Input /></Form.Item>
1645
1673
  <Form.Item noStyle shouldUpdate>
@@ -2032,9 +2060,10 @@ const InterfaceManagement = () => {
2032
2060
  </div>
2033
2061
 
2034
2062
  {/* 请求入参匹配设置(接口级,用于同URL多接口区分) */}
2063
+ {interfaceParamMatcherEnabled && (
2035
2064
  <Form.Item
2036
2065
  label="接口匹配条件"
2037
- tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。满足条件时此接口才会被命中。若需要同一接口内不同入参返回不同响应,请在下方「响应内容编辑」中为每个响应单独配置匹配规则。支持嵌套属性路径,如:a.b.c"
2066
+ tooltip="接口级入参匹配:用于区分同一URL下的不同接口定义。可为每条规则指定响应内容,匹配时直接返回该响应;不指定则走响应级匹配逻辑。支持嵌套属性路径,如:a.b.c"
2038
2067
  >
2039
2068
  <Form.List name="paramMatchers">
2040
2069
  {(fields, { add, remove }) => (
@@ -2075,6 +2104,20 @@ const InterfaceManagement = () => {
2075
2104
  <Option value="regex">正则</Option>
2076
2105
  </Select>
2077
2106
  </Form.Item>
2107
+ <Form.Item noStyle shouldUpdate>
2108
+ {({ getFieldValue }) => {
2109
+ const responses = getFieldValue('responses') || [];
2110
+ return (
2111
+ <Form.Item {...restField} name={[name, 'targetResponseId']} style={{ width: 140, margin: 0 }}>
2112
+ <Select allowClear placeholder="指定响应(可选)">
2113
+ {responses.filter(r => r && r.id).map(r => (
2114
+ <Option key={r.id} value={r.id}>{r.name || '未命名'}</Option>
2115
+ ))}
2116
+ </Select>
2117
+ </Form.Item>
2118
+ );
2119
+ }}
2120
+ </Form.Item>
2078
2121
  <Button
2079
2122
  type="text"
2080
2123
  danger
@@ -2123,6 +2166,7 @@ const InterfaceManagement = () => {
2123
2166
  )}
2124
2167
  </Form.List>
2125
2168
  </Form.Item>
2169
+ )}
2126
2170
 
2127
2171
  {/* 隐藏的表单字段用于存储响应数据 */}
2128
2172
  <Form.Item
@@ -221,7 +221,7 @@ const MockData = () => {
221
221
  };
222
222
 
223
223
  const viewInterfaces = (feature) => {
224
- // 导航到该功能的接口列表页面
224
+ localStorage.setItem('selectedFeatureId', feature.id);
225
225
  history.push(`/interface/${feature.id}`);
226
226
  };
227
227
 
@@ -1,33 +1,49 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import AppLayout from '../components/AppLayout';
3
3
  import { Card, Typography, Switch, Space, Divider } from 'antd';
4
+ import axios from 'axios';
4
5
  import '../styles/settings.css';
5
6
 
6
7
  const { Title, Text, Paragraph } = Typography;
7
8
 
9
+ const syncSettingsToBackend = (patch) => {
10
+ axios.put('/cgi-bin/settings', patch).catch(() => {});
11
+ };
12
+
8
13
  const Settings = () => {
9
14
  const [displayMode, setDisplayMode] = useState('modal');
10
15
  const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
16
+ const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
17
+ const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
11
18
 
12
19
  useEffect(() => {
13
- // 从 localStorage 读取配置
14
20
  const savedMode = localStorage.getItem('dataManagementDisplayMode');
15
- if (savedMode) {
16
- setDisplayMode(savedMode);
17
- }
21
+ if (savedMode) setDisplayMode(savedMode);
18
22
 
19
23
  const savedInterfaceFormMode = localStorage.getItem('interfaceFormDisplayMode');
20
- if (savedInterfaceFormMode) {
21
- setInterfaceFormDisplayMode(savedInterfaceFormMode);
22
- }
24
+ if (savedInterfaceFormMode) setInterfaceFormDisplayMode(savedInterfaceFormMode);
25
+
26
+ // 优先从后端读取入参匹配开关,保持前后端一致
27
+ axios.get('/cgi-bin/settings').then(({ data }) => {
28
+ const ipm = data.interfaceParamMatcherEnabled !== false;
29
+ const rpm = data.responseParamMatcherEnabled !== false;
30
+ setInterfaceParamMatcherEnabled(ipm);
31
+ setResponseParamMatcherEnabled(rpm);
32
+ localStorage.setItem('interfaceParamMatcherEnabled', String(ipm));
33
+ localStorage.setItem('responseParamMatcherEnabled', String(rpm));
34
+ window.dispatchEvent(new Event('storage'));
35
+ }).catch(() => {
36
+ const savedInterfaceParamMatcher = localStorage.getItem('interfaceParamMatcherEnabled');
37
+ if (savedInterfaceParamMatcher !== null) setInterfaceParamMatcherEnabled(savedInterfaceParamMatcher !== 'false');
38
+ const savedResponseParamMatcher = localStorage.getItem('responseParamMatcherEnabled');
39
+ if (savedResponseParamMatcher !== null) setResponseParamMatcherEnabled(savedResponseParamMatcher !== 'false');
40
+ });
23
41
  }, []);
24
42
 
25
43
  const handleDisplayModeChange = (checked) => {
26
44
  const newMode = checked ? 'drawer' : 'modal';
27
45
  setDisplayMode(newMode);
28
46
  localStorage.setItem('dataManagementDisplayMode', newMode);
29
-
30
- // 触发 storage 事件,通知其他页面
31
47
  window.dispatchEvent(new Event('storage'));
32
48
  };
33
49
 
@@ -35,8 +51,20 @@ const Settings = () => {
35
51
  const newMode = checked ? 'drawer' : 'modal';
36
52
  setInterfaceFormDisplayMode(newMode);
37
53
  localStorage.setItem('interfaceFormDisplayMode', newMode);
38
-
39
- // 触发 storage 事件,通知其他页面
54
+ window.dispatchEvent(new Event('storage'));
55
+ };
56
+
57
+ const handleInterfaceParamMatcherChange = (checked) => {
58
+ setInterfaceParamMatcherEnabled(checked);
59
+ localStorage.setItem('interfaceParamMatcherEnabled', String(checked));
60
+ syncSettingsToBackend({ interfaceParamMatcherEnabled: checked });
61
+ window.dispatchEvent(new Event('storage'));
62
+ };
63
+
64
+ const handleResponseParamMatcherChange = (checked) => {
65
+ setResponseParamMatcherEnabled(checked);
66
+ localStorage.setItem('responseParamMatcherEnabled', String(checked));
67
+ syncSettingsToBackend({ responseParamMatcherEnabled: checked });
40
68
  window.dispatchEvent(new Event('storage'));
41
69
  };
42
70
 
@@ -92,6 +120,46 @@ const Settings = () => {
92
120
  </Space>
93
121
  </div>
94
122
  </div>
123
+
124
+ <div className="settings-item">
125
+ <div className="settings-item-header">
126
+ <div>
127
+ <div className="settings-item-title">接口入参匹配</div>
128
+ <div className="settings-item-description">
129
+ 是否在接口编辑表单中显示「接口匹配条件」配置区域
130
+ </div>
131
+ </div>
132
+ <Space>
133
+ <Text type="secondary">
134
+ {interfaceParamMatcherEnabled ? '已开启' : '已关闭'}
135
+ </Text>
136
+ <Switch
137
+ checked={interfaceParamMatcherEnabled}
138
+ onChange={handleInterfaceParamMatcherChange}
139
+ />
140
+ </Space>
141
+ </div>
142
+ </div>
143
+
144
+ <div className="settings-item">
145
+ <div className="settings-item-header">
146
+ <div>
147
+ <div className="settings-item-title">响应入参匹配</div>
148
+ <div className="settings-item-description">
149
+ 是否在响应内容编辑器中显示「入参匹配规则」配置区域
150
+ </div>
151
+ </div>
152
+ <Space>
153
+ <Text type="secondary">
154
+ {responseParamMatcherEnabled ? '已开启' : '已关闭'}
155
+ </Text>
156
+ <Switch
157
+ checked={responseParamMatcherEnabled}
158
+ onChange={handleResponseParamMatcherChange}
159
+ />
160
+ </Space>
161
+ </div>
162
+ </div>
95
163
  </Card>
96
164
  </div>
97
165
  </AppLayout>