whistle.mock-plugins 1.2.2-beta.9 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COLUMN_RESIZE_IMPLEMENTATION.md +162 -0
- package/TEST_GUIDE.md +156 -0
- package/VERSION.md +57 -3
- package/app/cgi-bin/proxy-fetch.js +117 -0
- package/app/cgi-bin/settings.js +37 -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 +25 -2
- package/lib/models.js +9 -0
- package/lib/routes/interfaces.js +6 -3
- package/lib/routes/settings.js +51 -0
- package/lib/ruleManager.js +122 -63
- package/lib/server.js +4 -0
- package/lib/ui-server.js +20 -4
- package/package.json +2 -1
- package/ui/src/App.css +1 -1
- package/ui/src/components/AppLayout.js +317 -80
- package/ui/src/components/FeatureCard.js +7 -2
- package/ui/src/components/KeyboardShortcutsHelp.js +58 -0
- package/ui/src/components/VersionModal.js +4 -41
- package/ui/src/components/interface-management/DataManagementDrawer.js +115 -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 +123 -177
- package/ui/src/components/interface-management/index.js +1 -0
- package/ui/src/hooks/useKeyboardShortcuts.js +99 -0
- package/ui/src/index.html +5 -2
- package/ui/src/index.js +12 -2
- package/ui/src/pages/InterfaceManagementV2.js +563 -147
- package/ui/src/pages/MockDataV2.js +21 -13
- package/ui/src/pages/Settings.js +275 -8
- 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
- package/ui/src/styles/earth-theme.css +618 -0
- package/ui/src/styles/interface-management.css +37 -1
- package/ui/src/styles/settings.css +45 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Drawer, Form, Input, message, Button, Space } from 'antd';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { ResponseContentEditor, PreviewModal, formatResponseContent, refreshCacheAfterUpdate } from '.';
|
|
5
|
+
|
|
6
|
+
const DataManagementDrawer = ({ visible, onCancel, interfaceItem, onSaved }) => {
|
|
7
|
+
const [form] = Form.useForm();
|
|
8
|
+
const [previewVisible, setPreviewVisible] = useState(false);
|
|
9
|
+
const [previewContent, setPreviewContent] = useState(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (visible && interfaceItem) {
|
|
13
|
+
let responses = Array.isArray(interfaceItem.responses) ? interfaceItem.responses : [];
|
|
14
|
+
let activeResponseId = interfaceItem.activeResponseId || (responses[0] && responses[0].id) || '';
|
|
15
|
+
form.setFieldsValue({ responses, activeResponseId });
|
|
16
|
+
}
|
|
17
|
+
}, [visible, interfaceItem]);
|
|
18
|
+
|
|
19
|
+
const handlePreview = () => {
|
|
20
|
+
const values = form.getFieldsValue(true);
|
|
21
|
+
const responses = Array.isArray(values.responses) ? values.responses : [];
|
|
22
|
+
const activeResponse = responses.find(r => r.id === values.activeResponseId) || responses[0];
|
|
23
|
+
if (!activeResponse) {
|
|
24
|
+
message.error('未找到有效的响应内容');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const contentType = 'application/json; charset=utf-8';
|
|
28
|
+
const formatted = formatResponseContent(activeResponse.content || '', contentType);
|
|
29
|
+
setPreviewContent({ title: `预览: ${activeResponse.name || '未命名响应'}`, content: formatted, contentType, description: '' });
|
|
30
|
+
setPreviewVisible(true);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleOk = async () => {
|
|
34
|
+
try {
|
|
35
|
+
await form.validateFields();
|
|
36
|
+
const values = form.getFieldsValue(true);
|
|
37
|
+
const payload = {
|
|
38
|
+
responses: Array.isArray(values.responses) ? values.responses : [],
|
|
39
|
+
activeResponseId: values.activeResponseId || (values.responses && values.responses[0] && values.responses[0].id) || ''
|
|
40
|
+
};
|
|
41
|
+
if (!interfaceItem || !interfaceItem.id) {
|
|
42
|
+
message.error('缺少接口ID,无法保存');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const res = await axios.patch(`/cgi-bin/interfaces/${interfaceItem.id}`, payload);
|
|
46
|
+
if (res.data && res.data.code === 0) {
|
|
47
|
+
message.success('保存成功');
|
|
48
|
+
await refreshCacheAfterUpdate();
|
|
49
|
+
if (onSaved) onSaved();
|
|
50
|
+
if (onCancel) onCancel();
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(res.data?.message || '保存失败');
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err && err.errorFields) return; // 表单校验错误
|
|
56
|
+
console.error('保存失败', err);
|
|
57
|
+
message.error(err.response?.data?.message || err.message || '保存失败');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<Drawer
|
|
64
|
+
title="数据管理"
|
|
65
|
+
placement="right"
|
|
66
|
+
width={1040}
|
|
67
|
+
open={visible}
|
|
68
|
+
onClose={onCancel}
|
|
69
|
+
destroyOnClose
|
|
70
|
+
bodyStyle={{ paddingBottom: 80 }}
|
|
71
|
+
footer={
|
|
72
|
+
<div style={{ textAlign: 'right' }}>
|
|
73
|
+
<Space>
|
|
74
|
+
<Button onClick={onCancel}>取消</Button>
|
|
75
|
+
<Button type="primary" onClick={handleOk}>保存</Button>
|
|
76
|
+
</Space>
|
|
77
|
+
</div>
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<Form form={form} layout="vertical" initialValues={{ responses: [], activeResponseId: '' }}>
|
|
81
|
+
<Form.Item name="responses" hidden>
|
|
82
|
+
<Input />
|
|
83
|
+
</Form.Item>
|
|
84
|
+
<Form.Item name="activeResponseId" hidden>
|
|
85
|
+
<Input />
|
|
86
|
+
</Form.Item>
|
|
87
|
+
<Form.Item noStyle shouldUpdate>
|
|
88
|
+
{({ getFieldValue }) => {
|
|
89
|
+
const responses = getFieldValue('responses') || [];
|
|
90
|
+
const activeResponseId = getFieldValue('activeResponseId') || (responses[0] && responses[0].id) || '';
|
|
91
|
+
return (
|
|
92
|
+
<ResponseContentEditor
|
|
93
|
+
form={form}
|
|
94
|
+
responses={responses}
|
|
95
|
+
activeResponseId={activeResponseId}
|
|
96
|
+
onPreview={handlePreview}
|
|
97
|
+
enableSorting={true}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}}
|
|
101
|
+
</Form.Item>
|
|
102
|
+
</Form>
|
|
103
|
+
</Drawer>
|
|
104
|
+
|
|
105
|
+
<PreviewModal
|
|
106
|
+
visible={previewVisible}
|
|
107
|
+
onCancel={() => setPreviewVisible(false)}
|
|
108
|
+
previewContent={previewContent}
|
|
109
|
+
/>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default DataManagementDrawer;
|
|
115
|
+
|
|
@@ -22,22 +22,20 @@ const PreviewModal = ({
|
|
|
22
22
|
<div style={{
|
|
23
23
|
marginBottom: '16px',
|
|
24
24
|
padding: '8px',
|
|
25
|
-
background: '#f9f9f9',
|
|
26
25
|
borderRadius: '4px',
|
|
27
|
-
borderLeft: '4px solid #
|
|
26
|
+
borderLeft: '4px solid #00f0ff'
|
|
28
27
|
}}>
|
|
29
28
|
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>描述:</div>
|
|
30
|
-
<div>{previewContent.description}</div>
|
|
29
|
+
<div style={{ opacity: 0.7 }}>{previewContent.description}</div>
|
|
31
30
|
</div>
|
|
32
31
|
)}
|
|
33
32
|
|
|
34
33
|
<div style={{
|
|
35
34
|
maxHeight: '60vh',
|
|
36
35
|
overflow: 'auto',
|
|
37
|
-
backgroundColor: '#f5f5f5',
|
|
38
36
|
padding: '16px',
|
|
39
37
|
borderRadius: '4px',
|
|
40
|
-
fontFamily: 'monospace
|
|
38
|
+
fontFamily: "'Share Tech Mono', monospace",
|
|
41
39
|
}}>
|
|
42
40
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
|
43
41
|
{previewContent?.content}
|
|
@@ -47,4 +45,4 @@ const PreviewModal = ({
|
|
|
47
45
|
);
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export default PreviewModal;
|
|
48
|
+
export default PreviewModal;
|
|
@@ -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;
|