mock-service-cli 4.1.0 → 4.2.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.
@@ -156,6 +156,100 @@
156
156
  background: #e0e0e0;
157
157
  }
158
158
 
159
+ .toolbar-action-btn {
160
+ padding: 6px 10px;
161
+ background: #f0f0f0;
162
+ border: none;
163
+ border-radius: 4px;
164
+ font-size: 12px;
165
+ cursor: pointer;
166
+ transition: all 0.2s;
167
+ }
168
+
169
+ .toolbar-action-btn:hover:not(:disabled) {
170
+ background: #e0e0e0;
171
+ }
172
+
173
+ .toolbar-action-btn.danger:hover:not(:disabled) {
174
+ background: #dc3545;
175
+ color: white;
176
+ }
177
+
178
+ .toolbar-action-btn:disabled {
179
+ opacity: 0.45;
180
+ cursor: not-allowed;
181
+ }
182
+
183
+ .edit-mode-toggle {
184
+ display: inline-flex;
185
+ align-items: center;
186
+ gap: 8px;
187
+ font-size: 12px;
188
+ color: #666;
189
+ user-select: none;
190
+ white-space: nowrap;
191
+ }
192
+
193
+ .edit-mode-toggle input {
194
+ position: absolute;
195
+ width: 1px;
196
+ height: 1px;
197
+ opacity: 0;
198
+ pointer-events: none;
199
+ }
200
+
201
+ .edit-mode-switch {
202
+ width: 38px;
203
+ height: 20px;
204
+ background: #d1d5db;
205
+ border-radius: 999px;
206
+ position: relative;
207
+ transition: background 0.2s;
208
+ flex-shrink: 0;
209
+ cursor: pointer;
210
+ }
211
+
212
+ .edit-mode-switch::after {
213
+ content: '';
214
+ position: absolute;
215
+ width: 16px;
216
+ height: 16px;
217
+ border-radius: 50%;
218
+ top: 2px;
219
+ left: 2px;
220
+ background: #fff;
221
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
222
+ transition: transform 0.2s;
223
+ }
224
+
225
+ .edit-mode-toggle input:checked + .edit-mode-switch {
226
+ background: #667eea;
227
+ }
228
+
229
+ .edit-mode-toggle input:checked + .edit-mode-switch::after {
230
+ transform: translateX(18px);
231
+ }
232
+
233
+ body:not(.editing) .edit-only {
234
+ display: none !important;
235
+ }
236
+
237
+ body.editing .edit-inline {
238
+ display: inline-flex !important;
239
+ }
240
+
241
+ body.editing .edit-inline-block {
242
+ display: inline-block !important;
243
+ }
244
+
245
+ body.editing .edit-flex {
246
+ display: flex !important;
247
+ }
248
+
249
+ body.editing .edit-block {
250
+ display: block !important;
251
+ }
252
+
159
253
  .back-btn {
160
254
  padding: 6px 12px;
161
255
  background: #f0f0f0;
@@ -318,6 +412,11 @@
318
412
  background: #f8f9fa;
319
413
  }
320
414
 
415
+ .file-item.selected {
416
+ background: #eef2ff;
417
+ outline: 1px solid #667eea;
418
+ }
419
+
321
420
  .list-view .file-item {
322
421
  flex-direction: row;
323
422
  padding: 6px 12px;
@@ -348,6 +447,17 @@
348
447
  height: 100%;
349
448
  }
350
449
 
450
+ .file-select {
451
+ align-self: flex-start;
452
+ margin-bottom: 6px;
453
+ cursor: pointer;
454
+ }
455
+
456
+ .list-view .file-select {
457
+ align-self: center;
458
+ margin: 0 8px 0 0;
459
+ }
460
+
351
461
  .file-info {
352
462
  text-align: center;
353
463
  word-break: break-all;
@@ -426,6 +536,16 @@
426
536
  color: white;
427
537
  }
428
538
 
