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,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
3
|
+
import '../styles/main-nav.css';
|
|
4
|
+
|
|
5
|
+
const MainNav = () => {
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
const pathname = location.pathname;
|
|
8
|
+
|
|
9
|
+
// 检查当前路径是否匹配导航项
|
|
10
|
+
const isActive = (path) => {
|
|
11
|
+
if (path === '/' && pathname === '/') return true;
|
|
12
|
+
if (path !== '/' && pathname.startsWith(path)) return true;
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="main-nav">
|
|
18
|
+
<div className="main-header">
|
|
19
|
+
<h1>Whistle Mock 数据工厂</h1>
|
|
20
|
+
</div>
|
|
21
|
+
<nav className="nav-tabs">
|
|
22
|
+
<Link to="/" className={`nav-tab ${isActive('/') ? 'active' : ''}`}>
|
|
23
|
+
首页
|
|
24
|
+
</Link>
|
|
25
|
+
<Link to="/mock" className={`nav-tab ${isActive('/mock') ? 'active' : ''}`}>
|
|
26
|
+
Mock数据
|
|
27
|
+
</Link>
|
|
28
|
+
<Link to="/file-proxy" className={`nav-tab ${isActive('/file-proxy') ? 'active' : ''}`}>
|
|
29
|
+
接口配置
|
|
30
|
+
</Link>
|
|
31
|
+
<Link to="/url-redirect" className={`nav-tab ${isActive('/url-redirect') ? 'active' : ''}`}>
|
|
32
|
+
规则管理
|
|
33
|
+
</Link>
|
|
34
|
+
<Link to="/settings" className={`nav-tab ${isActive('/settings') ? 'active' : ''}`}>
|
|
35
|
+
设置
|
|
36
|
+
</Link>
|
|
37
|
+
</nav>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default MainNav;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import MainNav from './MainNav';
|
|
3
|
+
import '../styles/page-container.css';
|
|
4
|
+
|
|
5
|
+
const PageContainer = ({ children }) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="page-container">
|
|
8
|
+
<MainNav />
|
|
9
|
+
<div className="page-content">
|
|
10
|
+
{children}
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default PageContainer;
|
package/ui/src/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
// 导入样式
|
|
6
|
+
import 'antd/dist/reset.css'; // 导入antd样式,使用适合的版本
|
|
7
|
+
import './styles/global.css';
|
|
8
|
+
import './App.css';
|
|
9
|
+
|
|
10
|
+
// 导入页面组件
|
|
11
|
+
import MockData from './pages/MockData';
|
|
12
|
+
import InterfaceManagement from './pages/InterfaceManagement';
|
|
13
|
+
import Settings from './pages/Settings';
|
|
14
|
+
|
|
15
|
+
// 导入布局组件
|
|
16
|
+
import AppLayout from './components/AppLayout';
|
|
17
|
+
|
|
18
|
+
const App = () => {
|
|
19
|
+
return (
|
|
20
|
+
<Router>
|
|
21
|
+
<div className="App">
|
|
22
|
+
<Switch>
|
|
23
|
+
<Route exact path="/" component={MockData} />
|
|
24
|
+
<Route exact path="/interface" component={InterfaceManagement} />
|
|
25
|
+
<Route path="/interface/:featureId" component={InterfaceManagement} />
|
|
26
|
+
<Route path="/settings" component={Settings} />
|
|
27
|
+
<Redirect to="/" />
|
|
28
|
+
</Switch>
|
|
29
|
+
</div>
|
|
30
|
+
</Router>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 确保使用正确的渲染方法,兼容React 18和旧版本
|
|
35
|
+
const root = document.getElementById('root');
|
|
36
|
+
if (ReactDOM.createRoot) {
|
|
37
|
+
// React 18+
|
|
38
|
+
const rootElement = ReactDOM.createRoot(root);
|
|
39
|
+
rootElement.render(<App />);
|
|
40
|
+
} else {
|
|
41
|
+
// 旧版本React
|
|
42
|
+
ReactDOM.render(<App />, root);
|
|
43
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import AppLayout from '../components/AppLayout';
|
|
3
|
+
import '../styles/file-proxy.css';
|
|
4
|
+
|
|
5
|
+
const FileProxy = () => {
|
|
6
|
+
const [fileProxies, setFileProxies] = useState([
|
|
7
|
+
{
|
|
8
|
+
id: 1,
|
|
9
|
+
localPath: '/path/to/local/file.json',
|
|
10
|
+
remotePath: '/api/data',
|
|
11
|
+
description: '用户数据接口',
|
|
12
|
+
active: true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 2,
|
|
16
|
+
localPath: '/path/to/local/orders.json',
|
|
17
|
+
remotePath: '/api/orders',
|
|
18
|
+
description: '订单数据接口',
|
|
19
|
+
active: false
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 3,
|
|
23
|
+
localPath: '/path/to/local/products.json',
|
|
24
|
+
remotePath: '/api/products',
|
|
25
|
+
description: '商品数据接口',
|
|
26
|
+
active: true
|
|
27
|
+
}
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
31
|
+
const [currentProxy, setCurrentProxy] = useState(null);
|
|
32
|
+
const [localPath, setLocalPath] = useState('');
|
|
33
|
+
const [remotePath, setRemotePath] = useState('');
|
|
34
|
+
const [description, setDescription] = useState('');
|
|
35
|
+
|
|
36
|
+
const openModal = (proxy = null) => {
|
|
37
|
+
if (proxy) {
|
|
38
|
+
setCurrentProxy(proxy);
|
|
39
|
+
setLocalPath(proxy.localPath);
|
|
40
|
+
setRemotePath(proxy.remotePath);
|
|
41
|
+
setDescription(proxy.description);
|
|
42
|
+
} else {
|
|
43
|
+
setCurrentProxy(null);
|
|
44
|
+
setLocalPath('');
|
|
45
|
+
setRemotePath('');
|
|
46
|
+
setDescription('');
|
|
47
|
+
}
|
|
48
|
+
setIsModalOpen(true);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const closeModal = () => {
|
|
52
|
+
setIsModalOpen(false);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleSubmit = (e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
|
|
58
|
+
if (!localPath || !remotePath) {
|
|
59
|
+
alert('请填写本地文件路径和远程路径');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (currentProxy) {
|
|
64
|
+
// 编辑现有配置
|
|
65
|
+
setFileProxies(fileProxies.map(proxy =>
|
|
66
|
+
proxy.id === currentProxy.id
|
|
67
|
+
? { ...proxy, localPath, remotePath, description }
|
|
68
|
+
: proxy
|
|
69
|
+
));
|
|
70
|
+
} else {
|
|
71
|
+
// 添加新配置
|
|
72
|
+
const newProxy = {
|
|
73
|
+
id: fileProxies.length > 0 ? Math.max(...fileProxies.map(p => p.id)) + 1 : 1,
|
|
74
|
+
localPath,
|
|
75
|
+
remotePath,
|
|
76
|
+
description,
|
|
77
|
+
active: true
|
|
78
|
+
};
|
|
79
|
+
setFileProxies([...fileProxies, newProxy]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
closeModal();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const toggleActive = (id) => {
|
|
86
|
+
setFileProxies(fileProxies.map(proxy =>
|
|
87
|
+
proxy.id === id ? { ...proxy, active: !proxy.active } : proxy
|
|
88
|
+
));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const deleteProxy = (id) => {
|
|
92
|
+
if (window.confirm('确定要删除此条配置吗?')) {
|
|
93
|
+
setFileProxies(fileProxies.filter(proxy => proxy.id !== id));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<AppLayout>
|
|
99
|
+
<div className="file-proxy-container">
|
|
100
|
+
<div className="page-title-bar">
|
|
101
|
+
<div>
|
|
102
|
+
<h1 className="page-title">文件代理配置</h1>
|
|
103
|
+
<div className="page-description">
|
|
104
|
+
配置文件代理规则,支持本地文件和远程文件
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="page-actions">
|
|
108
|
+
<button className="add-button" onClick={() => openModal()}>
|
|
109
|
+
添加文件代理
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="proxy-list-container">
|
|
115
|
+
{fileProxies.length === 0 ? (
|
|
116
|
+
<div className="empty-data">
|
|
117
|
+
<div className="empty-icon">📁</div>
|
|
118
|
+
<div className="empty-text">暂无文件代理配置</div>
|
|
119
|
+
<button className="add-button" onClick={() => openModal()}>
|
|
120
|
+
添加文件代理
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
) : (
|
|
124
|
+
<table className="proxy-table">
|
|
125
|
+
<thead>
|
|
126
|
+
<tr>
|
|
127
|
+
<th>状态</th>
|
|
128
|
+
<th>本地文件路径</th>
|
|
129
|
+
<th>远程路径</th>
|
|
130
|
+
<th>描述</th>
|
|
131
|
+
<th>操作</th>
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
{fileProxies.map(proxy => (
|
|
136
|
+
<tr key={proxy.id} className={proxy.active ? '' : 'inactive'}>
|
|
137
|
+
<td>
|
|
138
|
+
<label className="switch">
|
|
139
|
+
<input
|
|
140
|
+
type="checkbox"
|
|
141
|
+
checked={proxy.active}
|
|
142
|
+
onChange={() => toggleActive(proxy.id)}
|
|
143
|
+
/>
|
|
144
|
+
<span className="slider"></span>
|
|
145
|
+
</label>
|
|
146
|
+
</td>
|
|
147
|
+
<td className="local-path">{proxy.localPath}</td>
|
|
148
|
+
<td>{proxy.remotePath}</td>
|
|
149
|
+
<td>{proxy.description || '-'}</td>
|
|
150
|
+
<td>
|
|
151
|
+
<div className="action-buttons">
|
|
152
|
+
<button
|
|
153
|
+
className="edit-button"
|
|
154
|
+
onClick={() => openModal(proxy)}
|
|
155
|
+
>
|
|
156
|
+
编辑
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
className="delete-button"
|
|
160
|
+
onClick={() => deleteProxy(proxy.id)}
|
|
161
|
+
>
|
|
162
|
+
删除
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</td>
|
|
166
|
+
</tr>
|
|
167
|
+
))}
|
|
168
|
+
</tbody>
|
|
169
|
+
</table>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{isModalOpen && (
|
|
174
|
+
<div className="modal-overlay">
|
|
175
|
+
<div className="modal">
|
|
176
|
+
<div className="modal-header">
|
|
177
|
+
<h2>{currentProxy ? '编辑文件代理' : '添加文件代理'}</h2>
|
|
178
|
+
<button className="close-button" onClick={closeModal}>×</button>
|
|
179
|
+
</div>
|
|
180
|
+
<form className="proxy-form" onSubmit={handleSubmit}>
|
|
181
|
+
<div className="form-group">
|
|
182
|
+
<label>本地文件路径</label>
|
|
183
|
+
<div className="file-input-group">
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
value={localPath}
|
|
187
|
+
onChange={(e) => setLocalPath(e.target.value)}
|
|
188
|
+
placeholder="/path/to/local/file.json"
|
|
189
|
+
required
|
|
190
|
+
/>
|
|
191
|
+
<button type="button" className="browse-button">浏览...</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="help-text">选择本地文件作为数据源</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="form-group">
|
|
197
|
+
<label>远程路径</label>
|
|
198
|
+
<input
|
|
199
|
+
type="text"
|
|
200
|
+
value={remotePath}
|
|
201
|
+
onChange={(e) => setRemotePath(e.target.value)}
|
|
202
|
+
placeholder="/api/data"
|
|
203
|
+
required
|
|
204
|
+
/>
|
|
205
|
+
<div className="help-text">访问此路径时将返回本地文件内容</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="form-group">
|
|
209
|
+
<label>描述(可选)</label>
|
|
210
|
+
<input
|
|
211
|
+
type="text"
|
|
212
|
+
value={description}
|
|
213
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
214
|
+
placeholder="接口描述"
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="form-actions">
|
|
219
|
+
<button type="button" className="cancel-button" onClick={closeModal}>
|
|
220
|
+
取消
|
|
221
|
+
</button>
|
|
222
|
+
<button type="submit" className="submit-button">
|
|
223
|
+
{currentProxy ? '保存修改' : '添加代理'}
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</form>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</AppLayout>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export default FileProxy;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Card, Table, Button, Space, Input, Tree, Modal, message, Breadcrumb, Typography } from 'antd';
|
|
3
|
+
import {
|
|
4
|
+
FileOutlined,
|
|
5
|
+
FolderOutlined,
|
|
6
|
+
PlusOutlined,
|
|
7
|
+
EditOutlined,
|
|
8
|
+
DeleteOutlined,
|
|
9
|
+
SaveOutlined,
|
|
10
|
+
UploadOutlined,
|
|
11
|
+
ExclamationCircleOutlined,
|
|
12
|
+
FolderAddOutlined
|
|
13
|
+
} from '@ant-design/icons';
|
|
14
|
+
import { ProxyAPI } from '../services/api';
|
|
15
|
+
|
|
16
|
+
const { Text } = Typography;
|
|
17
|
+
const { DirectoryTree } = Tree;
|
|
18
|
+
const { confirm } = Modal;
|
|
19
|
+
const { TextArea } = Input;
|
|
20
|
+
|
|
21
|
+
const FilesPage = () => {
|
|
22
|
+
const [files, setFiles] = useState([]);
|
|
23
|
+
const [loading, setLoading] = useState(false);
|
|
24
|
+
const [currentPath, setCurrentPath] = useState('');
|
|
25
|
+
const [breadcrumbs, setBreadcrumbs] = useState([{ title: '根目录', path: '' }]);
|
|
26
|
+
const [fileContent, setFileContent] = useState('');
|
|
27
|
+
const [currentFile, setCurrentFile] = useState(null);
|
|
28
|
+
const [fileModalVisible, setFileModalVisible] = useState(false);
|
|
29
|
+
const [folderModalVisible, setFolderModalVisible] = useState(false);
|
|
30
|
+
const [newFolderName, setNewFolderName] = useState('');
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
fetchFiles();
|
|
34
|
+
}, [currentPath]);
|
|
35
|
+
|
|
36
|
+
const fetchFiles = async () => {
|
|
37
|
+
setLoading(true);
|
|
38
|
+
try {
|
|
39
|
+
const data = await ProxyAPI.listFiles(currentPath);
|
|
40
|
+
setFiles(data);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("获取文件列表失败:", error);
|
|
43
|
+
message.error('获取文件列表失败');
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleOpenFile = async (file) => {
|
|
50
|
+
if (file.isDirectory) {
|
|
51
|
+
navigateToFolder(file.path);
|
|
52
|
+
} else {
|
|
53
|
+
try {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
const data = await ProxyAPI.getFile(file.path);
|
|
56
|
+
setCurrentFile(file);
|
|
57
|
+
setFileContent(data.content);
|
|
58
|
+
setFileModalVisible(true);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("获取文件内容失败:", error);
|
|
61
|
+
message.error('获取文件内容失败');
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const navigateToFolder = (path) => {
|
|
69
|
+
setCurrentPath(path);
|
|
70
|
+
|
|
71
|
+
// 更新面包屑
|
|
72
|
+
const parts = path.split('/').filter(Boolean);
|
|
73
|
+
const crumbs = [{ title: '根目录', path: '' }];
|
|
74
|
+
|
|
75
|
+
let currentPathSegment = '';
|
|
76
|
+
parts.forEach(part => {
|
|
77
|
+
currentPathSegment += currentPathSegment ? `/${part}` : part;
|
|
78
|
+
crumbs.push({
|
|
79
|
+
title: part,
|
|
80
|
+
path: currentPathSegment
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
setBreadcrumbs(crumbs);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleBreadcrumbClick = (path) => {
|
|
88
|
+
navigateToFolder(path);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleSaveFile = async () => {
|
|
92
|
+
try {
|
|
93
|
+
setLoading(true);
|
|
94
|
+
await ProxyAPI.saveFile(currentFile.path, fileContent);
|
|
95
|
+
setFileModalVisible(false);
|
|
96
|
+
message.success('文件保存成功');
|
|
97
|
+
fetchFiles();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("保存文件失败:", error);
|
|
100
|
+
message.error('保存文件失败');
|
|
101
|
+
} finally {
|
|
102
|
+
setLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleDeleteFile = (file) => {
|
|
107
|
+
confirm({
|
|
108
|
+
title: '确认删除',
|
|
109
|
+
icon: <ExclamationCircleOutlined />,
|
|
110
|
+
content: `确定要删除${file.isDirectory ? '文件夹' : '文件'} "${file.name}" 吗?`,
|
|
111
|
+
okText: '确认',
|
|
112
|
+
cancelText: '取消',
|
|
113
|
+
onOk: async () => {
|
|
114
|
+
try {
|
|
115
|
+
await ProxyAPI.deleteFile(file.path);
|
|
116
|
+
message.success('删除成功');
|
|
117
|
+
fetchFiles();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("删除失败:", error);
|
|
120
|
+
message.error('删除失败');
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleCreateFile = async () => {
|
|
127
|
+
const newFileName = `new-file-${Date.now()}.json`;
|
|
128
|
+
const newFilePath = currentPath ? `${currentPath}/${newFileName}` : newFileName;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await ProxyAPI.saveFile(newFilePath, '{\n "data": "新文件内容"\n}');
|
|
132
|
+
message.success('文件创建成功');
|
|
133
|
+
fetchFiles();
|
|
134
|
+
|
|
135
|
+
// 创建后立即打开编辑
|
|
136
|
+
const newFile = { path: newFilePath, name: newFileName, isDirectory: false };
|
|
137
|
+
handleOpenFile(newFile);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("创建文件失败:", error);
|
|
140
|
+
message.error('创建文件失败');
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleCreateFolder = () => {
|
|
145
|
+
setNewFolderName('');
|
|
146
|
+
setFolderModalVisible(true);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleFolderModalOk = async () => {
|
|
150
|
+
if (!newFolderName.trim()) {
|
|
151
|
+
message.error('请输入文件夹名称');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const newFolderPath = currentPath ? `${currentPath}/${newFolderName}` : newFolderName;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// 通过创建一个占位文件来创建文件夹
|
|
159
|
+
await ProxyAPI.saveFile(`${newFolderPath}/.placeholder`, '');
|
|
160
|
+
setFolderModalVisible(false);
|
|
161
|
+
message.success('文件夹创建成功');
|
|
162
|
+
fetchFiles();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("创建文件夹失败:", error);
|
|
165
|
+
message.error('创建文件夹失败');
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const columns = [
|
|
170
|
+
{
|
|
171
|
+
title: '名称',
|
|
172
|
+
dataIndex: 'name',
|
|
173
|
+
key: 'name',
|
|
174
|
+
render: (text, record) => (
|
|
175
|
+
<Space>
|
|
176
|
+
{record.isDirectory ? <FolderOutlined /> : <FileOutlined />}
|
|
177
|
+
<a onClick={() => handleOpenFile(record)}>{text}</a>
|
|
178
|
+
</Space>
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
title: '类型',
|
|
183
|
+
dataIndex: 'isDirectory',
|
|
184
|
+
key: 'type',
|
|
185
|
+
width: 100,
|
|
186
|
+
render: (isDirectory) => isDirectory ? '文件夹' : '文件',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
title: '大小',
|
|
190
|
+
dataIndex: 'size',
|
|
191
|
+
key: 'size',
|
|
192
|
+
width: 120,
|
|
193
|
+
render: (size, record) => record.isDirectory ? '-' : `${size} 字节`,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
title: '修改时间',
|
|
197
|
+
dataIndex: 'mtime',
|
|
198
|
+
key: 'mtime',
|
|
199
|
+
width: 200,
|
|
200
|
+
render: (text) => text ? new Date(text).toLocaleString() : '-',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
title: '操作',
|
|
204
|
+
key: 'action',
|
|
205
|
+
width: 120,
|
|
206
|
+
render: (_, record) => (
|
|
207
|
+
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDeleteFile(record)}>
|
|
208
|
+
删除
|
|
209
|
+
</Button>
|
|
210
|
+
),
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="files-page">
|
|
216
|
+
<Card
|
|
217
|
+
title="文件管理"
|
|
218
|
+
extra={
|
|
219
|
+
<Space>
|
|
220
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateFile}>
|
|
221
|
+
新建文件
|
|
222
|
+
</Button>
|
|
223
|
+
<Button icon={<FolderAddOutlined />} onClick={handleCreateFolder}>
|
|
224
|
+
新建文件夹
|
|
225
|
+
</Button>
|
|
226
|
+
</Space>
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
<Breadcrumb style={{ marginBottom: '16px' }}>
|
|
230
|
+
{breadcrumbs.map((crumb, index) => (
|
|
231
|
+
<Breadcrumb.Item key={index}>
|
|
232
|
+
<a onClick={() => handleBreadcrumbClick(crumb.path)}>{crumb.title}</a>
|
|
233
|
+
</Breadcrumb.Item>
|
|
234
|
+
))}
|
|
235
|
+
</Breadcrumb>
|
|
236
|
+
|
|
237
|
+
<Table
|
|
238
|
+
columns={columns}
|
|
239
|
+
dataSource={files}
|
|
240
|
+
rowKey="path"
|
|
241
|
+
loading={loading}
|
|
242
|
+
pagination={false}
|
|
243
|
+
/>
|
|
244
|
+
</Card>
|
|
245
|
+
|
|
246
|
+
<Modal
|
|
247
|
+
title={currentFile?.name || '文件内容'}
|
|
248
|
+
open={fileModalVisible}
|
|
249
|
+
onCancel={() => setFileModalVisible(false)}
|
|
250
|
+
footer={[
|
|
251
|
+
<Button key="cancel" onClick={() => setFileModalVisible(false)}>
|
|
252
|
+
取消
|
|
253
|
+
</Button>,
|
|
254
|
+
<Button
|
|
255
|
+
key="save"
|
|
256
|
+
type="primary"
|
|
257
|
+
icon={<SaveOutlined />}
|
|
258
|
+
onClick={handleSaveFile}
|
|
259
|
+
loading={loading}
|
|
260
|
+
>
|
|
261
|
+
保存
|
|
262
|
+
</Button>,
|
|
263
|
+
]}
|
|
264
|
+
width={800}
|
|
265
|
+
>
|
|
266
|
+
<TextArea
|
|
267
|
+
value={fileContent}
|
|
268
|
+
onChange={(e) => setFileContent(e.target.value)}
|
|
269
|
+
autoSize={{ minRows: 20, maxRows: 30 }}
|
|
270
|
+
/>
|
|
271
|
+
</Modal>
|
|
272
|
+
|
|
273
|
+
<Modal
|
|
274
|
+
title="新建文件夹"
|
|
275
|
+
open={folderModalVisible}
|
|
276
|
+
onOk={handleFolderModalOk}
|
|
277
|
+
onCancel={() => setFolderModalVisible(false)}
|
|
278
|
+
okText="创建"
|
|
279
|
+
cancelText="取消"
|
|
280
|
+
>
|
|
281
|
+
<Input
|
|
282
|
+
placeholder="请输入文件夹名称"
|
|
283
|
+
value={newFolderName}
|
|
284
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
285
|
+
onPressEnter={handleFolderModalOk}
|
|
286
|
+
/>
|
|
287
|
+
</Modal>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export default FilesPage;
|