mock-service-cli 3.6.2 → 3.6.4
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/README.md +1 -1
- package/lib/file-explorer.html +177 -114
- package/lib/fileExplorerServer.js +24 -9
- package/package.json +1 -1
- /package/{LICENSE.md → LICENSE} +0 -0
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ npx mock-service-cli [options] [path]
|
|
|
71
71
|
| `-P` 或 `--web-port` | Web 服务器端口 | 9090 |
|
|
72
72
|
| `-b` 或 `--web-baseurl` | 指定 SPA Web 服务器的公共路径 | - |
|
|
73
73
|
| `-R` 或 `--static-server` | 启用静态服务器,指定静态资源目录 | - |
|
|
74
|
-
| `-w` 或 `--open` | 自动打开 API
|
|
74
|
+
| `-w` 或 `--open` | 自动打开 API 概览页、文件浏览器页 | false |
|
|
75
75
|
| `-e` 或 `--explorer` | 启用文件浏览器服务器,指定要浏览的目录 | ./ |
|
|
76
76
|
|
|
77
77
|
## 📖 使用示例
|
package/lib/file-explorer.html
CHANGED
|
@@ -295,13 +295,13 @@
|
|
|
295
295
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
296
296
|
gap: 8px;
|
|
297
297
|
overflow: auto;
|
|
298
|
-
flex: 1;
|
|
299
298
|
}
|
|
300
299
|
|
|
301
300
|
.file-grid.list-view {
|
|
302
301
|
grid-template-columns: 1fr;
|
|
303
302
|
gap: 0;
|
|
304
303
|
padding: 4px 0;
|
|
304
|
+
flex: unset;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
.file-item {
|
|
@@ -324,6 +324,7 @@
|
|
|
324
324
|
border-bottom: 1px solid #f0f0f0;
|
|
325
325
|
border-radius: 0;
|
|
326
326
|
min-height: auto;
|
|
327
|
+
height: 37px;
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
.file-icon {
|
|
@@ -809,29 +810,141 @@
|
|
|
809
810
|
let currentImageIndex = -1;
|
|
810
811
|
let imageFiles = [];
|
|
811
812
|
|
|
812
|
-
|
|
813
|
-
|
|
813
|
+
let eventHandlers = [];
|
|
814
|
+
let searchDebounceTimer = null;
|
|
814
815
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const isDirectory = fileItem.dataset.isDirectory === 'true';
|
|
822
|
-
showContextMenu(e.clientX, e.clientY, filePath, isDirectory);
|
|
823
|
-
}
|
|
824
|
-
});
|
|
816
|
+
function debounce(func, wait) {
|
|
817
|
+
return function (...args) {
|
|
818
|
+
clearTimeout(searchDebounceTimer);
|
|
819
|
+
searchDebounceTimer = setTimeout(() => func.apply(this, args), wait);
|
|
820
|
+
};
|
|
821
|
+
}
|
|
825
822
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
});
|
|
823
|
+
function addEventListenerWithCleanup(element, event, handler, options) {
|
|
824
|
+
element.addEventListener(event, handler, options);
|
|
825
|
+
eventHandlers.push({ element, event, handler, options });
|
|
826
|
+
}
|
|
829
827
|
|
|
830
|
-
|
|
831
|
-
|
|
828
|
+
function cleanupEventListeners() {
|
|
829
|
+
if (searchDebounceTimer) {
|
|
830
|
+
clearTimeout(searchDebounceTimer);
|
|
831
|
+
searchDebounceTimer = null;
|
|
832
|
+
}
|
|
833
|
+
eventHandlers.forEach(({ element, event, handler, options }) => {
|
|
834
|
+
element.removeEventListener(event, handler, options);
|
|
832
835
|
});
|
|
836
|
+
eventHandlers = [];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function handleFileGridClick(e) {
|
|
840
|
+
const actionBtn = e.target.closest('[data-action]');
|
|
841
|
+
if (actionBtn) {
|
|
842
|
+
e.stopPropagation();
|
|
843
|
+
const action = actionBtn.dataset.action;
|
|
844
|
+
const path = actionBtn.dataset.path;
|
|
845
|
+
const filename = actionBtn.dataset.filename;
|
|
846
|
+
|
|
847
|
+
switch (action) {
|
|
848
|
+
case 'copyPath':
|
|
849
|
+
copyToClipboard(path);
|
|
850
|
+
break;
|
|
851
|
+
case 'openInExplorer':
|
|
852
|
+
openInExplorer(path);
|
|
853
|
+
break;
|
|
854
|
+
case 'download':
|
|
855
|
+
downloadFile(path, filename);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const fileItem = e.target.closest('.file-item');
|
|
862
|
+
if (fileItem) {
|
|
863
|
+
const filePath = fileItem.dataset.path;
|
|
864
|
+
const isDirectory = fileItem.dataset.isDirectory === 'true';
|
|
865
|
+
handleItemClick(filePath, isDirectory);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function handleFileGridContextmenu(e) {
|
|
870
|
+
const fileItem = e.target.closest('.file-item');
|
|
871
|
+
if (fileItem) {
|
|
872
|
+
e.preventDefault();
|
|
873
|
+
const filePath = fileItem.dataset.path;
|
|
874
|
+
const isDirectory = fileItem.dataset.isDirectory === 'true';
|
|
875
|
+
showContextMenu(e.clientX, e.clientY, filePath, isDirectory);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function handleDocumentClick() {
|
|
880
|
+
hideContextMenu();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function handleDocumentScroll() {
|
|
884
|
+
hideContextMenu();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function handleModalClick(e) {
|
|
888
|
+
if (e.target === e.currentTarget) {
|
|
889
|
+
closeModal();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function handleModalWheel(e) {
|
|
894
|
+
const img = document.getElementById('previewImage');
|
|
895
|
+
if (!img) return;
|
|
896
|
+
|
|
897
|
+
e.preventDefault();
|
|
898
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
899
|
+
zoomImage(delta);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function handleDocumentKeydown(e) {
|
|
903
|
+
if (e.key === 'Escape') {
|
|
904
|
+
closeModal();
|
|
905
|
+
}
|
|
906
|
+
if (e.key === 'Backspace' && document.activeElement.tagName !== 'INPUT') {
|
|
907
|
+
e.preventDefault();
|
|
908
|
+
goBack();
|
|
909
|
+
}
|
|
910
|
+
const img = document.getElementById('previewImage');
|
|
911
|
+
if (img) {
|
|
912
|
+
if (e.key === '+' || e.key === '=') {
|
|
913
|
+
zoomImage(0.1);
|
|
914
|
+
} else if (e.key === '-') {
|
|
915
|
+
zoomImage(-0.1);
|
|
916
|
+
} else if (e.key === '0') {
|
|
917
|
+
resetImage();
|
|
918
|
+
} else if (e.key === '[' || e.key === '{') {
|
|
919
|
+
rotateImage(-90);
|
|
920
|
+
} else if (e.key === ']' || e.key === '}') {
|
|
921
|
+
rotateImage(90);
|
|
922
|
+
} else if (e.key === 'ArrowLeft') {
|
|
923
|
+
navigateImage(-1);
|
|
924
|
+
} else if (e.key === 'ArrowRight') {
|
|
925
|
+
navigateImage(1);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
931
|
+
loadDirectory('/');
|
|
932
|
+
|
|
933
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
934
|
+
const previewModal = document.getElementById('previewModal');
|
|
935
|
+
|
|
936
|
+
addEventListenerWithCleanup(fileGrid, 'click', handleFileGridClick);
|
|
937
|
+
addEventListenerWithCleanup(fileGrid, 'contextmenu', handleFileGridContextmenu);
|
|
938
|
+
addEventListenerWithCleanup(document, 'click', handleDocumentClick);
|
|
939
|
+
addEventListenerWithCleanup(document, 'scroll', handleDocumentScroll);
|
|
940
|
+
addEventListenerWithCleanup(previewModal, 'click', handleModalClick);
|
|
941
|
+
addEventListenerWithCleanup(previewModal, 'wheel', handleModalWheel, { passive: false });
|
|
942
|
+
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
833
943
|
});
|
|
834
944
|
|
|
945
|
+
window.addEventListener('beforeunload', cleanupEventListeners);
|
|
946
|
+
window.addEventListener('pagehide', cleanupEventListeners);
|
|
947
|
+
|
|
835
948
|
function showContextMenu(x, y, filePath, isDirectory) {
|
|
836
949
|
currentContextMenuFile = { path: filePath, isDirectory };
|
|
837
950
|
|
|
@@ -1029,10 +1142,10 @@
|
|
|
1029
1142
|
}
|
|
1030
1143
|
}
|
|
1031
1144
|
|
|
1032
|
-
|
|
1145
|
+
const handleSearch = debounce(() => {
|
|
1033
1146
|
searchQuery = document.getElementById('searchInput').value;
|
|
1034
1147
|
processAndRenderFiles();
|
|
1035
|
-
}
|
|
1148
|
+
}, 200);
|
|
1036
1149
|
|
|
1037
1150
|
function handleSort() {
|
|
1038
1151
|
sortBy = document.getElementById('sortSelect').value;
|
|
@@ -1112,64 +1225,61 @@
|
|
|
1112
1225
|
return;
|
|
1113
1226
|
}
|
|
1114
1227
|
|
|
1115
|
-
|
|
1228
|
+
const fragment = document.createDocumentFragment();
|
|
1229
|
+
|
|
1116
1230
|
files.forEach(file => {
|
|
1117
1231
|
const icon = getFileIcon(file);
|
|
1118
1232
|
const size = formatSize(file.size);
|
|
1119
1233
|
const mtime = formatDate(file.mtime);
|
|
1120
1234
|
const hiddenClass = file.isHidden ? 'hidden-file' : '';
|
|
1121
1235
|
|
|
1236
|
+
const fileItem = document.createElement('div');
|
|
1237
|
+
fileItem.className = `file-item ${hiddenClass}`;
|
|
1238
|
+
fileItem.dataset.path = file.path;
|
|
1239
|
+
fileItem.dataset.isDirectory = file.isDirectory;
|
|
1240
|
+
|
|
1122
1241
|
if (currentView === 'list') {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
file
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
? `<button class="action-btn download" onclick="event.stopPropagation(); downloadFile('${
|
|
1147
|
-
file.path
|
|
1148
|
-
}', '${escapeHtml(file.name)}')" title="下载">⬇️</button>`
|
|
1149
|
-
: ''
|
|
1150
|
-
}
|
|
1151
|
-
</div>
|
|
1152
|
-
</div>
|
|
1153
|
-
`;
|
|
1242
|
+
fileItem.innerHTML = `
|
|
1243
|
+
<div class="file-icon">${icon}</div>
|
|
1244
|
+
<div class="file-info">
|
|
1245
|
+
<div class="file-name">${escapeHtml(file.name)}</div>
|
|
1246
|
+
<div class="file-meta">
|
|
1247
|
+
${file.isDirectory ? '<span>目录</span>' : `<span>${size}</span>`}
|
|
1248
|
+
<span>${mtime}</span>
|
|
1249
|
+
</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
<div class="action-buttons">
|
|
1252
|
+
<button class="copy-btn" data-action="copyPath" data-path="${file.path}" title="复制路径">📋</button>
|
|
1253
|
+
<button class="action-btn open" data-action="openInExplorer" data-path="${
|
|
1254
|
+
file.path
|
|
1255
|
+
}" title="在文件管理器中打开">📂</button>
|
|
1256
|
+
${
|
|
1257
|
+
!file.isDirectory
|
|
1258
|
+
? `<button class="action-btn download" data-action="download" data-path="${
|
|
1259
|
+
file.path
|
|
1260
|
+
}" data-filename="${escapeHtml(file.name)}" title="下载">⬇️</button>`
|
|
1261
|
+
: ''
|
|
1262
|
+
}
|
|
1263
|
+
</div>
|
|
1264
|
+
`;
|
|
1154
1265
|
} else {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
file
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
</div>
|
|
1165
|
-
</div>
|
|
1166
|
-
</div>
|
|
1167
|
-
`;
|
|
1266
|
+
fileItem.innerHTML = `
|
|
1267
|
+
<div class="file-icon">${icon}</div>
|
|
1268
|
+
<div class="file-info">
|
|
1269
|
+
<div class="file-name">${escapeHtml(file.name)}</div>
|
|
1270
|
+
<div class="file-meta">
|
|
1271
|
+
${file.isDirectory ? '目录' : size}
|
|
1272
|
+
</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
`;
|
|
1168
1275
|
}
|
|
1276
|
+
|
|
1277
|
+
fragment.appendChild(fileItem);
|
|
1169
1278
|
});
|
|
1170
1279
|
|
|
1171
|
-
//
|
|
1172
|
-
fileGrid.innerHTML =
|
|
1280
|
+
// 清空并一次性更新 DOM
|
|
1281
|
+
fileGrid.innerHTML = '';
|
|
1282
|
+
fileGrid.appendChild(fragment);
|
|
1173
1283
|
}
|
|
1174
1284
|
|
|
1175
1285
|
function handleItemClick(path, isDirectory) {
|
|
@@ -1749,53 +1859,6 @@
|
|
|
1749
1859
|
div.textContent = text;
|
|
1750
1860
|
return div.innerHTML;
|
|
1751
1861
|
}
|
|
1752
|
-
|
|
1753
|
-
document.getElementById('previewModal').addEventListener('click', e => {
|
|
1754
|
-
if (e.target === e.currentTarget) {
|
|
1755
|
-
closeModal();
|
|
1756
|
-
}
|
|
1757
|
-
});
|
|
1758
|
-
|
|
1759
|
-
document.getElementById('previewModal').addEventListener(
|
|
1760
|
-
'wheel',
|
|
1761
|
-
e => {
|
|
1762
|
-
const img = document.getElementById('previewImage');
|
|
1763
|
-
if (!img) return;
|
|
1764
|
-
|
|
1765
|
-
e.preventDefault();
|
|
1766
|
-
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
1767
|
-
zoomImage(delta);
|
|
1768
|
-
},
|
|
1769
|
-
{ passive: false }
|
|
1770
|
-
);
|
|
1771
|
-
|
|
1772
|
-
document.addEventListener('keydown', e => {
|
|
1773
|
-
if (e.key === 'Escape') {
|
|
1774
|
-
closeModal();
|
|
1775
|
-
}
|
|
1776
|
-
if (e.key === 'Backspace' && document.activeElement.tagName !== 'INPUT') {
|
|
1777
|
-
e.preventDefault();
|
|
1778
|
-
goBack();
|
|
1779
|
-
}
|
|
1780
|
-
const img = document.getElementById('previewImage');
|
|
1781
|
-
if (img) {
|
|
1782
|
-
if (e.key === '+' || e.key === '=') {
|
|
1783
|
-
zoomImage(0.1);
|
|
1784
|
-
} else if (e.key === '-') {
|
|
1785
|
-
zoomImage(-0.1);
|
|
1786
|
-
} else if (e.key === '0') {
|
|
1787
|
-
resetImage();
|
|
1788
|
-
} else if (e.key === '[' || e.key === '{') {
|
|
1789
|
-
rotateImage(-90);
|
|
1790
|
-
} else if (e.key === ']' || e.key === '}') {
|
|
1791
|
-
rotateImage(90);
|
|
1792
|
-
} else if (e.key === 'ArrowLeft') {
|
|
1793
|
-
navigateImage(-1);
|
|
1794
|
-
} else if (e.key === 'ArrowRight') {
|
|
1795
|
-
navigateImage(1);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
});
|
|
1799
1862
|
</script>
|
|
1800
1863
|
</body>
|
|
1801
1864
|
</html>
|
|
@@ -49,15 +49,20 @@ function init() {
|
|
|
49
49
|
dirPath = decodeURIComponent(dirPath);
|
|
50
50
|
|
|
51
51
|
let fullPath;
|
|
52
|
-
|
|
52
|
+
const resolvedRoot = path.resolve(explorerRoot);
|
|
53
|
+
|
|
54
|
+
if (explorerRoot === '/') {
|
|
55
|
+
// 根目录,可以直接使用绝对路径
|
|
53
56
|
fullPath = dirPath;
|
|
54
57
|
} else {
|
|
55
|
-
|
|
58
|
+
// 非根目录,应该将 dirPath 视为相对于 explorerRoot 的路径
|
|
59
|
+
// 如果 dirPath 以 / 开头,去掉前面的 /
|
|
60
|
+
const relativePath = dirPath.startsWith('/') ? dirPath.substring(1) : dirPath;
|
|
61
|
+
fullPath = path.resolve(explorerRoot, relativePath);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
// 防止路径遍历攻击 - 确保路径不会越界
|
|
59
65
|
if (explorerRoot !== '/') {
|
|
60
|
-
const resolvedRoot = path.resolve(explorerRoot);
|
|
61
66
|
if (!fullPath.startsWith(resolvedRoot)) {
|
|
62
67
|
return res.status(403).json({ error: 'Access denied' });
|
|
63
68
|
}
|
|
@@ -127,15 +132,20 @@ function init() {
|
|
|
127
132
|
filePath = decodeURIComponent(filePath);
|
|
128
133
|
|
|
129
134
|
let fullPath;
|
|
130
|
-
|
|
135
|
+
const resolvedRoot = path.resolve(explorerRoot);
|
|
136
|
+
|
|
137
|
+
if (explorerRoot === '/') {
|
|
138
|
+
// 根目录,可以直接使用绝对路径
|
|
131
139
|
fullPath = filePath;
|
|
132
140
|
} else {
|
|
133
|
-
|
|
141
|
+
// 非根目录,应该将 filePath 视为相对于 explorerRoot 的路径
|
|
142
|
+
// 如果 filePath 以 / 开头,去掉前面的 /
|
|
143
|
+
const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
144
|
+
fullPath = path.resolve(explorerRoot, relativePath);
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
// 防止路径遍历攻击 - 确保路径不会越界
|
|
137
148
|
if (explorerRoot !== '/') {
|
|
138
|
-
const resolvedRoot = path.resolve(explorerRoot);
|
|
139
149
|
if (!fullPath.startsWith(resolvedRoot)) {
|
|
140
150
|
return res.status(403).json({ error: 'Access denied' });
|
|
141
151
|
}
|
|
@@ -181,15 +191,20 @@ function init() {
|
|
|
181
191
|
filePath = decodeURIComponent(filePath);
|
|
182
192
|
|
|
183
193
|
let fullPath;
|
|
184
|
-
|
|
194
|
+
const resolvedRoot = path.resolve(explorerRoot);
|
|
195
|
+
|
|
196
|
+
if (explorerRoot === '/') {
|
|
197
|
+
// 根目录,可以直接使用绝对路径
|
|
185
198
|
fullPath = filePath;
|
|
186
199
|
} else {
|
|
187
|
-
|
|
200
|
+
// 非根目录,应该将 filePath 视为相对于 explorerRoot 的路径
|
|
201
|
+
// 如果 filePath 以 / 开头,去掉前面的 /
|
|
202
|
+
const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
203
|
+
fullPath = path.resolve(explorerRoot, relativePath);
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
// 防止路径遍历攻击 - 确保路径不会越界
|
|
191
207
|
if (explorerRoot !== '/') {
|
|
192
|
-
const resolvedRoot = path.resolve(explorerRoot);
|
|
193
208
|
if (!fullPath.startsWith(resolvedRoot)) {
|
|
194
209
|
return res.status(403).json({ error: 'Access denied' });
|
|
195
210
|
}
|
package/package.json
CHANGED
/package/{LICENSE.md → LICENSE}
RENAMED
|
File without changes
|