539
+ .action-btn.rename:hover {
540
+ background: #ffc107;
541
+ color: #333;
542
+ }
543
+
544
+ .action-btn.delete:hover {
545
+ background: #dc3545;
546
+ color: white;
547
+ }
548
+
429
549
  .modal {
430
550
  display: none;
431
551
  position: fixed;
@@ -746,6 +866,30 @@
746
866
  返回上级
747
867
  </button>
748
868
  <button class="refresh-btn" onclick="loadDirectory(currentPath)" title="刷新">🔄</button>
869
+ <label class="edit-mode-toggle" title="启用编辑模式">
870
+ <input id="editModeToggle" type="checkbox" onchange="toggleEditMode(this.checked)" />
871
+ <span class="edit-mode-switch"></span>
872
+ <span class="edit-mode-label">编辑</span>
873
+ </label>
874
+ <button
875
+ class="toolbar-action-btn edit-only edit-inline"
876
+ onclick="createEntry('directory')"
877
+ title="新建文件夹"
878
+ >
879
+ 新建文件夹
880
+ </button>
881
+ <button class="toolbar-action-btn edit-only edit-inline" onclick="createEntry('file')" title="新建文件">
882
+ 新建文件
883
+ </button>
884
+ <button
885
+ class="toolbar-action-btn danger edit-only edit-inline"
886
+ id="deleteSelectedBtn"
887
+ onclick="deleteSelectedPaths()"
888
+ title="删除选中项"
889
+ disabled
890
+ >
891
+ 删除选中
892
+ </button>
749
893
  <button
750
894
  class="toggle-hidden-btn"
751
895
  id="toggleHiddenBtn"
@@ -803,6 +947,13 @@
803
947
  🖥️ 在文件管理器中打开
804
948
  </div>
805
949
  <div class="context-menu-item" id="ctxDownload" onclick="handleContextMenuAction('download')">⬇️ 下载</div>
950
+ <div class="context-menu-divider edit-only edit-block"></div>
951
+ <div class="context-menu-item edit-only edit-flex" id="ctxRename" onclick="handleContextMenuAction('rename')">
952
+ ✏️ 重命名
953
+ </div>
954
+ <div class="context-menu-item edit-only edit-flex" id="ctxDelete" onclick="handleContextMenuAction('delete')">
955
+ 🗑️ 删除
956
+ </div>
806
957
  </div>
807
958
 
808
959
  <script>
@@ -821,6 +972,8 @@
821
972
  let currentContextMenuFile = null;
822
973
  let currentImageIndex = -1;
823
974
  let imageFiles = [];
975
+ let selectedPaths = new Set();
976
+ let editMode = false;
824
977
 
825
978
  let eventHandlers = [];
826
979
  let searchDebounceTimer = null;
@@ -848,6 +1001,34 @@
848
1001
  eventHandlers = [];
849
1002
  }
850
1003
 
1004
+ function initializeEditMode() {
1005
+ editMode = false;
1006
+ }
1007
+
1008
+ function syncEditModeUI() {
1009
+ const body = document.body;
1010
+ const toggle = document.getElementById('editModeToggle');
1011
+
1012
+ body.classList.toggle('editing', editMode);
1013
+ if (toggle) {
1014
+ toggle.checked = editMode;
1015
+ }
1016
+
1017
+ if (!editMode) {
1018
+ selectedPaths.clear();
1019
+ hideContextMenu();
1020
+ currentContextMenuFile = null;
1021
+ }
1022
+
1023
+ updateSelectionToolbar();
1024
+ }
1025
+
1026
+ function toggleEditMode(enabled) {
1027
+ editMode = Boolean(enabled);
1028
+ syncEditModeUI();
1029
+ processAndRenderFiles();
1030
+ }
1031
+
851
1032
  function handleFileGridClick(e) {
852
1033
  const actionBtn = e.target.closest('[data-action]');
853
1034
  if (actionBtn) {
@@ -866,6 +1047,15 @@
866
1047
  case 'download':
867
1048
  downloadFile(path, filename);
868
1049
  break;
1050
+ case 'rename':
1051
+ renamePath(path, filename);
1052
+ break;
1053
+ case 'delete':
1054
+ deletePath(path, filename);
1055
+ break;
1056
+ case 'toggleSelect':
1057
+ toggleSelectedPath(path, actionBtn.checked, actionBtn.closest('.file-item'));
1058
+ break;
869
1059
  }
870
1060
  return;
871
1061
  }
@@ -940,6 +1130,8 @@
940
1130
  }
941
1131
 
942
1132
  document.addEventListener('DOMContentLoaded', () => {
1133
+ initializeEditMode();
1134
+ syncEditModeUI();
943
1135
  loadDirectory('/');
944
1136
 
945
1137
  const fileGrid = document.getElementById('fileGrid');
@@ -1007,6 +1199,12 @@
1007
1199
  downloadFile(path, path.split('/').pop());
1008
1200
  }
1009
1201
  break;
1202
+ case 'rename':
1203
+ renamePath(path, path.split('/').pop());
1204
+ break;
1205
+ case 'delete':
1206
+ deletePath(path, path.split('/').pop());
1207
+ break;
1010
1208
  }
1011
1209
 
1012
1210
  hideContextMenu();
