whistle.mock-plugins 1.0.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/API.md +247 -0
- package/README.md +91 -0
- package/app/cgi-bin/features.js +285 -0
- package/app/cgi-bin/index.js +31 -0
- package/app/cgi-bin/interfaces.js +409 -0
- package/app/cgi-bin/logs.js +151 -0
- package/app/cgi-bin/test-interface.js +152 -0
- package/app/public/index.html +1 -0
- package/app/public/js/index.js +2 -0
- package/app/public/js/index.js.LICENSE.txt +150 -0
- package/app/util/index.js +24 -0
- package/index.html +52 -0
- package/initialize.js +32 -0
- package/lib/dataManager.js +500 -0
- package/lib/index.js +3 -0
- package/lib/models.js +64 -0
- package/lib/routes/features.js +213 -0
- package/lib/routes/interfaces.js +304 -0
- package/lib/routes/proxy.js +239 -0
- package/lib/ruleManager.js +551 -0
- package/lib/rules-server.js +267 -0
- package/lib/server.js +805 -0
- package/lib/storage.js +16 -0
- package/lib/ui-server.js +68 -0
- package/package.json +51 -0
- package/public/bundle.js +2 -0
- package/public/bundle.js.LICENSE.txt +38 -0
- package/public/index.html +1 -0
- package/public/plugin.mock-plugin.html +14 -0
- package/rules.txt +22 -0
- package/test/rulesServer.test.js +120 -0
- package/ui/src/App.css +173 -0
- package/ui/src/App.js +24 -0
- package/ui/src/components/AppLayout.js +149 -0
- package/ui/src/components/FeatureCard.js +53 -0
- package/ui/src/components/Layout.js +94 -0
- package/ui/src/components/MainNav.js +42 -0
- package/ui/src/components/PageContainer.js +16 -0
- package/ui/src/index.html +11 -0
- package/ui/src/index.js +43 -0
- package/ui/src/pages/FileProxy.js +235 -0
- package/ui/src/pages/FilesPage.js +292 -0
- package/ui/src/pages/Home.js +128 -0
- package/ui/src/pages/HomePage.js +195 -0
- package/ui/src/pages/InterfaceManagement.js +829 -0
- package/ui/src/pages/InterfacePage.js +342 -0
- package/ui/src/pages/MockData.js +472 -0
- package/ui/src/pages/Settings.js +192 -0
- package/ui/src/pages/SettingsPage.js +165 -0
- package/ui/src/pages/UrlRedirect.js +225 -0
- package/ui/src/pages/Welcome.js +155 -0
- package/ui/src/services/api.js +186 -0
- package/ui/src/styles/app-layout.css +337 -0
- package/ui/src/styles/feature-card.css +172 -0
- package/ui/src/styles/file-proxy.css +301 -0
- package/ui/src/styles/global.css +224 -0
- package/ui/src/styles/home.css +123 -0
- package/ui/src/styles/index.css +419 -0
- package/ui/src/styles/interface-management.css +694 -0
- package/ui/src/styles/layout.css +159 -0
- package/ui/src/styles/logs-page.css +427 -0
- package/ui/src/styles/main-nav.css +52 -0
- package/ui/src/styles/mock-data.css +850 -0
- package/ui/src/styles/page-container.css +11 -0
- package/ui/src/styles/settings.css +208 -0
- package/ui/src/styles/url-redirect.css +279 -0
- package/ui/src/styles/welcome.css +282 -0
- package/webpack.config.js +49 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license React
|
|
3
|
+
* react-dom.production.min.js
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
6
|
+
*
|
|
7
|
+
* This source code is licensed under the MIT license found in the
|
|
8
|
+
* LICENSE file in the root directory of this source tree.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @license React
|
|
13
|
+
* react.production.min.js
|
|
14
|
+
*
|
|
15
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
16
|
+
*
|
|
17
|
+
* This source code is licensed under the MIT license found in the
|
|
18
|
+
* LICENSE file in the root directory of this source tree.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @license React
|
|
23
|
+
* scheduler.production.min.js
|
|
24
|
+
*
|
|
25
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
26
|
+
*
|
|
27
|
+
* This source code is licensed under the MIT license found in the
|
|
28
|
+
* LICENSE file in the root directory of this source tree.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @license React v16.13.1
|
|
32
|
+
* react-is.production.min.js
|
|
33
|
+
*
|
|
34
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
35
|
+
*
|
|
36
|
+
* This source code is licensed under the MIT license found in the
|
|
37
|
+
* LICENSE file in the root directory of this source tree.
|
|
38
|
+
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Whistle Mock 插件</title><script defer="defer" src="/bundle.js"></script></head><body><div id="root"></div></body></html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="refresh" content="0;url=/ui/">
|
|
5
|
+
<title>Redirecting...</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<script>
|
|
9
|
+
// 重定向到正确的路径
|
|
10
|
+
window.location.href = '/ui/';
|
|
11
|
+
</script>
|
|
12
|
+
<p>如果页面没有自动跳转,<a href="/ui/">请点击这里</a></p>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/rules.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Whistle Mock Plugin 使用指南
|
|
2
|
+
# 版本: 0.3.0
|
|
3
|
+
|
|
4
|
+
# ===== 基本规则配置 =====
|
|
5
|
+
# 在 Whistle 的规则配置中,使用以下格式添加规则:
|
|
6
|
+
* whistle.mock-plugin://
|
|
7
|
+
|
|
8
|
+
# 该插件现在会自动匹配已配置的接口规则,对于没有在插件中配置的接口,不会进行拦截
|
|
9
|
+
|
|
10
|
+
# ===== 规则匹配逻辑 =====
|
|
11
|
+
# 1. 请求会先检查是否匹配接口配置中定义的 URL 模式
|
|
12
|
+
# 2. 如果找到匹配的接口配置,且该接口处于启用状态,才会由插件处理
|
|
13
|
+
# 3. 如果没有匹配的接口配置,请求会直接透传到真实服务器
|
|
14
|
+
|
|
15
|
+
# ===== 常见配置示例 =====
|
|
16
|
+
# 不需要添加其他规则,只需要保留上面的全局规则即可
|
|
17
|
+
# 插件会根据在插件界面中配置的接口规则自动决定哪些请求需要处理
|
|
18
|
+
|
|
19
|
+
# ===== 调试功能 =====
|
|
20
|
+
# 通过访问以下URL可以查看插件的配置和状态:
|
|
21
|
+
# 查看全部配置: whistle.mock-plugin/_debug/config
|
|
22
|
+
# 查看启用的接口: whistle.mock-plugin/_debug/enabled-interfaces
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试 rulesServer 规则匹配功能
|
|
3
|
+
*
|
|
4
|
+
* 这个文件提供了一个简单的测试用例,用于验证 rulesServer 的匹配功能
|
|
5
|
+
* 实际环境中,您可以运行这个测试来确认规则匹配是否按预期工作
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// 模拟 fs-extra 模块
|
|
9
|
+
const fsMock = {
|
|
10
|
+
existsSync: jest.fn().mockReturnValue(true),
|
|
11
|
+
readJsonSync: jest.fn().mockReturnValue({
|
|
12
|
+
interfaces: [
|
|
13
|
+
{
|
|
14
|
+
id: '1',
|
|
15
|
+
name: '测试接口1',
|
|
16
|
+
urlPattern: '/api/test1',
|
|
17
|
+
httpMethod: 'GET',
|
|
18
|
+
enabled: true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: '2',
|
|
22
|
+
name: '测试接口2',
|
|
23
|
+
urlPattern: '/api/test2',
|
|
24
|
+
httpMethod: 'POST',
|
|
25
|
+
enabled: true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: '3',
|
|
29
|
+
name: '测试接口3',
|
|
30
|
+
urlPattern: '/api/test3/*',
|
|
31
|
+
httpMethod: 'ALL',
|
|
32
|
+
enabled: true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: '4',
|
|
36
|
+
name: '测试接口4',
|
|
37
|
+
urlPattern: '/api/test4',
|
|
38
|
+
httpMethod: 'GET',
|
|
39
|
+
enabled: false // 禁用状态
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// 模拟模块
|
|
46
|
+
jest.mock('fs-extra', () => fsMock);
|
|
47
|
+
jest.mock('path', () => ({
|
|
48
|
+
join: (...args) => args.join('/')
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// 导入被测试的模块
|
|
52
|
+
const rulesServer = require('../lib/rules-server');
|
|
53
|
+
|
|
54
|
+
describe('rulesServer 测试', () => {
|
|
55
|
+
let requestHandler;
|
|
56
|
+
let req;
|
|
57
|
+
let res;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
// 模拟 server
|
|
61
|
+
const server = {
|
|
62
|
+
on: (event, handler) => {
|
|
63
|
+
if (event === 'request') {
|
|
64
|
+
requestHandler = handler;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// 模拟请求和响应对象
|
|
70
|
+
req = {
|
|
71
|
+
originalReq: {
|
|
72
|
+
url: 'http://example.com/api/test1',
|
|
73
|
+
method: 'GET'
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
res = {
|
|
78
|
+
end: jest.fn()
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 初始化 rulesServer
|
|
82
|
+
rulesServer(server, {});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('匹配到的URL应该返回mock-plugin规则', () => {
|
|
86
|
+
// 测试匹配成功的情况
|
|
87
|
+
req.originalReq.url = 'http://example.com/api/test1';
|
|
88
|
+
req.originalReq.method = 'GET';
|
|
89
|
+
|
|
90
|
+
requestHandler(req, res);
|
|
91
|
+
expect(res.end).toHaveBeenCalledWith('mock-plugin://');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('未匹配的URL应该返回空字符串', () => {
|
|
95
|
+
// 测试匹配失败的情况
|
|
96
|
+
req.originalReq.url = 'http://example.com/api/not-exist';
|
|
97
|
+
req.originalReq.method = 'GET';
|
|
98
|
+
|
|
99
|
+
requestHandler(req, res);
|
|
100
|
+
expect(res.end).toHaveBeenCalledWith('');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('禁用的接口不应该被匹配', () => {
|
|
104
|
+
// 测试禁用接口的情况
|
|
105
|
+
req.originalReq.url = 'http://example.com/api/test4';
|
|
106
|
+
req.originalReq.method = 'GET';
|
|
107
|
+
|
|
108
|
+
requestHandler(req, res);
|
|
109
|
+
expect(res.end).toHaveBeenCalledWith('');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('通配符模式应该正确匹配', () => {
|
|
113
|
+
// 测试通配符匹配
|
|
114
|
+
req.originalReq.url = 'http://example.com/api/test3/123';
|
|
115
|
+
req.originalReq.method = 'GET';
|
|
116
|
+
|
|
117
|
+
requestHandler(req, res);
|
|
118
|
+
expect(res.end).toHaveBeenCalledWith('mock-plugin://');
|
|
119
|
+
});
|
|
120
|
+
});
|
package/ui/src/App.css
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
.App {
|
|
2
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
|
3
|
+
height: 100%;
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* 页面容器样式 */
|
|
8
|
+
.page-container {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
background-color: #fff;
|
|
12
|
+
border-radius: 8px;
|
|
13
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
|
14
|
+
padding: 24px;
|
|
15
|
+
transition: all 0.3s;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* 页面标题栏样式 */
|
|
19
|
+
.page-title-bar {
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
align-items: center;
|
|
23
|
+
margin-bottom: 24px;
|
|
24
|
+
padding-bottom: 16px;
|
|
25
|
+
border-bottom: 1px solid #f0f0f0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.page-title {
|
|
29
|
+
margin: 0;
|
|
30
|
+
color: rgba(0, 0, 0, 0.85);
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
font-size: 20px;
|
|
33
|
+
line-height: 32px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.page-description {
|
|
37
|
+
margin-top: 8px;
|
|
38
|
+
color: rgba(0, 0, 0, 0.45);
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.page-actions {
|
|
43
|
+
display: flex;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* 卡片样式 */
|
|
48
|
+
.custom-card {
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
transition: all 0.3s;
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.custom-card:hover {
|
|
56
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
57
|
+
transform: translateY(-3px);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 表格样式 */
|
|
61
|
+
.custom-table {
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.custom-table .ant-table-thead > tr > th {
|
|
67
|
+
background: #fafafa;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* 表单样式 */
|
|
72
|
+
.custom-form-item-label {
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
color: rgba(0, 0, 0, 0.85);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.custom-form-item-hint {
|
|
78
|
+
color: rgba(0, 0, 0, 0.45);
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
margin-top: 4px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* 按钮样式 */
|
|
84
|
+
.custom-button {
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
display: inline-flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.custom-button-icon {
|
|
92
|
+
margin-right: 8px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* 状态标签样式 */
|
|
96
|
+
.status-tag {
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
padding: 2px 8px;
|
|
99
|
+
font-size: 12px;
|
|
100
|
+
line-height: 1.5;
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.status-tag-success {
|
|
106
|
+
background-color: #f6ffed;
|
|
107
|
+
border: 1px solid #b7eb8f;
|
|
108
|
+
color: #52c41a;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.status-tag-warning {
|
|
112
|
+
background-color: #fffbe6;
|
|
113
|
+
border: 1px solid #ffe58f;
|
|
114
|
+
color: #faad14;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.status-tag-error {
|
|
118
|
+
background-color: #fff2f0;
|
|
119
|
+
border: 1px solid #ffccc7;
|
|
120
|
+
color: #ff4d4f;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* 动画效果 */
|
|
124
|
+
.fade-in {
|
|
125
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@keyframes fadeIn {
|
|
129
|
+
from {
|
|
130
|
+
opacity: 0;
|
|
131
|
+
}
|
|
132
|
+
to {
|
|
133
|
+
opacity: 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* 滚动条样式 */
|
|
138
|
+
::-webkit-scrollbar {
|
|
139
|
+
width: 6px;
|
|
140
|
+
height: 6px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
::-webkit-scrollbar-track {
|
|
144
|
+
background: #f1f1f1;
|
|
145
|
+
border-radius: 3px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
::-webkit-scrollbar-thumb {
|
|
149
|
+
background: #c1c1c1;
|
|
150
|
+
border-radius: 3px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
::-webkit-scrollbar-thumb:hover {
|
|
154
|
+
background: #a8a8a8;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* 响应式调整 */
|
|
158
|
+
@media (max-width: 576px) {
|
|
159
|
+
.page-container {
|
|
160
|
+
padding: 16px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.page-title-bar {
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
align-items: flex-start;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.page-actions {
|
|
169
|
+
margin-top: 16px;
|
|
170
|
+
width: 100%;
|
|
171
|
+
justify-content: flex-start;
|
|
172
|
+
}
|
|
173
|
+
}
|
package/ui/src/App.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
|
|
3
|
+
import MockData from './pages/MockData';
|
|
4
|
+
import InterfaceManagement from './pages/InterfaceManagement';
|
|
5
|
+
import Settings from './pages/Settings';
|
|
6
|
+
import './App.css';
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
return (
|
|
10
|
+
<Router>
|
|
11
|
+
<div className="App">
|
|
12
|
+
<Switch>
|
|
13
|
+
<Route exact path="/" component={MockData} />
|
|
14
|
+
<Route exact path="/interface" component={InterfaceManagement} />
|
|
15
|
+
<Route path="/interface/:featureId" component={InterfaceManagement} />
|
|
16
|
+
<Route path="/settings" component={Settings} />
|
|
17
|
+
<Redirect to="/" />
|
|
18
|
+
</Switch>
|
|
19
|
+
</div>
|
|
20
|
+
</Router>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default App;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Layout, Menu, Button, Breadcrumb, Typography, Space, theme, Divider, Badge } from 'antd';
|
|
3
|
+
import {
|
|
4
|
+
AppstoreOutlined,
|
|
5
|
+
ApiOutlined,
|
|
6
|
+
SettingOutlined,
|
|
7
|
+
FileTextOutlined,
|
|
8
|
+
MenuFoldOutlined,
|
|
9
|
+
MenuUnfoldOutlined,
|
|
10
|
+
DashboardOutlined
|
|
11
|
+
} from '@ant-design/icons';
|
|
12
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
13
|
+
import '../styles/app-layout.css';
|
|
14
|
+
|
|
15
|
+
const { Header, Content, Sider } = Layout;
|
|
16
|
+
const { Title, Text } = Typography;
|
|
17
|
+
|
|
18
|
+
const AppLayout = ({ children }) => {
|
|
19
|
+
const location = useLocation();
|
|
20
|
+
const currentPath = location.pathname;
|
|
21
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
22
|
+
const { token } = theme.useToken();
|
|
23
|
+
|
|
24
|
+
// 根据当前路径获取选中的菜单项和面包屑
|
|
25
|
+
const getSelectedKeys = () => {
|
|
26
|
+
if (currentPath === '/' || currentPath === '#/') return ['1'];
|
|
27
|
+
if (currentPath.startsWith('/interface') || currentPath.includes('/interface')) return ['2'];
|
|
28
|
+
if (currentPath === '/settings' || currentPath.includes('/settings')) return ['3'];
|
|
29
|
+
return [];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 根据当前路径获取面包屑
|
|
33
|
+
const getBreadcrumbs = () => {
|
|
34
|
+
const breadcrumbs = [
|
|
35
|
+
{ path: '/', title: '首页' }
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (currentPath.startsWith('/interface')) {
|
|
39
|
+
breadcrumbs.push({ path: '/interface', title: '接口管理' });
|
|
40
|
+
|
|
41
|
+
// 如果有featureId参数,添加特定功能模块名称
|
|
42
|
+
if (currentPath.includes('/interface/') && currentPath !== '/interface/') {
|
|
43
|
+
breadcrumbs.push({ path: currentPath, title: '功能接口详情' });
|
|
44
|
+
}
|
|
45
|
+
} else if (currentPath === '/settings') {
|
|
46
|
+
breadcrumbs.push({ path: '/settings', title: '系统设置' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return breadcrumbs;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// 获取当前页面标题
|
|
53
|
+
const getPageTitle = () => {
|
|
54
|
+
if (currentPath === '/' || currentPath === '#/') return '功能模块管理';
|
|
55
|
+
if (currentPath.startsWith('/interface')) return '接口配置管理';
|
|
56
|
+
if (currentPath === '/settings') return '系统设置';
|
|
57
|
+
return 'Whistle Mock Plugin';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 菜单配置
|
|
61
|
+
const menuItems = [
|
|
62
|
+
{
|
|
63
|
+
key: '1',
|
|
64
|
+
icon: <AppstoreOutlined />,
|
|
65
|
+
label: <Link to="/">功能模块</Link>,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: '2',
|
|
69
|
+
icon: <ApiOutlined />,
|
|
70
|
+
label: <Link to="/interface">接口管理</Link>,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: '3',
|
|
74
|
+
icon: <SettingOutlined />,
|
|
75
|
+
label: <Link to="/settings">系统设置</Link>,
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const breadcrumbs = getBreadcrumbs();
|
|
80
|
+
const colorBgContainer = token.colorBgContainer;
|
|
81
|
+
const colorBgLayout = token.colorBgLayout;
|
|
82
|
+
const borderRadius = token.borderRadius;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Layout className="app-layout">
|
|
86
|
+
{/* 顶部导航栏 */}
|
|
87
|
+
<Header className="app-header" style={{ background: colorBgContainer }}>
|
|
88
|
+
<div className="header-left">
|
|
89
|
+
<div className="logo-container">
|
|
90
|
+
<DashboardOutlined className="logo-icon" />
|
|
91
|
+
<div className="logo-text">Whistle Mock</div>
|
|
92
|
+
</div>
|
|
93
|
+
<Button
|
|
94
|
+
type="text"
|
|
95
|
+
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
96
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
97
|
+
className="collapse-button"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="header-right">
|
|
101
|
+
<Space>
|
|
102
|
+
<Text type="secondary">v0.1.0</Text>
|
|
103
|
+
</Space>
|
|
104
|
+
</div>
|
|
105
|
+
</Header>
|
|
106
|
+
|
|
107
|
+
<Layout>
|
|
108
|
+
{/* 侧边导航 */}
|
|
109
|
+
<Sider
|
|
110
|
+
width={220}
|
|
111
|
+
className="app-sider"
|
|
112
|
+
collapsed={collapsed}
|
|
113
|
+
collapsible
|
|
114
|
+
trigger={null}
|
|
115
|
+
style={{ background: colorBgContainer }}
|
|
116
|
+
>
|
|
117
|
+
<Menu
|
|
118
|
+
mode="inline"
|
|
119
|
+
selectedKeys={getSelectedKeys()}
|
|
120
|
+
items={menuItems}
|
|
121
|
+
style={{
|
|
122
|
+
height: '100%',
|
|
123
|
+
borderRight: 0,
|
|
124
|
+
padding: '8px',
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</Sider>
|
|
128
|
+
|
|
129
|
+
{/* 主内容区 */}
|
|
130
|
+
<Layout className="app-content-wrapper" style={{ padding: 0, background: colorBgLayout }}>
|
|
131
|
+
{/* 内容区 */}
|
|
132
|
+
<Content
|
|
133
|
+
className="app-content"
|
|
134
|
+
style={{
|
|
135
|
+
background: colorBgContainer,
|
|
136
|
+
padding: 24,
|
|
137
|
+
borderRadius: borderRadius,
|
|
138
|
+
minHeight: 280,
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</Content>
|
|
143
|
+
</Layout>
|
|
144
|
+
</Layout>
|
|
145
|
+
</Layout>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default AppLayout;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../styles/feature-card.css';
|
|
3
|
+
|
|
4
|
+
const FeatureCard = ({ feature }) => {
|
|
5
|
+
const { name, description, active, interfaceCount, createdAt } = feature;
|
|
6
|
+
|
|
7
|
+
// 生成随机的主题色(实际项目中可能会根据某种规则来确定)
|
|
8
|
+
const getRandomColor = () => {
|
|
9
|
+
const colors = ['#1890ff', '#13c2c2', '#52c41a', '#faad14', '#722ed1'];
|
|
10
|
+
return colors[Math.floor(Math.random() * colors.length)];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const themeColor = getRandomColor();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="feature-card">
|
|
17
|
+
<div className="feature-card-color-bar" style={{ backgroundColor: themeColor }}></div>
|
|
18
|
+
<div className="feature-card-header">
|
|
19
|
+
<h3 className="feature-card-title">{name}</h3>
|
|
20
|
+
<span className={`feature-card-badge ${active ? 'active' : 'inactive'}`}>
|
|
21
|
+
{active ? '已启用' : '未启用'}
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="feature-card-content">
|
|
25
|
+
<p className="feature-card-description">{description || '没有描述'}</p>
|
|
26
|
+
<div className="feature-card-stats">
|
|
27
|
+
<div className="feature-stat">
|
|
28
|
+
<span className="feature-stat-value">{interfaceCount}</span>
|
|
29
|
+
<span className="feature-stat-label">接口数量</span>
|
|
30
|
+
</div>
|
|
31
|
+
{createdAt && (
|
|
32
|
+
<div className="feature-stat">
|
|
33
|
+
<span className="feature-stat-label">创建于</span>
|
|
34
|
+
<span className="feature-stat-value feature-date">{createdAt}</span>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="feature-card-footer">
|
|
40
|
+
<button className="card-btn edit-btn">
|
|
41
|
+
<i className="icon-edit"></i>
|
|
42
|
+
编辑
|
|
43
|
+
</button>
|
|
44
|
+
<button className="card-btn primary-btn" style={{ borderColor: themeColor, color: themeColor }}>
|
|
45
|
+
<i className="icon-manage"></i>
|
|
46
|
+
管理接口
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default FeatureCard;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import '../styles/layout.css';
|
|
3
|
+
|
|
4
|
+
const Layout = ({ children }) => {
|
|
5
|
+
const [activeFeature, setActiveFeature] = useState(null);
|
|
6
|
+
const [features, setFeatures] = useState([
|
|
7
|
+
{ id: 1, name: '用户数据', active: true, count: 3 },
|
|
8
|
+
{ id: 2, name: '订单信息', active: false, count: 2 },
|
|
9
|
+
{ id: 3, name: '商品列表', active: false, count: 5 }
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const handleFeatureClick = (id) => {
|
|
13
|
+
setActiveFeature(id);
|
|
14
|
+
// 更新功能状态
|
|
15
|
+
setFeatures(features.map(feature => ({
|
|
16
|
+
...feature,
|
|
17
|
+
active: feature.id === id ? !feature.active : feature.active
|
|
18
|
+
})));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleCreateFeature = () => {
|
|
22
|
+
// 模拟创建新功能
|
|
23
|
+
const newId = features.length > 0 ? Math.max(...features.map(f => f.id)) + 1 : 1;
|
|
24
|
+
const newFeature = {
|
|
25
|
+
id: newId,
|
|
26
|
+
name: `新功能 ${newId}`,
|
|
27
|
+
active: false,
|
|
28
|
+
count: 0
|
|
29
|
+
};
|
|
30
|
+
setFeatures([...features, newFeature]);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="layout">
|
|
35
|
+
{/* 顶部导航栏 */}
|
|
36
|
+
<div className="header">
|
|
37
|
+
<div className="logo">
|
|
38
|
+
<div className="logo-icon">W</div>
|
|
39
|
+
Whistle 接口代理插件
|
|
40
|
+
</div>
|
|
41
|
+
<div className="search">
|
|
42
|
+
<input type="text" placeholder="搜索..." />
|
|
43
|
+
</div>
|
|
44
|
+
<div className="header-actions">
|
|
45
|
+
<button>
|
|
46
|
+
<i>🔄</i> 刷新
|
|
47
|
+
</button>
|
|
48
|
+
<button>
|
|
49
|
+
<i>⚙️</i> 设置
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* 主内容区域 */}
|
|
55
|
+
<div className="main-container">
|
|
56
|
+
{/* 左侧导航 */}
|
|
57
|
+
<div className="sidebar">
|
|
58
|
+
<div className="create-feature">
|
|
59
|
+
<button onClick={handleCreateFeature}>+ 创建新功能</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="feature-list">
|
|
63
|
+
{features.map(feature => (
|
|
64
|
+
<div
|
|
65
|
+
key={feature.id}
|
|
66
|
+
className={`feature-item ${activeFeature === feature.id ? 'active' : ''}`}
|
|
67
|
+
onClick={() => handleFeatureClick(feature.id)}
|
|
68
|
+
>
|
|
69
|
+
<div className="feature-item-name">
|
|
70
|
+
<div className={`status-icon ${feature.active ? 'active' : ''}`}>
|
|
71
|
+
{feature.active ? '✓' : ''}
|
|
72
|
+
</div>
|
|
73
|
+
{feature.name}
|
|
74
|
+
</div>
|
|
75
|
+
<div className="feature-item-count">{feature.count}</div>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="sidebar-footer">
|
|
81
|
+
Whistle Mock Plugin v0.2.0
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* 内容区 */}
|
|
86
|
+
<div className="content">
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default Layout;
|