mock-service-cli 4.4.0 → 4.5.1
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 +24 -1
- package/dist/api-overview.html +1 -0
- package/dist/edition.json +1 -0
- package/dist/favicon-api-overview.svg +8 -0
- package/dist/favicon-file-explorer-login.svg +8 -0
- package/dist/favicon-file-explorer.svg +6 -0
- package/dist/favicon-static-server.svg +6 -0
- package/dist/file-explorer-login.html +1 -0
- package/dist/file-explorer.html +364 -46
- package/dist/meta.json +9991 -0
- package/dist/runtime.js +24 -24
- package/docs/static-server.config.example.json +28 -0
- package/package.json +15 -77
package/dist/file-explorer.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>文件浏览器 - File Explorer</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="__FILE_EXPLORER_FAVICON_URL__" />
|
|
7
8
|
<style>
|
|
8
9
|
* {
|
|
9
10
|
margin: 0;
|
|
@@ -1010,14 +1011,29 @@
|
|
|
1010
1011
|
<button class="toolbar-action-btn edit-only edit-inline" onclick="createEntry('file')" title="新建文件">
|
|
1011
1012
|
新建文件
|
|
1012
1013
|
</button>
|
|
1013
|
-
<button
|
|
1014
|
+
<button
|
|
1015
|
+
class="toolbar-action-btn edit-only edit-inline"
|
|
1016
|
+
onclick="document.getElementById('uploadFilesInput').click()"
|
|
1017
|
+
title="上传文件"
|
|
1018
|
+
>
|
|
1014
1019
|
上传文件
|
|
1015
1020
|
</button>
|
|
1016
|
-
<button
|
|
1021
|
+
<button
|
|
1022
|
+
class="toolbar-action-btn edit-only edit-inline"
|
|
1023
|
+
onclick="document.getElementById('uploadFolderInput').click()"
|
|
1024
|
+
title="上传文件夹"
|
|
1025
|
+
>
|
|
1017
1026
|
上传文件夹
|
|
1018
1027
|
</button>
|
|
1019
1028
|
<input id="uploadFilesInput" type="file" multiple hidden onchange="uploadSelectedFiles(this.files)" />
|
|
1020
|
-
<input
|
|
1029
|
+
<input
|
|
1030
|
+
id="uploadFolderInput"
|
|
1031
|
+
type="file"
|
|
1032
|
+
webkitdirectory
|
|
1033
|
+
directory
|
|
1034
|
+
hidden
|
|
1035
|
+
onchange="uploadSelectedFiles(this.files)"
|
|
1036
|
+
/>
|
|
1021
1037
|
<button
|
|
1022
1038
|
class="toolbar-action-btn danger edit-only edit-inline"
|
|
1023
1039
|
id="deleteSelectedBtn"
|
|
@@ -1027,6 +1043,15 @@
|
|
|
1027
1043
|
>
|
|
1028
1044
|
删除选中
|
|
1029
1045
|
</button>
|
|
1046
|
+
<button
|
|
1047
|
+
class="toolbar-action-btn edit-only edit-inline"
|
|
1048
|
+
id="archiveSelectedBtn"
|
|
1049
|
+
onclick="createArchiveFromSelection()"
|
|
1050
|
+
title="压缩选中项"
|
|
1051
|
+
disabled
|
|
1052
|
+
>
|
|
1053
|
+
压缩选中
|
|
1054
|
+
</button>
|
|
1030
1055
|
<button
|
|
1031
1056
|
class="toggle-hidden-btn"
|
|
1032
1057
|
id="toggleHiddenBtn"
|
|
@@ -1091,6 +1116,16 @@
|
|
|
1091
1116
|
<div class="context-menu-item edit-only edit-flex" id="ctxDelete" onclick="handleContextMenuAction('delete')">
|
|
1092
1117
|
🗑️ 删除
|
|
1093
1118
|
</div>
|
|
1119
|
+
<div class="context-menu-divider edit-only edit-block"></div>
|
|
1120
|
+
<div class="context-menu-item edit-only edit-flex" id="ctxCompress" onclick="handleContextMenuAction('compress')">
|
|
1121
|
+
🗜️ 压缩
|
|
1122
|
+
</div>
|
|
1123
|
+
<div class="context-menu-item edit-only edit-flex" id="ctxExtract" onclick="handleContextMenuAction('extract')">
|
|
1124
|
+
📦 解压到当前目录
|
|
1125
|
+
</div>
|
|
1126
|
+
<div class="context-menu-item edit-only edit-flex" id="ctxArchivePreview" onclick="handleContextMenuAction('archivePreview')">
|
|
1127
|
+
🔎 查看压缩包
|
|
1128
|
+
</div>
|
|
1094
1129
|
</div>
|
|
1095
1130
|
|
|
1096
1131
|
<script>
|
|
@@ -1119,6 +1154,12 @@
|
|
|
1119
1154
|
let fileGridResizeObserver = null;
|
|
1120
1155
|
let healthCheckTimer = null;
|
|
1121
1156
|
let serviceWasUnavailable = false;
|
|
1157
|
+
let explorerInitializationPromise = null;
|
|
1158
|
+
let explorerLifecycleVersion = 0;
|
|
1159
|
+
let currentArchiveJobId = null;
|
|
1160
|
+
let currentArchiveJobOperation = null;
|
|
1161
|
+
let explorerRootId = null;
|
|
1162
|
+
let archiveCapabilities = { createFormats: [], readExtensions: [] };
|
|
1122
1163
|
|
|
1123
1164
|
const EXPLORER_AUTH_STORAGE_KEY = 'mock-service-cli.file-explorer.password';
|
|
1124
1165
|
const EXPLORER_PATH_STORAGE_KEY = 'mock-service-cli.file-explorer.path';
|
|
@@ -1127,6 +1168,9 @@
|
|
|
1127
1168
|
const GRID_ROW_HEIGHT = 132;
|
|
1128
1169
|
const GRID_MIN_ITEM_WIDTH = 100;
|
|
1129
1170
|
const VIRTUAL_BUFFER_ROWS = 6;
|
|
1171
|
+
const MAX_UPLOAD_FILE_SIZE = 2 * 1024 * 1024 * 1024;
|
|
1172
|
+
const MAX_UPLOAD_BATCH_SIZE = 1024 * 1024 * 1024;
|
|
1173
|
+
const MAX_UPLOAD_BATCH_FILE_COUNT = 100;
|
|
1130
1174
|
|
|
1131
1175
|
let eventHandlers = [];
|
|
1132
1176
|
let searchDebounceTimer = null;
|
|
@@ -1170,6 +1214,12 @@
|
|
|
1170
1214
|
}
|
|
1171
1215
|
}
|
|
1172
1216
|
|
|
1217
|
+
function handlePageHide() {
|
|
1218
|
+
explorerLifecycleVersion += 1;
|
|
1219
|
+
explorerInitializationPromise = null;
|
|
1220
|
+
cleanupEventListeners();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1173
1223
|
function redirectToLogin() {
|
|
1174
1224
|
sessionStorage.removeItem(EXPLORER_AUTH_STORAGE_KEY);
|
|
1175
1225
|
window.location.replace('/__login');
|
|
@@ -1201,6 +1251,7 @@
|
|
|
1201
1251
|
}
|
|
1202
1252
|
|
|
1203
1253
|
function startHealthCheck() {
|
|
1254
|
+
if (healthCheckTimer) return;
|
|
1204
1255
|
healthCheckTimer = setInterval(async () => {
|
|
1205
1256
|
try {
|
|
1206
1257
|
const response = await explorerFetch('/__api/health');
|
|
@@ -1231,6 +1282,7 @@
|
|
|
1231
1282
|
const config = await response.json();
|
|
1232
1283
|
if (!response.ok || config.error) throw new Error(config.error || 'Failed to load configuration');
|
|
1233
1284
|
editMode = Boolean(config.editMode);
|
|
1285
|
+
explorerRootId = typeof config.rootId === 'string' ? config.rootId : null;
|
|
1234
1286
|
} catch (error) {
|
|
1235
1287
|
console.warn('Failed to load file explorer configuration:', error);
|
|
1236
1288
|
editMode = false;
|
|
@@ -1238,6 +1290,20 @@
|
|
|
1238
1290
|
syncEditModeUI();
|
|
1239
1291
|
}
|
|
1240
1292
|
|
|
1293
|
+
async function loadArchiveCapabilities() {
|
|
1294
|
+
archiveCapabilities = { createFormats: [], readExtensions: [] };
|
|
1295
|
+
if (!editMode) return;
|
|
1296
|
+
try {
|
|
1297
|
+
const capabilities = await requestJson('/__api/archive/capabilities');
|
|
1298
|
+
archiveCapabilities = {
|
|
1299
|
+
createFormats: Array.isArray(capabilities.createFormats) ? capabilities.createFormats : [],
|
|
1300
|
+
readExtensions: Array.isArray(capabilities.readExtensions) ? capabilities.readExtensions : []
|
|
1301
|
+
};
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
console.warn('Failed to load archive capabilities:', error);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1241
1307
|
function handleFileGridClick(e) {
|
|
1242
1308
|
const actionBtn = e.target.closest('[data-action]');
|
|
1243
1309
|
if (actionBtn) {
|
|
@@ -1349,46 +1415,90 @@
|
|
|
1349
1415
|
}
|
|
1350
1416
|
}
|
|
1351
1417
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
addEventListenerWithCleanup(fileGrid, 'contextmenu', handleFileGridContextmenu);
|
|
1358
|
-
addEventListenerWithCleanup(fileGrid, 'scroll', handleFileGridScroll, { passive: true });
|
|
1359
|
-
addEventListenerWithCleanup(document, 'click', handleDocumentClick);
|
|
1360
|
-
addEventListenerWithCleanup(document, 'scroll', handleDocumentScroll);
|
|
1361
|
-
addEventListenerWithCleanup(previewModal, 'click', handleModalClick);
|
|
1362
|
-
addEventListenerWithCleanup(previewModal, 'wheel', handleModalWheel, { passive: false });
|
|
1363
|
-
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
1364
|
-
|
|
1365
|
-
fileGridResizeObserver = new ResizeObserver(() => {
|
|
1366
|
-
if (virtualState) {
|
|
1367
|
-
renderFiles(currentFiles, false);
|
|
1418
|
+
function getStoredExplorerPath() {
|
|
1419
|
+
try {
|
|
1420
|
+
const stored = JSON.parse(sessionStorage.getItem(EXPLORER_PATH_STORAGE_KEY));
|
|
1421
|
+
if (stored && stored.rootId === explorerRootId && typeof stored.path === 'string' && stored.path.startsWith('/')) {
|
|
1422
|
+
return stored.path;
|
|
1368
1423
|
}
|
|
1369
|
-
})
|
|
1370
|
-
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
// Earlier releases stored the path directly; discard that unscoped value.
|
|
1426
|
+
}
|
|
1427
|
+
sessionStorage.removeItem(EXPLORER_PATH_STORAGE_KEY);
|
|
1428
|
+
return '/';
|
|
1429
|
+
}
|
|
1371
1430
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
});
|
|
1431
|
+
function storeExplorerPath(path) {
|
|
1432
|
+
if (!explorerRootId) return;
|
|
1433
|
+
sessionStorage.setItem(EXPLORER_PATH_STORAGE_KEY, JSON.stringify({ rootId: explorerRootId, path }));
|
|
1434
|
+
}
|
|
1377
1435
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1436
|
+
async function initializeExplorer() {
|
|
1437
|
+
if (explorerInitializationPromise) return explorerInitializationPromise;
|
|
1438
|
+
|
|
1439
|
+
const lifecycleVersion = explorerLifecycleVersion;
|
|
1440
|
+
const initializationPromise = (async () => {
|
|
1441
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
1442
|
+
const previewModal = document.getElementById('previewModal');
|
|
1443
|
+
|
|
1444
|
+
addEventListenerWithCleanup(fileGrid, 'click', handleFileGridClick);
|
|
1445
|
+
addEventListenerWithCleanup(fileGrid, 'contextmenu', handleFileGridContextmenu);
|
|
1446
|
+
addEventListenerWithCleanup(fileGrid, 'scroll', handleFileGridScroll, { passive: true });
|
|
1447
|
+
addEventListenerWithCleanup(document, 'click', handleDocumentClick);
|
|
1448
|
+
addEventListenerWithCleanup(document, 'scroll', handleDocumentScroll);
|
|
1449
|
+
addEventListenerWithCleanup(previewModal, 'click', handleModalClick);
|
|
1450
|
+
addEventListenerWithCleanup(previewModal, 'wheel', handleModalWheel, { passive: false });
|
|
1451
|
+
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
1452
|
+
|
|
1453
|
+
fileGridResizeObserver = new ResizeObserver(() => {
|
|
1454
|
+
if (virtualState) {
|
|
1455
|
+
renderFiles(currentFiles, false);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
fileGridResizeObserver.observe(fileGrid);
|
|
1459
|
+
|
|
1460
|
+
if (!(await verifyExplorerAuth()) || lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1461
|
+
await loadExplorerConfig();
|
|
1462
|
+
if (lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1463
|
+
await loadArchiveCapabilities();
|
|
1464
|
+
if (lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1465
|
+
startHealthCheck();
|
|
1466
|
+
await loadDirectory(getStoredExplorerPath(), { fallbackToRoot: true });
|
|
1467
|
+
})();
|
|
1468
|
+
explorerInitializationPromise = initializationPromise;
|
|
1469
|
+
|
|
1470
|
+
try {
|
|
1471
|
+
await initializationPromise;
|
|
1472
|
+
} finally {
|
|
1473
|
+
if (explorerInitializationPromise === initializationPromise) {
|
|
1474
|
+
explorerInitializationPromise = null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
document.addEventListener('DOMContentLoaded', () => initializeExplorer());
|
|
1480
|
+
|
|
1481
|
+
window.addEventListener('pagehide', handlePageHide);
|
|
1482
|
+
window.addEventListener('pageshow', event => {
|
|
1483
|
+
if (event.persisted) initializeExplorer();
|
|
1484
|
+
});
|
|
1380
1485
|
|
|
1381
1486
|
function showContextMenu(x, y, filePath, isDirectory) {
|
|
1382
1487
|
currentContextMenuFile = { path: filePath, isDirectory };
|
|
1383
1488
|
|
|
1384
1489
|
const menu = document.getElementById('contextMenu');
|
|
1385
1490
|
const ctxDownload = document.getElementById('ctxDownload');
|
|
1491
|
+
const isArchive = !isDirectory && isArchiveFile(filePath);
|
|
1492
|
+
const ctxExtract = document.getElementById('ctxExtract');
|
|
1493
|
+
const ctxArchivePreview = document.getElementById('ctxArchivePreview');
|
|
1386
1494
|
|
|
1387
1495
|
if (isDirectory) {
|
|
1388
1496
|
ctxDownload.classList.add('disabled');
|
|
1389
1497
|
} else {
|
|
1390
1498
|
ctxDownload.classList.remove('disabled');
|
|
1391
1499
|
}
|
|
1500
|
+
ctxExtract.classList.toggle('disabled', !isArchive);
|
|
1501
|
+
ctxArchivePreview.classList.toggle('disabled', !isArchive);
|
|
1392
1502
|
|
|
1393
1503
|
menu.style.left = `${x}px`;
|
|
1394
1504
|
menu.style.top = `${y}px`;
|
|
@@ -1434,12 +1544,21 @@
|
|
|
1434
1544
|
case 'delete':
|
|
1435
1545
|
deletePath(path, path.split('/').pop());
|
|
1436
1546
|
break;
|
|
1547
|
+
case 'compress':
|
|
1548
|
+
createArchiveFromSelection([path]);
|
|
1549
|
+
break;
|
|
1550
|
+
case 'extract':
|
|
1551
|
+
if (!isDirectory && isArchiveFile(path)) extractArchive(path);
|
|
1552
|
+
break;
|
|
1553
|
+
case 'archivePreview':
|
|
1554
|
+
if (!isDirectory && isArchiveFile(path)) previewArchive(path);
|
|
1555
|
+
break;
|
|
1437
1556
|
}
|
|
1438
1557
|
|
|
1439
1558
|
hideContextMenu();
|
|
1440
1559
|
}
|
|
1441
1560
|
|
|
1442
|
-
async function loadDirectory(path) {
|
|
1561
|
+
async function loadDirectory(path, { fallbackToRoot = false } = {}) {
|
|
1443
1562
|
const fileGrid = document.getElementById('fileGrid');
|
|
1444
1563
|
const oldCurrentPath = currentPath;
|
|
1445
1564
|
const requestId = ++directoryRequestId;
|
|
@@ -1463,12 +1582,14 @@
|
|
|
1463
1582
|
const data = await response.json();
|
|
1464
1583
|
|
|
1465
1584
|
if (!response.ok || data.error) {
|
|
1466
|
-
|
|
1585
|
+
const error = new Error(data.error || 'Failed to load directory');
|
|
1586
|
+
error.status = response.status;
|
|
1587
|
+
throw error;
|
|
1467
1588
|
}
|
|
1468
1589
|
if (requestId !== directoryRequestId) return;
|
|
1469
1590
|
|
|
1470
1591
|
currentPath = data.currentPath;
|
|
1471
|
-
|
|
1592
|
+
storeExplorerPath(currentPath);
|
|
1472
1593
|
allFiles = data.files || [];
|
|
1473
1594
|
|
|
1474
1595
|
if (document.getElementById('searchInput')) {
|
|
@@ -1481,6 +1602,10 @@
|
|
|
1481
1602
|
updateBackButton(data.parentPath);
|
|
1482
1603
|
} catch (error) {
|
|
1483
1604
|
if (error.name === 'AbortError' || requestId !== directoryRequestId) return;
|
|
1605
|
+
if (fallbackToRoot && path !== '/' && error.status === 404) {
|
|
1606
|
+
sessionStorage.removeItem(EXPLORER_PATH_STORAGE_KEY);
|
|
1607
|
+
return loadDirectory('/', { fallbackToRoot: false });
|
|
1608
|
+
}
|
|
1484
1609
|
console.error('Error loading directory:', error);
|
|
1485
1610
|
currentPath = oldCurrentPath;
|
|
1486
1611
|
const parentPath = currentPath === '/' ? null : currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
|
@@ -1640,27 +1765,205 @@
|
|
|
1640
1765
|
return data;
|
|
1641
1766
|
}
|
|
1642
1767
|
|
|
1643
|
-
|
|
1644
|
-
const
|
|
1645
|
-
|
|
1768
|
+
function isArchiveFile(filePath) {
|
|
1769
|
+
const lowerPath = String(filePath || '').toLowerCase();
|
|
1770
|
+
return archiveCapabilities.readExtensions.some(extension => lowerPath.endsWith(`.${String(extension).toLowerCase()}`));
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
async function createArchiveFromSelection(sourceOverride) {
|
|
1774
|
+
const sources = sourceOverride || [...selectedPaths];
|
|
1775
|
+
if (!sources.length) {
|
|
1776
|
+
showToast('请先选择要压缩的文件或文件夹');
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const defaultName = sources.length === 1 ? sources[0].split('/').pop().replace(/\.[^.]+$/, '') : 'archive';
|
|
1780
|
+
const name = window.prompt('压缩包名称', defaultName);
|
|
1781
|
+
if (name === null || !name.trim()) return;
|
|
1782
|
+
const formats = archiveCapabilities.createFormats;
|
|
1783
|
+
if (!formats.length) {
|
|
1784
|
+
showToast('当前版本未提供压缩格式');
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const format = window.prompt(`压缩格式(${formats.join(' 或 ')})`, formats[0]);
|
|
1788
|
+
if (format === null) return;
|
|
1789
|
+
if (!formats.includes(format.trim())) {
|
|
1790
|
+
alert(`仅支持 ${formats.join(' 或 ')}`);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
await startArchiveJob({
|
|
1794
|
+
operation: 'create',
|
|
1795
|
+
sources,
|
|
1796
|
+
destinationPath: currentPath,
|
|
1797
|
+
name: name.trim(),
|
|
1798
|
+
format: format.trim()
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
async function extractArchive(filePath) {
|
|
1803
|
+
const confirmed = window.confirm(`将 “${filePath.split('/').pop()}” 解压到当前目录吗?已有同名文件将导致整个操作失败。`);
|
|
1804
|
+
if (!confirmed) return;
|
|
1805
|
+
await startArchiveJob({ operation: 'extract', path: filePath, destinationPath: currentPath });
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async function startArchiveJob(payload) {
|
|
1809
|
+
const operationName = payload.operation === 'extract' ? '解压' : '压缩';
|
|
1810
|
+
try {
|
|
1811
|
+
const job = await requestJson('/__api/archive/jobs', {
|
|
1812
|
+
method: 'POST',
|
|
1813
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1814
|
+
body: JSON.stringify(payload)
|
|
1815
|
+
});
|
|
1816
|
+
currentArchiveJobId = job.id;
|
|
1817
|
+
currentArchiveJobOperation = job.type || payload.operation;
|
|
1818
|
+
monitorArchiveJob(job.id, currentArchiveJobOperation);
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
alert(`${operationName}无法启动: ${error.message}`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function archiveOperationName(operation) {
|
|
1825
|
+
return operation === 'extract' ? '解压' : '压缩';
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
async function monitorArchiveJob(id, operation) {
|
|
1829
|
+
const modal = document.getElementById('previewModal');
|
|
1830
|
+
const title = document.getElementById('previewTitle');
|
|
1831
|
+
const body = document.getElementById('previewBody');
|
|
1832
|
+
const operationName = archiveOperationName(operation || currentArchiveJobOperation);
|
|
1833
|
+
title.textContent = `${operationName}任务`;
|
|
1834
|
+
modal.classList.add('active');
|
|
1835
|
+
try {
|
|
1836
|
+
const job = await requestJson(`/__api/archive/jobs/${encodeURIComponent(id)}`);
|
|
1837
|
+
const jobOperationName = archiveOperationName(job.type || operation || currentArchiveJobOperation);
|
|
1838
|
+
title.textContent = `${jobOperationName}任务`;
|
|
1839
|
+
const progress = job.progress || {};
|
|
1840
|
+
body.innerHTML = `
|
|
1841
|
+
<div class="preview-container">
|
|
1842
|
+
<p>状态:${escapeHtml(job.status)}</p>
|
|
1843
|
+
<p>进度:${Number(progress.processedEntries || 0)} / ${Number(progress.totalEntries || 0)} 项</p>
|
|
1844
|
+
${job.status === 'running' || job.status === 'queued' ? '<button class="toolbar-action-btn danger" onclick="cancelArchiveJob()">取消任务</button>' : ''}
|
|
1845
|
+
${job.error ? `<p style="color:#d32f2f">${escapeHtml(job.error)}</p>` : ''}
|
|
1846
|
+
</div>
|
|
1847
|
+
`;
|
|
1848
|
+
if (job.status === 'running' || job.status === 'queued') {
|
|
1849
|
+
setTimeout(() => {
|
|
1850
|
+
if (currentArchiveJobId === id) monitorArchiveJob(id, job.type || operation);
|
|
1851
|
+
}, 500);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
currentArchiveJobId = null;
|
|
1855
|
+
currentArchiveJobOperation = null;
|
|
1856
|
+
if (job.status === 'completed') {
|
|
1857
|
+
showToast(`${jobOperationName}完成`);
|
|
1858
|
+
await loadDirectory(currentPath);
|
|
1859
|
+
}
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
currentArchiveJobId = null;
|
|
1862
|
+
currentArchiveJobOperation = null;
|
|
1863
|
+
body.innerHTML = `<div class="empty-state"><p>${escapeHtml(operationName)}失败: ${escapeHtml(error.message)}</p></div>`;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
async function cancelArchiveJob() {
|
|
1868
|
+
if (!currentArchiveJobId) return;
|
|
1869
|
+
try {
|
|
1870
|
+
await requestJson(`/__api/archive/jobs/${encodeURIComponent(currentArchiveJobId)}`, { method: 'DELETE' });
|
|
1871
|
+
showToast(`正在取消${archiveOperationName(currentArchiveJobOperation)}任务`);
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
alert(`取消任务失败: ${error.message}`);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
async function previewArchive(filePath) {
|
|
1878
|
+
const modal = document.getElementById('previewModal');
|
|
1879
|
+
const title = document.getElementById('previewTitle');
|
|
1880
|
+
const body = document.getElementById('previewBody');
|
|
1881
|
+
title.textContent = `${filePath.split('/').pop()} - 压缩包内容`;
|
|
1882
|
+
body.innerHTML = '<div class="loading"><div class="spinner"></div><p>读取压缩包目录...</p></div>';
|
|
1883
|
+
modal.classList.add('active');
|
|
1884
|
+
try {
|
|
1885
|
+
const preview = await requestJson(`/__api/archive/preview?path=${encodeURIComponent(filePath)}`);
|
|
1886
|
+
const entries = preview.entries || [];
|
|
1887
|
+
body.innerHTML = `
|
|
1888
|
+
<div class="preview-container">
|
|
1889
|
+
<div class="binary-info">${entries.length} 项 · 解压后 ${formatSize(preview.totalSize || 0)} · 包内数据 ${formatSize(preview.totalPackedSize || 0)}</div>
|
|
1890
|
+
<div class="text-preview">${entries
|
|
1891
|
+
.map(entry => {
|
|
1892
|
+
const depth = entry.path.split('/').length - 1;
|
|
1893
|
+
const label = `${entry.isDirectory ? '📁' : '📄'} ${entry.path.split('/').pop()}${entry.isDirectory ? '/' : ` (${formatSize(entry.size || 0)})`}`;
|
|
1894
|
+
return `<div style="padding-left:${depth * 16}px">${escapeHtml(label)}</div>`;
|
|
1895
|
+
})
|
|
1896
|
+
.join('')}</div>
|
|
1897
|
+
</div>
|
|
1898
|
+
`;
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
body.innerHTML = `<div class="empty-state"><p>读取压缩包失败: ${escapeHtml(error.message)}</p></div>`;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function createUploadBatches(files) {
|
|
1905
|
+
const batches = [];
|
|
1906
|
+
const failed = [];
|
|
1907
|
+
let batch = [];
|
|
1908
|
+
let batchSize = 0;
|
|
1909
|
+
|
|
1910
|
+
Array.from(files).forEach(file => {
|
|
1911
|
+
if (file.size > MAX_UPLOAD_FILE_SIZE) {
|
|
1912
|
+
failed.push({ name: file.webkitRelativePath || file.name, error: '单文件超过 2GB 限制' });
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const exceedsBatchLimit =
|
|
1916
|
+
batch.length >= MAX_UPLOAD_BATCH_FILE_COUNT || batchSize + file.size > MAX_UPLOAD_BATCH_SIZE;
|
|
1917
|
+
if (batch.length && exceedsBatchLimit) {
|
|
1918
|
+
batches.push(batch);
|
|
1919
|
+
batch = [];
|
|
1920
|
+
batchSize = 0;
|
|
1921
|
+
}
|
|
1922
|
+
batch.push(file);
|
|
1923
|
+
batchSize += file.size;
|
|
1924
|
+
});
|
|
1925
|
+
if (batch.length) batches.push(batch);
|
|
1926
|
+
return { batches, failed };
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
async function uploadBatch(files) {
|
|
1646
1930
|
const formData = new FormData();
|
|
1647
1931
|
formData.append('parentPath', currentPath);
|
|
1648
|
-
|
|
1932
|
+
files.forEach(file => {
|
|
1649
1933
|
formData.append('files', file, file.webkitRelativePath || file.name);
|
|
1650
1934
|
});
|
|
1651
1935
|
|
|
1936
|
+
const response = await explorerFetch('/__api/upload', { method: 'POST', body: formData });
|
|
1937
|
+
const result = await response.json();
|
|
1938
|
+
if (!response.ok && response.status !== 207) throw new Error(result.error || '上传失败');
|
|
1939
|
+
return result;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
async function uploadSelectedFiles(files) {
|
|
1943
|
+
const input = document.activeElement;
|
|
1944
|
+
if (!files || files.length === 0) return;
|
|
1945
|
+
const { batches, failed } = createUploadBatches(files);
|
|
1946
|
+
const uploaded = [];
|
|
1947
|
+
|
|
1652
1948
|
try {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1949
|
+
for (let index = 0; index < batches.length; index++) {
|
|
1950
|
+
const batch = batches[index];
|
|
1951
|
+
showToast(`正在上传第 ${index + 1}/${batches.length} 批(${batch.length} 项)`);
|
|
1952
|
+
try {
|
|
1953
|
+
const result = await uploadBatch(batch);
|
|
1954
|
+
uploaded.push(...result.uploaded);
|
|
1955
|
+
failed.push(...result.failed);
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
batch.forEach(file => failed.push({ name: file.webkitRelativePath || file.name, error: error.message }));
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1656
1961
|
const messages = [];
|
|
1657
|
-
if (
|
|
1658
|
-
if (
|
|
1962
|
+
if (uploaded.length) messages.push(`已上传 ${uploaded.length} 项`);
|
|
1963
|
+
if (failed.length) messages.push(`${failed.length} 项未上传`);
|
|
1659
1964
|
showToast(messages.join(';'));
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
console.error('上传失败:', error);
|
|
1663
|
-
alert(`上传失败: ${error.message}`);
|
|
1965
|
+
if (failed.length) console.warn('上传失败项:', failed);
|
|
1966
|
+
if (uploaded.length) await loadDirectory(currentPath);
|
|
1664
1967
|
} finally {
|
|
1665
1968
|
document.getElementById('uploadFilesInput').value = '';
|
|
1666
1969
|
document.getElementById('uploadFolderInput').value = '';
|
|
@@ -1821,11 +2124,16 @@
|
|
|
1821
2124
|
|
|
1822
2125
|
function updateSelectionToolbar() {
|
|
1823
2126
|
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
|
2127
|
+
const archiveSelectedBtn = document.getElementById('archiveSelectedBtn');
|
|
1824
2128
|
if (!deleteSelectedBtn) return;
|
|
1825
2129
|
|
|
1826
2130
|
const count = selectedPaths.size;
|
|
1827
2131
|
deleteSelectedBtn.disabled = count === 0;
|
|
1828
2132
|
deleteSelectedBtn.textContent = count ? `删除选中(${count})` : '删除选中';
|
|
2133
|
+
if (archiveSelectedBtn) {
|
|
2134
|
+
archiveSelectedBtn.disabled = count === 0;
|
|
2135
|
+
archiveSelectedBtn.textContent = count ? `压缩选中(${count})` : '压缩选中';
|
|
2136
|
+
}
|
|
1829
2137
|
}
|
|
1830
2138
|
|
|
1831
2139
|
function renderBreadcrumb(currentPath) {
|
|
@@ -2393,6 +2701,10 @@
|
|
|
2393
2701
|
currentImageIndex = findCurrentImageIndex(path);
|
|
2394
2702
|
|
|
2395
2703
|
try {
|
|
2704
|
+
if (isArchiveFile(path)) {
|
|
2705
|
+
await previewArchive(path);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2396
2708
|
const ext = path.split('.').pop().toLowerCase();
|
|
2397
2709
|
|
|
2398
2710
|
const fileInfo = await getFileInfo(path);
|
|
@@ -2452,7 +2764,9 @@
|
|
|
2452
2764
|
</div>
|
|
2453
2765
|
</div>
|
|
2454
2766
|
`;
|
|
2455
|
-
document
|
|
2767
|
+
document
|
|
2768
|
+
.getElementById('previewDownloadBtn')
|
|
2769
|
+
.addEventListener('click', () => downloadFile(path, path.split('/').pop()));
|
|
2456
2770
|
} else {
|
|
2457
2771
|
if (isLargeFile) {
|
|
2458
2772
|
body.innerHTML = `
|
|
@@ -2468,7 +2782,9 @@
|
|
|
2468
2782
|
</div>
|
|
2469
2783
|
</div>
|
|
2470
2784
|
`;
|
|
2471
|
-
document
|
|
2785
|
+
document
|
|
2786
|
+
.getElementById('previewDownloadBtn')
|
|
2787
|
+
.addEventListener('click', () => downloadFile(path, path.split('/').pop()));
|
|
2472
2788
|
} else {
|
|
2473
2789
|
const response = await explorerFetch(`/__api/file?path=${encodeURIComponent(path)}`);
|
|
2474
2790
|
const contentType = response.headers.get('content-type');
|
|
@@ -2549,6 +2865,8 @@
|
|
|
2549
2865
|
|
|
2550
2866
|
function closeModal() {
|
|
2551
2867
|
document.getElementById('previewModal').classList.remove('active');
|
|
2868
|
+
currentArchiveJobId = null;
|
|
2869
|
+
currentArchiveJobOperation = null;
|
|
2552
2870
|
releasePreviewObjectUrl();
|
|
2553
2871
|
}
|
|
2554
2872
|
|