mock-service-cli 3.6.3 → 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 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 概览页面 | false |
74
+ | `-w` 或 `--open` | 自动打开 API 概览页、文件浏览器页 | false |
75
75
  | `-e` 或 `--explorer` | 启用文件浏览器服务器,指定要浏览的目录 | ./ |
76
76
 
77
77
  ## 📖 使用示例
@@ -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
- document.addEventListener('DOMContentLoaded', () => {
813
- loadDirectory('/');
813
+ let eventHandlers = [];
814
+ let searchDebounceTimer = null;
814
815
 
815
- const fileGrid = document.getElementById('fileGrid');
816
- fileGrid.addEventListener('contextmenu', e => {
817
- const fileItem = e.target.closest('.file-item');
818
- if (fileItem) {
819
- e.preventDefault();
820
- const filePath = fileItem.dataset.path;
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
- document.addEventListener('click', () => {
827
- hideContextMenu();
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
- document.addEventListener('scroll', () => {
831
- hideContextMenu();
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
- function handleSearch() {
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
- let html = '';
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
- html += `
1124
- <div class="file-item ${hiddenClass}" data-path="${file.path}" data-is-directory="${
1125
- file.isDirectory
1126
- }">
1127
- <div class="file-icon" onclick="handleItemClick('${file.path}', ${
1128
- file.isDirectory
1129
- })">${icon}</div>
1130
- <div class="file-info" onclick="handleItemClick('${file.path}', ${file.isDirectory})">
1131
- <div class="file-name">${escapeHtml(file.name)}</div>
1132
- <div class="file-meta">
1133
- ${file.isDirectory ? '<span>目录</span>' : `<span>${size}</span>`}
1134
- <span>${mtime}</span>
1135
- </div>
1136
- </div>
1137
- <div class="action-buttons">
1138
- <button class="copy-btn" onclick="event.stopPropagation(); copyToClipboard('${
1139
- file.path
1140
- }')" title="复制路径">📋</button>
1141
- <button class="action-btn open" onclick="event.stopPropagation(); openInExplorer('${
1142
- file.path
1143
- }')" title="在文件管理器中打开">📂</button>
1144
- ${
1145
- !file.isDirectory
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
- html += `
1156
- <div class="file-item ${hiddenClass}" data-path="${file.path}" data-is-directory="${
1157
- file.isDirectory
1158
- }" onclick="handleItemClick('${file.path}', ${file.isDirectory})">
1159
- <div class="file-icon">${icon}</div>
1160
- <div class="file-info">
1161
- <div class="file-name">${escapeHtml(file.name)}</div>
1162
- <div class="file-meta">
1163
- ${file.isDirectory ? '目录' : size}
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
- // 强制更新 DOM
1172
- fileGrid.innerHTML = html;
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mock-service-cli",
3
- "version": "3.6.3",
3
+ "version": "3.6.4",
4
4
  "description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page, File explorer",
5
5
  "main": "./lib/mockServer.js",
6
6
  "bin": {
File without changes