@@ -1022,6 +1220,8 @@
1022
1220
  // 重置所有状态
1023
1221
  allFiles = [];
1024
1222
  currentFiles = [];
1223
+ selectedPaths.clear();
1224
+ updateSelectionToolbar();
1025
1225
 
1026
1226
  // 显示加载状态
1027
1227
  fileGrid.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
@@ -1209,6 +1409,77 @@
1209
1409
  }
1210
1410
  }
1211
1411
 
1412
+ async function requestJson(url, options) {
1413
+ const response = await fetch(url, options);
1414
+ const data = await response.json();
1415
+ if (!response.ok || data.error) {
1416
+ throw new Error(data.error || '请求失败');
1417
+ }
1418
+ return data;
1419
+ }
1420
+
1421
+ async function createEntry(type) {
1422
+ const label = type === 'directory' ? '文件夹' : '文件';
1423
+ const name = window.prompt(`请输入${label}名称`);
1424
+ if (name === null) return;
1425
+
1426
+ const trimmedName = name.trim();
1427
+ if (!trimmedName) {
1428
+ alert(`${label}名称不能为空`);
1429
+ return;
1430
+ }
1431
+
1432
+ try {
1433
+ await requestJson('/__api/path', {
1434
+ method: 'POST',
1435
+ headers: {
1436
+ 'Content-Type': 'application/json'
1437
+ },
1438
+ body: JSON.stringify({
1439
+ parentPath: currentPath,
1440
+ name: trimmedName,
1441
+ type
1442
+ })
1443
+ });
1444
+
1445
+ showToast(`新建${label}成功`);
1446
+ await loadDirectory(currentPath);
1447
+ } catch (error) {
1448
+ console.error(`新建${label}失败:`, error);
1449
+ alert(`新建${label}失败: ` + error.message);
1450
+ }
1451
+ }
1452
+
1453
+ async function renamePath(filePath, fileName) {
1454
+ const nextName = window.prompt('请输入新名称', fileName || filePath.split('/').pop());
1455
+ if (nextName === null) return;
1456
+
1457
+ const trimmedName = nextName.trim();
1458
+ if (!trimmedName) {
1459
+ alert('名称不能为空');
1460
+ return;
1461
+ }
1462
+
1463
+ try {
1464
+ await requestJson('/__api/path', {
1465
+ method: 'PATCH',
1466
+ headers: {
1467
+ 'Content-Type': 'application/json'
1468
+ },
1469
+ body: JSON.stringify({
1470
+ path: filePath,
1471
+ name: trimmedName
1472
+ })
1473
+ });
1474
+
1475
+ showToast('重命名成功');
1476
+ await loadDirectory(currentPath);
1477
+ } catch (error) {
1478
+ console.error('重命名失败:', error);
1479
+ alert('重命名失败: ' + error.message);
1480
+ }
1481
+ }
1482
+
1212
1483
  function downloadFile(filePath, fileName) {
1213
1484
  const url = `/__api/file?path=${encodeURIComponent(filePath)}`;
1214
1485
  const a = document.createElement('a');
@@ -1219,6 +1490,85 @@
1219
1490
  document.body.removeChild(a);
1220
1491
  }
1221
1492
 
