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.
- package/VERSION.md +28 -0
- package/app/cgi-bin/settings.js +37 -0
- package/app/public/js/index.js +1 -1
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +80 -31
- package/lib/server.js +4 -0
- package/package.json +1 -1
- package/ui/src/components/interface-management/ResponseContentEditor.js +21 -45
- package/ui/src/pages/InterfaceManagementV2.js +72 -28
- package/ui/src/pages/MockDataV2.js +1 -1
- package/ui/src/pages/Settings.js +79 -11
|
@@ -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 };
|
package/lib/ruleManager.js
CHANGED
|
@@ -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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
//
|
|
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
|
@@ -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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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}
|
|
890
|
-
|
|
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 =
|
|
1031
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
package/ui/src/pages/Settings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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>
|