whistle.mock-plugins 1.2.2-beta.23 → 1.2.2-beta.28
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 +78 -2
- package/app/cgi-bin/proxy-fetch.js +117 -0
- package/app/cgi-bin/stats.js +149 -0
- package/app/public/index.html +1 -1
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +18 -0
- package/lib/ruleManager.js +27 -0
- package/lib/ui-server.js +20 -4
- package/package.json +1 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +213 -79
- package/ui/src/components/FeatureCard.js +1 -1
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
- package/ui/src/components/interface-management/PreviewModal.js +4 -6
- package/ui/src/components/interface-management/RealResponseRecorder.js +165 -0
- package/ui/src/components/interface-management/ResponseContentEditor.js +4 -7
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +9 -2
- package/ui/src/pages/InterfaceManagementV2.js +29 -6
- package/ui/src/pages/MockDataV2.js +20 -12
- package/ui/src/pages/Settings.js +35 -1
- package/ui/src/pages/Sniffer.js +521 -0
- package/ui/src/pages/Stats.js +288 -0
- package/ui/src/styles/cyber-theme.css +1234 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, Input, message, Space, Tabs, Tag } from 'antd';
|
|
3
|
+
import { DownloadOutlined, ThunderboltOutlined, CopyOutlined } from '@ant-design/icons';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
|
|
6
|
+
const { TextArea } = Input;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* JSON 转 Mock.js 模板
|
|
10
|
+
* 简单转换:字符串 -> '@string', 数字 -> '@integer(1,100)', 布尔 -> '@boolean'
|
|
11
|
+
* 保留对象结构
|
|
12
|
+
*/
|
|
13
|
+
const jsonToMockTemplate = (obj, depth = 0) => {
|
|
14
|
+
if (obj === null) return null;
|
|
15
|
+
if (typeof obj === 'string') {
|
|
16
|
+
// 保留特定格式字符串的提示
|
|
17
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(obj)) return '@date';
|
|
18
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(obj)) return '@datetime';
|
|
19
|
+
if (/^https?:\/\//.test(obj)) return '@url';
|
|
20
|
+
if (/^\w+@\w+\.\w+/.test(obj)) return '@email';
|
|
21
|
+
if (/^1[3-9]\d{9}$/.test(obj)) return '@string("number", 11)';
|
|
22
|
+
return '@csentence(3, 10)';
|
|
23
|
+
}
|
|
24
|
+
if (typeof obj === 'number') {
|
|
25
|
+
if (Number.isInteger(obj)) return '@integer(1, 999)';
|
|
26
|
+
return '@float(1, 100, 2, 2)';
|
|
27
|
+
}
|
|
28
|
+
if (typeof obj === 'boolean') return '@boolean';
|
|
29
|
+
if (Array.isArray(obj)) {
|
|
30
|
+
if (obj.length === 0) return [];
|
|
31
|
+
const template = jsonToMockTemplate(obj[0], depth + 1);
|
|
32
|
+
// 用 |1-5 表示数组长度随机
|
|
33
|
+
return [template];
|
|
34
|
+
}
|
|
35
|
+
if (typeof obj === 'object') {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
38
|
+
result[key] = jsonToMockTemplate(value, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
return obj;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const RealResponseRecorder = ({ urlPattern, onApply }) => {
|
|
46
|
+
const [fetchUrl, setFetchUrl] = useState('');
|
|
47
|
+
const [loading, setLoading] = useState(false);
|
|
48
|
+
const [response, setResponse] = useState(null);
|
|
49
|
+
const [activeTab, setActiveTab] = useState('preview');
|
|
50
|
+
|
|
51
|
+
const handleFetch = async () => {
|
|
52
|
+
const targetUrl = fetchUrl.trim() || urlPattern;
|
|
53
|
+
if (!targetUrl) {
|
|
54
|
+
message.warning('请输入目标 URL');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setLoading(true);
|
|
58
|
+
try {
|
|
59
|
+
const res = await axios.post('/cgi-bin/proxy-fetch', {
|
|
60
|
+
url: targetUrl,
|
|
61
|
+
method: 'GET',
|
|
62
|
+
});
|
|
63
|
+
if (res.data.code === 0) {
|
|
64
|
+
setResponse(res.data.data);
|
|
65
|
+
setActiveTab('preview');
|
|
66
|
+
message.success('抓取成功');
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(res.data.message);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
message.error('抓取失败: ' + (err.response?.data?.message || err.message));
|
|
72
|
+
} finally {
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleGenerateMock = () => {
|
|
78
|
+
if (!response?.body) return;
|
|
79
|
+
try {
|
|
80
|
+
const json = JSON.parse(response.body);
|
|
81
|
+
const template = jsonToMockTemplate(json);
|
|
82
|
+
const mockTemplate = JSON.stringify(template, null, 2);
|
|
83
|
+
onApply?.(mockTemplate);
|
|
84
|
+
message.success('Mock 模板已生成');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
message.error('响应不是 JSON,无法生成模板: ' + e.message);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleCopy = () => {
|
|
91
|
+
if (response?.body) {
|
|
92
|
+
navigator.clipboard.writeText(response.body);
|
|
93
|
+
message.success('已复制');
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div style={{ padding: '16px 0' }}>
|
|
99
|
+
<Space direction="vertical" style={{ width: '100%' }}>
|
|
100
|
+
<Space style={{ width: '100%' }}>
|
|
101
|
+
<Input
|
|
102
|
+
placeholder={urlPattern || '输入真实接口 URL'}
|
|
103
|
+
value={fetchUrl}
|
|
104
|
+
onChange={(e) => setFetchUrl(e.target.value)}
|
|
105
|
+
style={{ width: 400 }}
|
|
106
|
+
onPressEnter={handleFetch}
|
|
107
|
+
/>
|
|
108
|
+
<Button
|
|
109
|
+
type="primary"
|
|
110
|
+
icon={<ThunderboltOutlined />}
|
|
111
|
+
loading={loading}
|
|
112
|
+
onClick={handleFetch}
|
|
113
|
+
>
|
|
114
|
+
抓取真实响应
|
|
115
|
+
</Button>
|
|
116
|
+
</Space>
|
|
117
|
+
|
|
118
|
+
{response && (
|
|
119
|
+
<>
|
|
120
|
+
<Space>
|
|
121
|
+
<Tag color={response.status >= 200 && response.status < 300 ? 'success' : 'error'}>
|
|
122
|
+
{response.status} {response.statusText}
|
|
123
|
+
</Tag>
|
|
124
|
+
<Button size="small" icon={<CopyOutlined />} onClick={handleCopy}>
|
|
125
|
+
复制原文
|
|
126
|
+
</Button>
|
|
127
|
+
<Button size="small" icon={<DownloadOutlined />} onClick={handleGenerateMock} type="primary">
|
|
128
|
+
生成 Mock 模板
|
|
129
|
+
</Button>
|
|
130
|
+
</Space>
|
|
131
|
+
|
|
132
|
+
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
|
133
|
+
<Tabs.TabPane tab="响应预览" key="preview">
|
|
134
|
+
<TextArea
|
|
135
|
+
value={response.body}
|
|
136
|
+
readOnly
|
|
137
|
+
rows={12}
|
|
138
|
+
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
|
139
|
+
/>
|
|
140
|
+
</Tabs.TabPane>
|
|
141
|
+
<Tabs.TabPane tab="生成的 Mock 模板" key="mock">
|
|
142
|
+
<TextArea
|
|
143
|
+
value={(() => {
|
|
144
|
+
try {
|
|
145
|
+
const json = JSON.parse(response.body);
|
|
146
|
+
const template = jsonToMockTemplate(json);
|
|
147
|
+
return JSON.stringify(template, null, 2);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return '// 响应不是 JSON,无法生成模板\n' + response.body;
|
|
150
|
+
}
|
|
151
|
+
})()}
|
|
152
|
+
readOnly
|
|
153
|
+
rows={12}
|
|
154
|
+
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
|
155
|
+
/>
|
|
156
|
+
</Tabs.TabPane>
|
|
157
|
+
</Tabs>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</Space>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export default RealResponseRecorder;
|
|
@@ -274,7 +274,7 @@ const ResponseContentEditor = ({
|
|
|
274
274
|
onClick={toggleTextareaFullscreen}
|
|
275
275
|
size="small"
|
|
276
276
|
type="text"
|
|
277
|
-
style={{ color: '#
|
|
277
|
+
style={{ color: '#595959' }}
|
|
278
278
|
>
|
|
279
279
|
退出全屏 (ESC)
|
|
280
280
|
</Button>
|
|
@@ -301,7 +301,7 @@ const ResponseContentEditor = ({
|
|
|
301
301
|
<div style={{ marginBottom: '12px' }}>
|
|
302
302
|
<Divider orientation="left" orientationMargin={0} style={{ margin: '0 0 10px 0', fontSize: '12px' }}>
|
|
303
303
|
<Space size={6}>
|
|
304
|
-
<FilterOutlined style={{ color: '#
|
|
304
|
+
<FilterOutlined style={{ color: '#1677ff' }} />
|
|
305
305
|
<span style={{ fontWeight: 'bold', color: '#333' }}>入参匹配规则</span>
|
|
306
306
|
{matchers.length > 0 && (
|
|
307
307
|
<Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>{matchers.length}条</Tag>
|
|
@@ -382,11 +382,10 @@ const ResponseContentEditor = ({
|
|
|
382
382
|
<div style={{
|
|
383
383
|
textAlign: 'center',
|
|
384
384
|
padding: '40px 20px',
|
|
385
|
-
background: '#fafafa',
|
|
386
385
|
borderRadius: '6px',
|
|
387
386
|
border: '1px dashed #d9d9d9'
|
|
388
387
|
}}>
|
|
389
|
-
<FileTextOutlined style={{ fontSize: '32px', color: '#
|
|
388
|
+
<FileTextOutlined style={{ fontSize: '32px', color: '#bfbfbf', marginBottom: '12px' }} />
|
|
390
389
|
<div style={{ color: '#999', marginBottom: '16px' }}>暂无响应数据</div>
|
|
391
390
|
<Button
|
|
392
391
|
type="primary"
|
|
@@ -433,9 +432,7 @@ const ResponseContentEditor = ({
|
|
|
433
432
|
style={{
|
|
434
433
|
display: 'flex',
|
|
435
434
|
alignItems: 'center',
|
|
436
|
-
gap: '4px'
|
|
437
|
-
borderColor: '#1890ff',
|
|
438
|
-
color: '#1890ff'
|
|
435
|
+
gap: '4px'
|
|
439
436
|
}}
|
|
440
437
|
>
|
|
441
438
|
添加响应
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
import { useHistory } from 'react-router-dom';
|
|
3
|
+
import { message } from 'antd';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 全局键盘快捷键 Hook
|
|
7
|
+
*
|
|
8
|
+
* 支持的快捷键:
|
|
9
|
+
* - Esc: 关闭当前弹窗/抽屉(触发全局 close-modal 事件)
|
|
10
|
+
* - Ctrl/Cmd + S: 保存(触发全局 save-form 事件)
|
|
11
|
+
* - Ctrl/Cmd + 1: 跳转到功能模块
|
|
12
|
+
* - Ctrl/Cmd + 2: 跳转到接口管理
|
|
13
|
+
* - Ctrl/Cmd + 3: 跳转到系统设置
|
|
14
|
+
* - Ctrl/Cmd + 4: 跳转到请求统计
|
|
15
|
+
* - Ctrl/Cmd + 5: 跳转到接口嗅探
|
|
16
|
+
* - Ctrl/Cmd + Shift + N: 新建接口(在接口管理页触发)
|
|
17
|
+
* - Ctrl/Cmd + K: 聚焦搜索框(触发全局 focus-search 事件)
|
|
18
|
+
* - Shift + ?: 显示快捷键帮助
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SHORTCUTS = [
|
|
22
|
+
{ key: 'Esc', desc: '关闭弹窗/抽屉' },
|
|
23
|
+
{ key: 'Ctrl + S', desc: '保存表单' },
|
|
24
|
+
{ key: 'Ctrl + 1', desc: '功能模块' },
|
|
25
|
+
{ key: 'Ctrl + 2', desc: '接口管理' },
|
|
26
|
+
{ key: 'Ctrl + 3', desc: '系统设置' },
|
|
27
|
+
{ key: 'Ctrl + 4', desc: '请求统计' },
|
|
28
|
+
{ key: 'Ctrl + 5', desc: '接口嗅探' },
|
|
29
|
+
{ key: 'Ctrl + Shift + N', desc: '新建接口' },
|
|
30
|
+
{ key: 'Ctrl + K', desc: '聚焦搜索' },
|
|
31
|
+
{ key: 'Shift + ?', desc: '快捷键帮助' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const getShortcutsList = () => SHORTCUTS;
|
|
35
|
+
|
|
36
|
+
export const useKeyboardShortcuts = () => {
|
|
37
|
+
const history = useHistory();
|
|
38
|
+
|
|
39
|
+
const handleKeyDown = useCallback((e) => {
|
|
40
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
41
|
+
const ctrlOrCmd = isMac ? e.metaKey : e.ctrlKey;
|
|
42
|
+
|
|
43
|
+
// 忽略在输入框、文本域中的快捷键(导航类除外)
|
|
44
|
+
const tagName = e.target?.tagName?.toLowerCase();
|
|
45
|
+
const isInput = tagName === 'input' || tagName === 'textarea' || e.target?.isContentEditable;
|
|
46
|
+
|
|
47
|
+
// Shift + ? 显示帮助(非输入框时)
|
|
48
|
+
if (e.shiftKey && e.key === '?' && !isInput) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
window.dispatchEvent(new CustomEvent('keyboard-shortcuts-help'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Esc 关闭弹窗/抽屉
|
|
55
|
+
if (e.key === 'Escape') {
|
|
56
|
+
window.dispatchEvent(new CustomEvent('shortcut-esc'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Ctrl/Cmd + 数字键 导航
|
|
61
|
+
if (ctrlOrCmd && !e.shiftKey && /^[1-5]$/.test(e.key)) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const routes = ['/', '/interface', '/settings', '/stats', '/sniffer'];
|
|
64
|
+
const names = ['功能模块', '接口管理', '系统设置', '请求统计', '接口嗅探'];
|
|
65
|
+
const idx = parseInt(e.key, 10) - 1;
|
|
66
|
+
history.push(routes[idx]);
|
|
67
|
+
message.success(`已切换到${names[idx]}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Ctrl/Cmd + S 保存
|
|
72
|
+
if (ctrlOrCmd && e.key.toLowerCase() === 's' && !e.shiftKey) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
window.dispatchEvent(new CustomEvent('shortcut-save'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Ctrl/Cmd + Shift + N 新建接口
|
|
79
|
+
if (ctrlOrCmd && e.shiftKey && e.key.toLowerCase() === 'n') {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
window.dispatchEvent(new CustomEvent('shortcut-new-interface'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ctrl/Cmd + K 聚焦搜索
|
|
86
|
+
if (ctrlOrCmd && e.key.toLowerCase() === 'k' && !e.shiftKey) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
window.dispatchEvent(new CustomEvent('shortcut-focus-search'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}, [history]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
95
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
96
|
+
}, [handleKeyDown]);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default useKeyboardShortcuts;
|
package/ui/src/index.html
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Whistle Mock
|
|
6
|
+
<title>Whistle Mock Plugin</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<div id="root"></div>
|
|
10
13
|
</body>
|
|
11
|
-
</html>
|
|
14
|
+
</html>
|
package/ui/src/index.js
CHANGED
|
@@ -3,14 +3,19 @@ import ReactDOM from 'react-dom';
|
|
|
3
3
|
import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
|
|
4
4
|
|
|
5
5
|
// 导入样式
|
|
6
|
-
import 'antd/dist/reset.css';
|
|
6
|
+
import 'antd/dist/reset.css';
|
|
7
7
|
import './styles/global.css';
|
|
8
8
|
import './App.css';
|
|
9
9
|
|
|
10
|
+
// 赛博朋克主题 CSS(规则均带 body.cyber-theme 前缀,默认不生效)
|
|
11
|
+
require('./styles/cyber-theme.css');
|
|
12
|
+
|
|
10
13
|
// 导入页面组件
|
|
11
14
|
import MockData from './pages/MockDataV2';
|
|
12
15
|
import InterfaceManagement from './pages/InterfaceManagementV2';
|
|
13
16
|
import Settings from './pages/Settings';
|
|
17
|
+
import Stats from './pages/Stats';
|
|
18
|
+
import Sniffer from './pages/Sniffer';
|
|
14
19
|
|
|
15
20
|
const App = () => {
|
|
16
21
|
return (
|
|
@@ -20,6 +25,8 @@ const App = () => {
|
|
|
20
25
|
<Route exact path="/" component={MockData} />
|
|
21
26
|
<Route exact path="/interface" component={InterfaceManagement} />
|
|
22
27
|
<Route path="/interface/:featureId" component={InterfaceManagement} />
|
|
28
|
+
<Route path="/stats" component={Stats} />
|
|
29
|
+
<Route path="/sniffer" component={Sniffer} />
|
|
23
30
|
<Route path="/settings" component={Settings} />
|
|
24
31
|
<Redirect to="/" />
|
|
25
32
|
</Switch>
|
|
@@ -37,4 +44,4 @@ if (ReactDOM.createRoot) {
|
|
|
37
44
|
} else {
|
|
38
45
|
// 旧版本React
|
|
39
46
|
ReactDOM.render(<App />, root);
|
|
40
|
-
}
|
|
47
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* InterfaceManagementV2.js - 接口管理页面
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React, { useState, useEffect } from 'react';
|
|
5
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
6
6
|
import { useParams, useHistory } from 'react-router-dom';
|
|
7
7
|
import AppLayout from '../components/AppLayout';
|
|
8
8
|
import {
|
|
@@ -87,6 +87,7 @@ const InterfaceManagement = () => {
|
|
|
87
87
|
const [currentResponseId, setCurrentResponseId] = useState(null);
|
|
88
88
|
|
|
89
89
|
// 搜索状态
|
|
90
|
+
const searchRef = useRef(null);
|
|
90
91
|
const [searchValue, setSearchValue] = useState('');
|
|
91
92
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
|
92
93
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
@@ -136,6 +137,29 @@ const InterfaceManagement = () => {
|
|
|
136
137
|
}
|
|
137
138
|
}, [selectedFeatureId]);
|
|
138
139
|
|
|
140
|
+
// 键盘快捷键监听
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const handleSave = () => {
|
|
143
|
+
if (modalVisible) {
|
|
144
|
+
handleSubmit();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const handleNew = () => {
|
|
148
|
+
handleAddInterface();
|
|
149
|
+
};
|
|
150
|
+
const handleFocusSearch = () => {
|
|
151
|
+
searchRef.current?.focus();
|
|
152
|
+
};
|
|
153
|
+
window.addEventListener('shortcut-save', handleSave);
|
|
154
|
+
window.addEventListener('shortcut-new-interface', handleNew);
|
|
155
|
+
window.addEventListener('shortcut-focus-search', handleFocusSearch);
|
|
156
|
+
return () => {
|
|
157
|
+
window.removeEventListener('shortcut-save', handleSave);
|
|
158
|
+
window.removeEventListener('shortcut-new-interface', handleNew);
|
|
159
|
+
window.removeEventListener('shortcut-focus-search', handleFocusSearch);
|
|
160
|
+
};
|
|
161
|
+
}, [modalVisible]);
|
|
162
|
+
|
|
139
163
|
// 监听数据管理显示模式变化
|
|
140
164
|
useEffect(() => {
|
|
141
165
|
const savedMode = localStorage.getItem('dataManagementDisplayMode');
|
|
@@ -1012,7 +1036,7 @@ const InterfaceManagement = () => {
|
|
|
1012
1036
|
))}
|
|
1013
1037
|
</div>
|
|
1014
1038
|
}>
|
|
1015
|
-
<span style={{ color: '#
|
|
1039
|
+
<span style={{ color: '#1677ff' }}>
|
|
1016
1040
|
{count}个
|
|
1017
1041
|
{hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
|
|
1018
1042
|
</span>
|
|
@@ -1067,7 +1091,7 @@ const InterfaceManagement = () => {
|
|
|
1067
1091
|
)}
|
|
1068
1092
|
{responsesWithMatchers.length > 0 && (
|
|
1069
1093
|
<div>
|
|
1070
|
-
<div style={{ fontWeight: 'bold', marginBottom: 4, color: '#
|
|
1094
|
+
<div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1677ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
|
|
1071
1095
|
{responsesWithMatchers.map((resp, ri) => (
|
|
1072
1096
|
<div key={ri} style={{ marginBottom: 4 }}>
|
|
1073
1097
|
<div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
|
|
@@ -1231,6 +1255,7 @@ const InterfaceManagement = () => {
|
|
|
1231
1255
|
</div>
|
|
1232
1256
|
|
|
1233
1257
|
<Search
|
|
1258
|
+
ref={searchRef}
|
|
1234
1259
|
placeholder="搜索接口名称或URL"
|
|
1235
1260
|
onSearch={handleSearch}
|
|
1236
1261
|
value={searchValue}
|
|
@@ -1500,7 +1525,6 @@ const InterfaceManagement = () => {
|
|
|
1500
1525
|
destroyOnClose
|
|
1501
1526
|
bodyStyle={{
|
|
1502
1527
|
padding: '0',
|
|
1503
|
-
background: '#fafafa',
|
|
1504
1528
|
overflowY: 'auto'
|
|
1505
1529
|
}}
|
|
1506
1530
|
footer={
|
|
@@ -1744,8 +1768,7 @@ const InterfaceManagement = () => {
|
|
|
1744
1768
|
bodyStyle={{
|
|
1745
1769
|
maxHeight: '75vh',
|
|
1746
1770
|
overflow: 'auto',
|
|
1747
|
-
padding: '0'
|
|
1748
|
-
background: '#fafafa'
|
|
1771
|
+
padding: '0'
|
|
1749
1772
|
}}
|
|
1750
1773
|
className="interface-modal"
|
|
1751
1774
|
>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import React, { useState, useEffect } from 'react';
|
|
6
|
+
import { useRef } from 'react';
|
|
6
7
|
import { useHistory } from 'react-router-dom';
|
|
7
8
|
import AppLayout from '../components/AppLayout';
|
|
8
9
|
import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag } from 'antd';
|
|
@@ -57,6 +58,24 @@ const MockData = () => {
|
|
|
57
58
|
fetchFeatures();
|
|
58
59
|
}, []);
|
|
59
60
|
|
|
61
|
+
// 键盘快捷键监听
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const handleSave = () => {
|
|
64
|
+
if (showModal) {
|
|
65
|
+
handleSubmit();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const handleNew = () => {
|
|
69
|
+
openModal();
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener('shortcut-save', handleSave);
|
|
72
|
+
window.addEventListener('shortcut-new-interface', handleNew);
|
|
73
|
+
return () => {
|
|
74
|
+
window.removeEventListener('shortcut-save', handleSave);
|
|
75
|
+
window.removeEventListener('shortcut-new-interface', handleNew);
|
|
76
|
+
};
|
|
77
|
+
}, [showModal]);
|
|
78
|
+
|
|
60
79
|
// 获取所有功能模块
|
|
61
80
|
const fetchFeatures = async () => {
|
|
62
81
|
try {
|
|
@@ -566,7 +585,7 @@ const MockData = () => {
|
|
|
566
585
|
width: 100,
|
|
567
586
|
align: 'center',
|
|
568
587
|
render: (count) => (
|
|
569
|
-
<Badge count={count || 0} showZero style={{ backgroundColor: '#
|
|
588
|
+
<Badge count={count || 0} showZero style={{ backgroundColor: '#1677ff' }} />
|
|
570
589
|
),
|
|
571
590
|
},
|
|
572
591
|
{
|
|
@@ -661,17 +680,6 @@ const MockData = () => {
|
|
|
661
680
|
<div className="title-section">
|
|
662
681
|
<div className="title-with-badge">
|
|
663
682
|
<h1 className="page-title">功能模块管理</h1>
|
|
664
|
-
<Badge
|
|
665
|
-
count="V2"
|
|
666
|
-
style={{
|
|
667
|
-
backgroundColor: '#52c41a',
|
|
668
|
-
fontSize: '12px',
|
|
669
|
-
height: '20px',
|
|
670
|
-
lineHeight: '20px',
|
|
671
|
-
borderRadius: '10px',
|
|
672
|
-
marginLeft: '12px'
|
|
673
|
-
}}
|
|
674
|
-
/>
|
|
675
683
|
</div>
|
|
676
684
|
<div className="page-description">
|
|
677
685
|
创建和管理功能模块,为每个功能配置独立的接口
|
package/ui/src/pages/Settings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import AppLayout from '../components/AppLayout';
|
|
3
|
-
import { Card, Typography, Switch, Space, Modal, Button } from 'antd';
|
|
3
|
+
import { Card, Typography, Switch, Space, Modal, Button, message } from 'antd';
|
|
4
4
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import '../styles/settings.css';
|
|
@@ -16,6 +16,7 @@ const Settings = () => {
|
|
|
16
16
|
const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
|
|
17
17
|
const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
|
|
18
18
|
const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
|
|
19
|
+
const [cyberThemeEnabled, setCyberThemeEnabled] = useState(true);
|
|
19
20
|
|
|
20
21
|
useEffect(() => {
|
|
21
22
|
const savedMode = localStorage.getItem('dataManagementDisplayMode');
|
|
@@ -39,6 +40,10 @@ const Settings = () => {
|
|
|
39
40
|
const savedResponseParamMatcher = localStorage.getItem('responseParamMatcherEnabled');
|
|
40
41
|
if (savedResponseParamMatcher !== null) setResponseParamMatcherEnabled(savedResponseParamMatcher !== 'false');
|
|
41
42
|
});
|
|
43
|
+
|
|
44
|
+
// 读取赛博朋克主题开关(默认开启)
|
|
45
|
+
const savedCyber = localStorage.getItem('cyberTheme');
|
|
46
|
+
setCyberThemeEnabled(savedCyber !== 'false');
|
|
42
47
|
}, []);
|
|
43
48
|
|
|
44
49
|
const handleDisplayModeChange = (checked) => {
|
|
@@ -69,6 +74,13 @@ const Settings = () => {
|
|
|
69
74
|
window.dispatchEvent(new Event('storage'));
|
|
70
75
|
};
|
|
71
76
|
|
|
77
|
+
const handleCyberThemeChange = (checked) => {
|
|
78
|
+
setCyberThemeEnabled(checked);
|
|
79
|
+
localStorage.setItem('cyberTheme', String(checked));
|
|
80
|
+
window.dispatchEvent(new Event('storage'));
|
|
81
|
+
message.success(checked ? '赛博朋克主题已开启' : '经典主题已开启');
|
|
82
|
+
};
|
|
83
|
+
|
|
72
84
|
const showMatcherRuleModal = (type) => {
|
|
73
85
|
if (type === 'interface') {
|
|
74
86
|
Modal.info({
|
|
@@ -229,6 +241,28 @@ const Settings = () => {
|
|
|
229
241
|
</div>
|
|
230
242
|
</div>
|
|
231
243
|
</Card>
|
|
244
|
+
|
|
245
|
+
<Card title="主题设置" className="settings-card" style={{ marginTop: 24 }}>
|
|
246
|
+
<div className="settings-item">
|
|
247
|
+
<div className="settings-item-header">
|
|
248
|
+
<div>
|
|
249
|
+
<div className="settings-item-title">赛博朋克风格</div>
|
|
250
|
+
<div className="settings-item-description">
|
|
251
|
+
启用深色赛博朋克霓虹主题,关闭则恢复经典亮色主题
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
<Space>
|
|
255
|
+
<Text type="secondary">
|
|
256
|
+
{cyberThemeEnabled ? '已开启' : '已关闭'}
|
|
257
|
+
</Text>
|
|
258
|
+
<Switch
|
|
259
|
+
checked={cyberThemeEnabled}
|
|
260
|
+
onChange={handleCyberThemeChange}
|
|
261
|
+
/>
|
|
262
|
+
</Space>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</Card>
|
|
232
266
|
</div>
|
|
233
267
|
</AppLayout>
|
|
234
268
|
);
|