1493
+ async function deletePath(filePath, fileName) {
1494
+ const label = fileName || filePath.split('/').pop() || filePath;
1495
+ const confirmed = window.confirm(`确定要删除 "${label}" 吗?此操作不可恢复。`);
1496
+
1497
+ if (!confirmed) return;
1498
+
1499
+ try {
1500
+ const response = await fetch('/__api/path', {
1501
+ method: 'DELETE',
1502
+ headers: {
1503
+ 'Content-Type': 'application/json'
1504
+ },
1505
+ body: JSON.stringify({ path: filePath })
1506
+ });
1507
+
1508
+ const data = await response.json();
1509
+ if (!response.ok || !data.success) {
1510
+ throw new Error(data.error || '删除失败');
1511
+ }
1512
+
1513
+ closeModal();
1514
+ showToast('删除成功');
1515
+ await loadDirectory(currentPath);
1516
+ } catch (error) {
1517
+ console.error('删除失败:', error);
1518
+ alert('删除失败: ' + error.message);
1519
+ }
1520
+ }
1521
+
1522
+ async function deleteSelectedPaths() {
1523
+ const paths = Array.from(selectedPaths);
1524
+ if (paths.length === 0) return;
1525
+
1526
+ const confirmed = window.confirm(`确定要删除选中的 ${paths.length} 项吗?此操作不可恢复。`);
1527
+ if (!confirmed) return;
1528
+
1529
+ try {
1530
+ await requestJson('/__api/paths', {
1531
+ method: 'DELETE',
1532
+ headers: {
1533
+ 'Content-Type': 'application/json'
1534
+ },
1535
+ body: JSON.stringify({ paths })
1536
+ });
1537
+
1538
+ selectedPaths.clear();
1539
+ updateSelectionToolbar();
1540
+ showToast('批量删除成功');
1541
+ await loadDirectory(currentPath);
1542
+ } catch (error) {
1543
+ console.error('批量删除失败:', error);
1544
+ alert('批量删除失败: ' + error.message);
1545
+ await loadDirectory(currentPath);
1546
+ }
1547
+ }
1548
+
1549
+ function toggleSelectedPath(filePath, checked, fileItem) {
1550
+ if (checked) {
1551
+ selectedPaths.add(filePath);
1552
+ } else {
1553
+ selectedPaths.delete(filePath);
1554
+ }
1555
+
1556
+ if (fileItem) {
1557
+ fileItem.classList.toggle('selected', checked);
1558
+ }
1559
+
1560
+ updateSelectionToolbar();
1561
+ }
1562
+
1563
+ function updateSelectionToolbar() {
1564
+ const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
1565
+ if (!deleteSelectedBtn) return;
1566
+
1567
+ const count = selectedPaths.size;
1568
+ deleteSelectedBtn.disabled = count === 0;
1569
+ deleteSelectedBtn.textContent = count ? `删除选中(${count})` : '删除选中';
1570
+ }
1571
+
1222
1572
  function renderBreadcrumb(currentPath, parentPath) {
1223
1573
  const breadcrumb = document.getElementById('breadcrumb');
1224
1574
  const parts = currentPath.split('/').filter(Boolean);
@@ -1266,14 +1616,26 @@
1266
1616
  const mtime = formatDate(file.mtime);
1267
1617
  const birthtime = formatDate(file.birthtime);
1268
1618
  const hiddenClass = file.isHidden ? 'hidden-file' : '';
1619
+ const selectedClass = selectedPaths.has(file.path) ? 'selected' : '';
1620
+ const selectedAttr = selectedPaths.has(file.path) ? 'checked' : '';
1621
+ const selectLabel = file.isDirectory ? '选择文件夹' : '选择文件';
1269
1622
 
1270
1623
  const fileItem = document.createElement('div');
1271
- fileItem.className = `file-item ${hiddenClass}`;
1624
+ fileItem.className = `file-item ${hiddenClass} ${selectedClass}`;
1272
1625
  fileItem.dataset.path = file.path;
1273
1626
  fileItem.dataset.isDirectory = file.isDirectory;
1274
1627
 
1275
1628
  if (currentView === 'list') {
1276
1629
  fileItem.innerHTML = `
1630
+ <input
1631
+ class="file-select edit-only edit-inline-block"
1632
+ type="checkbox"
1633
+ data-action="toggleSelect"
1634
+ data-path="${file.path}"
1635
+ data-filename="${escapeHtml(file.name)}"
1636
+ title="${selectLabel}"
1637
+ ${selectedAttr}
1638
+ />
1277
1639
  <div class="file-icon">${icon}</div>
1278
1640
  <div class="file-info">
1279
1641
  <div class="file-name">${escapeHtml(file.name)}</div>
@@ -1295,10 +1657,25 @@
1295
1657
  }" data-filename="${escapeHtml(file.name)}" title="下载">⬇️</button>`
1296
1658
  : ''
1297
1659
  }
1660
+ <button class="action-btn rename edit-only edit-inline" data-action="rename" data-path="${file.path}" data-filename="${escapeHtml(
1661
+ file.name
1662
+ )}" title="重命名">✏️</button>
1663
+ <button class="action-btn delete edit-only edit-inline" data-action="delete" data-path="${file.path}" data-filename="${escapeHtml(
1664
+ file.name
1665
+ )}" title="删除">🗑️</button>
1298
1666
  </div>
1299
1667
  `;
1300
1668
  } else {
1301
1669
  fileItem.innerHTML = `
1670
+ <input
1671
+ class="file-select edit-only edit-inline-block"
1672
+ type="checkbox"
1673
+ data-action="toggleSelect"
1674
+ data-path="${file.path}"
1675
+ data-filename="${escapeHtml(file.name)}"
1676
+ title="${selectLabel}"
1677
+ ${selectedAttr}
1678
+ />
1302
1679
  <div class="file-icon">${icon}</div>
1303
1680
  <div class="file-info">
1304
1681
  <div class="file-name">${escapeHtml(file.name)}</div>