mock-service-cli 4.2.0 → 4.3.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 +12 -1
- package/dist/file-explorer.html +559 -216
- package/dist/runtime.js +10 -10
- package/package.json +5 -2
package/dist/file-explorer.html
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
box-sizing: border-box;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.icon-sprite {
|
|
15
|
+
position: absolute;
|
|
16
|
+
width: 0;
|
|
17
|
+
height: 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
body {
|
|
15
22
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
16
23
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
@@ -83,6 +90,7 @@
|
|
|
83
90
|
flex: 1;
|
|
84
91
|
display: flex;
|
|
85
92
|
flex-direction: column;
|
|
93
|
+
min-height: 0;
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
.toolbar {
|
|
@@ -180,56 +188,6 @@
|
|
|
180
188
|
cursor: not-allowed;
|
|
181
189
|
}
|
|
182
190
|
|
|
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
191
|
body:not(.editing) .edit-only {
|
|
234
192
|
display: none !important;
|
|
235
193
|
}
|
|
@@ -384,18 +342,36 @@
|
|
|
384
342
|
}
|
|
385
343
|
|
|
386
344
|
.file-grid {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
390
|
-
gap: 8px;
|
|
345
|
+
--list-row-height: 37px;
|
|
346
|
+
position: relative;
|
|
391
347
|
overflow: auto;
|
|
348
|
+
flex: 1;
|
|
349
|
+
min-height: 0;
|
|
392
350
|
}
|
|
393
351
|
|
|
394
|
-
.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
352
|
+
.virtual-spacer {
|
|
353
|
+
width: 1px;
|
|
354
|
+
pointer-events: none;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.virtual-content {
|
|
358
|
+
position: absolute;
|
|
359
|
+
top: 0;
|
|
360
|
+
left: 0;
|
|
361
|
+
right: 0;
|
|
362
|
+
overflow-anchor: none;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.virtual-content.grid-view {
|
|
366
|
+
display: block;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.virtual-content.list-view {
|
|
370
|
+
display: block;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.virtual-content .file-item {
|
|
374
|
+
position: absolute;
|
|
399
375
|
}
|
|
400
376
|
|
|
401
377
|
.file-item {
|
|
@@ -405,7 +381,11 @@
|
|
|
405
381
|
padding: 10px 8px;
|
|
406
382
|
border-radius: 6px;
|
|
407
383
|
cursor: pointer;
|
|
408
|
-
transition:
|
|
384
|
+
transition: background-color 0.2s;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.grid-view .file-item {
|
|
388
|
+
height: 124px;
|
|
409
389
|
}
|
|
410
390
|
|
|
411
391
|
.file-item:hover {
|
|
@@ -423,7 +403,7 @@
|
|
|
423
403
|
border-bottom: 1px solid #f0f0f0;
|
|
424
404
|
border-radius: 0;
|
|
425
405
|
min-height: auto;
|
|
426
|
-
height:
|
|
406
|
+
height: var(--list-row-height);
|
|
427
407
|
}
|
|
428
408
|
|
|
429
409
|
.file-icon {
|
|
@@ -479,10 +459,18 @@
|
|
|
479
459
|
line-height: 1.3;
|
|
480
460
|
}
|
|
481
461
|
|
|
462
|
+
.grid-view .file-name {
|
|
463
|
+
display: -webkit-box;
|
|
464
|
+
-webkit-box-orient: vertical;
|
|
465
|
+
-webkit-line-clamp: 2;
|
|
466
|
+
overflow: hidden;
|
|
467
|
+
}
|
|
468
|
+
|
|
482
469
|
.list-view .file-name {
|
|
483
470
|
margin-bottom: 0;
|
|
484
471
|
flex: 1;
|
|
485
472
|
font-size: 12px;
|
|
473
|
+
min-width: 0;
|
|
486
474
|
}
|
|
487
475
|
|
|
488
476
|
.file-meta {
|
|
@@ -491,6 +479,11 @@
|
|
|
491
479
|
}
|
|
492
480
|
.birthtime {
|
|
493
481
|
padding-right: 4px;
|
|
482
|
+
display: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.birthtime.visible {
|
|
486
|
+
display: inline-block;
|
|
494
487
|
}
|
|
495
488
|
|
|
496
489
|
.list-view .file-meta {
|
|
@@ -499,6 +492,11 @@
|
|
|
499
492
|
font-size: 10px;
|
|
500
493
|
}
|
|
501
494
|
|
|
495
|
+
.date-compact,
|
|
496
|
+
.more-actions-btn {
|
|
497
|
+
display: none;
|
|
498
|
+
}
|
|
499
|
+
|
|
502
500
|
.hidden-file {
|
|
503
501
|
opacity: 0.6;
|
|
504
502
|
}
|
|
@@ -509,7 +507,50 @@
|
|
|
509
507
|
}
|
|
510
508
|
|
|
511
509
|
.list-view .action-buttons {
|
|
512
|
-
display:
|
|
510
|
+
display: none;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.list-view .more-actions-btn {
|
|
514
|
+
display: inline-flex;
|
|
515
|
+
flex: 0 0 32px;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
align-self: center;
|
|
519
|
+
width: 32px;
|
|
520
|
+
height: 28px;
|
|
521
|
+
margin-left: 8px;
|
|
522
|
+
padding: 0;
|
|
523
|
+
border: 1px solid transparent;
|
|
524
|
+
border-radius: 5px;
|
|
525
|
+
background: #f1f3f5;
|
|
526
|
+
color: #555;
|
|
527
|
+
font-size: 20px;
|
|
528
|
+
line-height: 1;
|
|
529
|
+
cursor: pointer;
|
|
530
|
+
touch-action: manipulation;
|
|
531
|
+
-webkit-tap-highlight-color: transparent;
|
|
532
|
+
transition:
|
|
533
|
+
background-color 120ms ease,
|
|
534
|
+
border-color 120ms ease,
|
|
535
|
+
color 120ms ease,
|
|
536
|
+
transform 120ms ease;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.list-view .more-actions-btn:hover {
|
|
540
|
+
border-color: #d9dce1;
|
|
541
|
+
background: #e5e7eb;
|
|
542
|
+
color: #222;
|
|
543
|
+
transform: scale(1.06);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.list-view .more-actions-btn:active {
|
|
547
|
+
background: #d9dce1;
|
|
548
|
+
transform: scale(0.92);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.list-view .more-actions-btn:focus-visible {
|
|
552
|
+
outline: 2px solid #667eea;
|
|
553
|
+
outline-offset: 1px;
|
|
513
554
|
}
|
|
514
555
|
|
|
515
556
|
.action-btn {
|
|
@@ -546,6 +587,90 @@
|
|
|
546
587
|
color: white;
|
|
547
588
|
}
|
|
548
589
|
|
|
590
|
+
@media (max-width: 767px) {
|
|
591
|
+
.file-grid.list-view {
|
|
592
|
+
--list-row-height: 48px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.file-grid.list-view.show-birthtime {
|
|
596
|
+
--list-row-height: 65px;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.list-view .file-item {
|
|
600
|
+
align-items: flex-start;
|
|
601
|
+
padding: 7px 10px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.list-view .file-icon {
|
|
605
|
+
flex: 0 0 20px;
|
|
606
|
+
margin-top: 2px;
|
|
607
|
+
margin-right: 7px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.list-view .file-select {
|
|
611
|
+
flex: 0 0 auto;
|
|
612
|
+
margin: 3px 7px 0 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.list-view .file-info {
|
|
616
|
+
display: flex;
|
|
617
|
+
flex: 1;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
align-items: stretch;
|
|
620
|
+
gap: 3px;
|
|
621
|
+
min-width: 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.list-view .file-name {
|
|
625
|
+
width: 100%;
|
|
626
|
+
overflow: hidden;
|
|
627
|
+
text-overflow: ellipsis;
|
|
628
|
+
white-space: nowrap;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.list-view .file-meta {
|
|
632
|
+
display: flex;
|
|
633
|
+
width: 100%;
|
|
634
|
+
gap: 3px;
|
|
635
|
+
flex-wrap: wrap;
|
|
636
|
+
overflow: hidden;
|
|
637
|
+
white-space: nowrap;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.list-view .file-meta > span {
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
text-overflow: ellipsis;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.date-full {
|
|
646
|
+
display: none;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.date-compact {
|
|
650
|
+
display: inline;
|
|
651
|
+
vertical-align: middle;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.list-view .birthtime.visible {
|
|
655
|
+
flex-basis: 100%;
|
|
656
|
+
display: block;
|
|
657
|
+
padding-right: 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.list-view .action-buttons {
|
|
661
|
+
display: none !important;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.list-view .more-actions-btn {
|
|
665
|
+
align-self: flex-start;
|
|
666
|
+
margin: 10px 0 0 6px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.file-grid.list-view.show-birthtime .more-actions-btn {
|
|
670
|
+
margin-top: 17px;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
549
674
|
.modal {
|
|
550
675
|
display: none;
|
|
551
676
|
position: fixed;
|
|
@@ -851,6 +976,15 @@
|
|
|
851
976
|
</style>
|
|
852
977
|
</head>
|
|
853
978
|
<body>
|
|
979
|
+
<svg class="icon-sprite" aria-hidden="true" focusable="false">
|
|
980
|
+
<symbol id="icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
981
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
982
|
+
</symbol>
|
|
983
|
+
<symbol id="icon-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
984
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
985
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
986
|
+
</symbol>
|
|
987
|
+
</svg>
|
|
854
988
|
<div class="container">
|
|
855
989
|
<div class="header">
|
|
856
990
|
<h1>📁 文件浏览器</h1>
|
|
@@ -866,11 +1000,6 @@
|
|
|
866
1000
|
返回上级
|
|
867
1001
|
</button>
|
|
868
1002
|
<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
1003
|
<button
|
|
875
1004
|
class="toolbar-action-btn edit-only edit-inline"
|
|
876
1005
|
onclick="createEntry('directory')"
|
|
@@ -967,6 +1096,7 @@
|
|
|
967
1096
|
let showHiddenFiles = true;
|
|
968
1097
|
let showBirthtime = false;
|
|
969
1098
|
let currentPreviewContent = '';
|
|
1099
|
+
let currentPreviewObjectUrl = null;
|
|
970
1100
|
let currentZoom = 1;
|
|
971
1101
|
let currentRotation = 0;
|
|
972
1102
|
let currentContextMenuFile = null;
|
|
@@ -974,6 +1104,16 @@
|
|
|
974
1104
|
let imageFiles = [];
|
|
975
1105
|
let selectedPaths = new Set();
|
|
976
1106
|
let editMode = false;
|
|
1107
|
+
let directoryRequestController = null;
|
|
1108
|
+
let directoryRequestId = 0;
|
|
1109
|
+
let virtualRenderFrame = null;
|
|
1110
|
+
let virtualState = null;
|
|
1111
|
+
let fileGridResizeObserver = null;
|
|
1112
|
+
|
|
1113
|
+
const LIST_ROW_HEIGHT = 37;
|
|
1114
|
+
const GRID_ROW_HEIGHT = 132;
|
|
1115
|
+
const GRID_MIN_ITEM_WIDTH = 100;
|
|
1116
|
+
const VIRTUAL_BUFFER_ROWS = 6;
|
|
977
1117
|
|
|
978
1118
|
let eventHandlers = [];
|
|
979
1119
|
let searchDebounceTimer = null;
|
|
@@ -999,20 +1139,22 @@
|
|
|
999
1139
|
element.removeEventListener(event, handler, options);
|
|
1000
1140
|
});
|
|
1001
1141
|
eventHandlers = [];
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1142
|
+
if (virtualRenderFrame) {
|
|
1143
|
+
cancelAnimationFrame(virtualRenderFrame);
|
|
1144
|
+
virtualRenderFrame = null;
|
|
1145
|
+
}
|
|
1146
|
+
if (directoryRequestController) {
|
|
1147
|
+
directoryRequestController.abort();
|
|
1148
|
+
directoryRequestController = null;
|
|
1149
|
+
}
|
|
1150
|
+
if (fileGridResizeObserver) {
|
|
1151
|
+
fileGridResizeObserver.disconnect();
|
|
1152
|
+
fileGridResizeObserver = null;
|
|
1153
|
+
}
|
|
1006
1154
|
}
|
|
1007
1155
|
|
|
1008
1156
|
function syncEditModeUI() {
|
|
1009
|
-
|
|
1010
|
-
const toggle = document.getElementById('editModeToggle');
|
|
1011
|
-
|
|
1012
|
-
body.classList.toggle('editing', editMode);
|
|
1013
|
-
if (toggle) {
|
|
1014
|
-
toggle.checked = editMode;
|
|
1015
|
-
}
|
|
1157
|
+
document.body.classList.toggle('editing', editMode);
|
|
1016
1158
|
|
|
1017
1159
|
if (!editMode) {
|
|
1018
1160
|
selectedPaths.clear();
|
|
@@ -1023,10 +1165,16 @@
|
|
|
1023
1165
|
updateSelectionToolbar();
|
|
1024
1166
|
}
|
|
1025
1167
|
|
|
1026
|
-
function
|
|
1027
|
-
|
|
1168
|
+
async function loadExplorerConfig() {
|
|
1169
|
+
try {
|
|
1170
|
+
const response = await fetch('/__api/config');
|
|
1171
|
+
const config = await response.json();
|
|
1172
|
+
editMode = Boolean(config.editMode);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
console.warn('Failed to load file explorer configuration:', error);
|
|
1175
|
+
editMode = false;
|
|
1176
|
+
}
|
|
1028
1177
|
syncEditModeUI();
|
|
1029
|
-
processAndRenderFiles();
|
|
1030
1178
|
}
|
|
1031
1179
|
|
|
1032
1180
|
function handleFileGridClick(e) {
|
|
@@ -1056,6 +1204,12 @@
|
|
|
1056
1204
|
case 'toggleSelect':
|
|
1057
1205
|
toggleSelectedPath(path, actionBtn.checked, actionBtn.closest('.file-item'));
|
|
1058
1206
|
break;
|
|
1207
|
+
case 'more': {
|
|
1208
|
+
const fileItem = actionBtn.closest('.file-item');
|
|
1209
|
+
const rect = actionBtn.getBoundingClientRect();
|
|
1210
|
+
showContextMenu(rect.right, rect.bottom, fileItem.dataset.path, fileItem.dataset.isDirectory === 'true');
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1059
1213
|
}
|
|
1060
1214
|
return;
|
|
1061
1215
|
}
|
|
@@ -1086,6 +1240,11 @@
|
|
|
1086
1240
|
hideContextMenu();
|
|
1087
1241
|
}
|
|
1088
1242
|
|
|
1243
|
+
function handleFileGridScroll() {
|
|
1244
|
+
hideContextMenu();
|
|
1245
|
+
scheduleVirtualRender();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1089
1248
|
function handleModalClick(e) {
|
|
1090
1249
|
if (e.target === e.currentTarget) {
|
|
1091
1250
|
closeModal();
|
|
@@ -1129,21 +1288,28 @@
|
|
|
1129
1288
|
}
|
|
1130
1289
|
}
|
|
1131
1290
|
|
|
1132
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
1133
|
-
initializeEditMode();
|
|
1134
|
-
syncEditModeUI();
|
|
1135
|
-
loadDirectory('/');
|
|
1136
|
-
|
|
1291
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
1137
1292
|
const fileGrid = document.getElementById('fileGrid');
|
|
1138
1293
|
const previewModal = document.getElementById('previewModal');
|
|
1139
1294
|
|
|
1140
1295
|
addEventListenerWithCleanup(fileGrid, 'click', handleFileGridClick);
|
|
1141
1296
|
addEventListenerWithCleanup(fileGrid, 'contextmenu', handleFileGridContextmenu);
|
|
1297
|
+
addEventListenerWithCleanup(fileGrid, 'scroll', handleFileGridScroll, { passive: true });
|
|
1142
1298
|
addEventListenerWithCleanup(document, 'click', handleDocumentClick);
|
|
1143
1299
|
addEventListenerWithCleanup(document, 'scroll', handleDocumentScroll);
|
|
1144
1300
|
addEventListenerWithCleanup(previewModal, 'click', handleModalClick);
|
|
1145
1301
|
addEventListenerWithCleanup(previewModal, 'wheel', handleModalWheel, { passive: false });
|
|
1146
1302
|
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
1303
|
+
|
|
1304
|
+
fileGridResizeObserver = new ResizeObserver(() => {
|
|
1305
|
+
if (virtualState) {
|
|
1306
|
+
renderFiles(currentFiles, false);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
fileGridResizeObserver.observe(fileGrid);
|
|
1310
|
+
|
|
1311
|
+
await loadExplorerConfig();
|
|
1312
|
+
loadDirectory('/');
|
|
1147
1313
|
});
|
|
1148
1314
|
|
|
1149
1315
|
window.addEventListener('beforeunload', cleanupEventListeners);
|
|
@@ -1211,56 +1377,48 @@
|
|
|
1211
1377
|
}
|
|
1212
1378
|
|
|
1213
1379
|
async function loadDirectory(path) {
|
|
1214
|
-
// console.log('Loading directory:', path);
|
|
1215
1380
|
const fileGrid = document.getElementById('fileGrid');
|
|
1216
|
-
|
|
1217
|
-
// 保存旧路径,以便失败时可以回退
|
|
1218
1381
|
const oldCurrentPath = currentPath;
|
|
1382
|
+
const requestId = ++directoryRequestId;
|
|
1383
|
+
|
|
1384
|
+
if (directoryRequestController) {
|
|
1385
|
+
directoryRequestController.abort();
|
|
1386
|
+
}
|
|
1387
|
+
directoryRequestController = new AbortController();
|
|
1219
1388
|
|
|
1220
|
-
// 重置所有状态
|
|
1221
1389
|
allFiles = [];
|
|
1222
1390
|
currentFiles = [];
|
|
1223
1391
|
selectedPaths.clear();
|
|
1224
1392
|
updateSelectionToolbar();
|
|
1225
1393
|
|
|
1226
|
-
// 显示加载状态
|
|
1227
1394
|
fileGrid.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
|
|
1228
1395
|
|
|
1229
1396
|
try {
|
|
1230
|
-
const response = await fetch(`/__api/list?path=${encodeURIComponent(path)}
|
|
1231
|
-
|
|
1232
|
-
|
|
1397
|
+
const response = await fetch(`/__api/list?path=${encodeURIComponent(path)}`, {
|
|
1398
|
+
signal: directoryRequestController.signal
|
|
1399
|
+
});
|
|
1233
1400
|
const data = await response.json();
|
|
1234
|
-
// console.log('API response data:', data);
|
|
1235
1401
|
|
|
1236
|
-
if (data.error) {
|
|
1402
|
+
if (!response.ok || data.error) {
|
|
1237
1403
|
throw new Error(data.error);
|
|
1238
1404
|
}
|
|
1405
|
+
if (requestId !== directoryRequestId) return;
|
|
1239
1406
|
|
|
1240
1407
|
currentPath = data.currentPath;
|
|
1241
1408
|
allFiles = data.files || [];
|
|
1242
1409
|
|
|
1243
|
-
// console.log('allFiles set to:', allFiles);
|
|
1244
|
-
|
|
1245
|
-
renderBreadcrumb(data.currentPath, data.parentPath);
|
|
1246
|
-
|
|
1247
|
-
// 确保在调用 processAndRenderFiles 之前 allFiles 已经设置好
|
|
1248
|
-
setTimeout(() => {
|
|
1249
|
-
processAndRenderFiles();
|
|
1250
|
-
}, 10);
|
|
1251
|
-
|
|
1252
|
-
updateBackButton(data.parentPath);
|
|
1253
|
-
|
|
1254
|
-
// 清空搜索框
|
|
1255
1410
|
if (document.getElementById('searchInput')) {
|
|
1256
1411
|
document.getElementById('searchInput').value = '';
|
|
1257
1412
|
}
|
|
1258
1413
|
searchQuery = '';
|
|
1414
|
+
|
|
1415
|
+
renderBreadcrumb(data.currentPath, data.parentPath);
|
|
1416
|
+
processAndRenderFiles();
|
|
1417
|
+
updateBackButton(data.parentPath);
|
|
1259
1418
|
} catch (error) {
|
|
1419
|
+
if (error.name === 'AbortError' || requestId !== directoryRequestId) return;
|
|
1260
1420
|
console.error('Error loading directory:', error);
|
|
1261
|
-
// 回退到旧路径
|
|
1262
1421
|
currentPath = oldCurrentPath;
|
|
1263
|
-
// 计算旧路径的父路径
|
|
1264
1422
|
const parentPath = currentPath === '/' ? null : currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
|
1265
1423
|
updateBackButton(parentPath);
|
|
1266
1424
|
fileGrid.innerHTML = `<div class="empty-state"><p>错误: ${error.message}</p></div>`;
|
|
@@ -1569,129 +1727,286 @@
|
|
|
1569
1727
|
deleteSelectedBtn.textContent = count ? `删除选中(${count})` : '删除选中';
|
|
1570
1728
|
}
|
|
1571
1729
|
|
|
1572
|
-
function renderBreadcrumb(currentPath
|
|
1730
|
+
function renderBreadcrumb(currentPath) {
|
|
1573
1731
|
const breadcrumb = document.getElementById('breadcrumb');
|
|
1574
1732
|
const parts = currentPath.split('/').filter(Boolean);
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1733
|
+
const fragment = document.createDocumentFragment();
|
|
1734
|
+
const root = document.createElement('div');
|
|
1735
|
+
root.className = 'breadcrumb-item';
|
|
1736
|
+
root.textContent = '根目录';
|
|
1737
|
+
root.addEventListener('click', () => loadDirectory('/'));
|
|
1738
|
+
fragment.appendChild(root);
|
|
1578
1739
|
let current = '';
|
|
1579
1740
|
parts.forEach((part, index) => {
|
|
1580
1741
|
current += '/' + part;
|
|
1581
1742
|
const isLast = index === parts.length - 1;
|
|
1582
|
-
|
|
1743
|
+
const separator = document.createElement('span');
|
|
1744
|
+
separator.className = 'breadcrumb-separator';
|
|
1745
|
+
separator.textContent = '/';
|
|
1746
|
+
fragment.appendChild(separator);
|
|
1747
|
+
const item = document.createElement('div');
|
|
1748
|
+
item.className = `breadcrumb-item${isLast ? ' current' : ''}`;
|
|
1749
|
+
item.textContent = part;
|
|
1583
1750
|
if (isLast) {
|
|
1584
|
-
html += `<div class="breadcrumb-item current">${escapeHtml(part)}</div>`;
|
|
1585
1751
|
} else {
|
|
1586
|
-
|
|
1752
|
+
const targetPath = current;
|
|
1753
|
+
item.addEventListener('click', () => loadDirectory(targetPath));
|
|
1587
1754
|
}
|
|
1755
|
+
fragment.appendChild(item);
|
|
1588
1756
|
});
|
|
1589
|
-
|
|
1590
|
-
breadcrumb.innerHTML = html;
|
|
1757
|
+
breadcrumb.replaceChildren(fragment);
|
|
1591
1758
|
}
|
|
1592
1759
|
|
|
1593
|
-
function renderFiles(files) {
|
|
1760
|
+
function renderFiles(files, resetScroll = true) {
|
|
1594
1761
|
const fileGrid = document.getElementById('fileGrid');
|
|
1595
1762
|
const toggleBirthtimeBtn = document.getElementById('toggleBirthtimeBtn');
|
|
1596
1763
|
|
|
1597
|
-
// 确保视图类名正确
|
|
1598
1764
|
if (currentView === 'list') {
|
|
1599
1765
|
fileGrid.classList.add('list-view');
|
|
1766
|
+
fileGrid.classList.toggle('show-birthtime', showBirthtime);
|
|
1600
1767
|
toggleBirthtimeBtn.style.display = 'inline-block';
|
|
1601
1768
|
} else {
|
|
1602
1769
|
toggleBirthtimeBtn.style.display = 'none';
|
|
1603
1770
|
fileGrid.classList.remove('list-view');
|
|
1771
|
+
fileGrid.classList.remove('show-birthtime');
|
|
1604
1772
|
}
|
|
1605
1773
|
|
|
1606
1774
|
if (!files || files.length === 0) {
|
|
1775
|
+
virtualState = null;
|
|
1607
1776
|
fileGrid.innerHTML = '<div class="empty-state"><p>该目录为空</p></div>';
|
|
1608
1777
|
return;
|
|
1609
1778
|
}
|
|
1610
1779
|
|
|
1611
|
-
const
|
|
1780
|
+
const isListView = currentView === 'list';
|
|
1781
|
+
const columnCount = isListView
|
|
1782
|
+
? 1
|
|
1783
|
+
: Math.max(
|
|
1784
|
+
1,
|
|
1785
|
+
Math.floor((Math.max(fileGrid.clientWidth, GRID_MIN_ITEM_WIDTH + 16) - 8) / (GRID_MIN_ITEM_WIDTH + 8))
|
|
1786
|
+
);
|
|
1787
|
+
const rowHeight = isListView ? getListRowHeight(fileGrid) : GRID_ROW_HEIGHT;
|
|
1788
|
+
const totalRows = Math.ceil(files.length / columnCount);
|
|
1789
|
+
const gridItemWidth = isListView ? null : (fileGrid.clientWidth - 16 - (columnCount - 1) * 8) / columnCount;
|
|
1790
|
+
const spacer = document.createElement('div');
|
|
1791
|
+
spacer.className = 'virtual-spacer';
|
|
1792
|
+
spacer.style.height = `${totalRows * rowHeight + (isListView ? 0 : 8)}px`;
|
|
1793
|
+
const content = document.createElement('div');
|
|
1794
|
+
content.className = `virtual-content ${isListView ? 'list-view' : 'grid-view'}`;
|
|
1795
|
+
|
|
1796
|
+
virtualState = {
|
|
1797
|
+
files,
|
|
1798
|
+
columnCount,
|
|
1799
|
+
rowHeight,
|
|
1800
|
+
gridItemWidth,
|
|
1801
|
+
isListView,
|
|
1802
|
+
spacer,
|
|
1803
|
+
content,
|
|
1804
|
+
itemPool: [],
|
|
1805
|
+
freeItems: [],
|
|
1806
|
+
renderedItems: new Map(),
|
|
1807
|
+
startRow: -1,
|
|
1808
|
+
endRow: -1
|
|
1809
|
+
};
|
|
1810
|
+
fileGrid.replaceChildren(spacer, content);
|
|
1811
|
+
if (resetScroll) fileGrid.scrollTop = 0;
|
|
1812
|
+
renderVirtualWindow();
|
|
1813
|
+
}
|
|
1612
1814
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const selectedAttr = selectedPaths.has(file.path) ? 'checked' : '';
|
|
1621
|
-
const selectLabel = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
1622
|
-
|
|
1623
|
-
const fileItem = document.createElement('div');
|
|
1624
|
-
fileItem.className = `file-item ${hiddenClass} ${selectedClass}`;
|
|
1625
|
-
fileItem.dataset.path = file.path;
|
|
1626
|
-
fileItem.dataset.isDirectory = file.isDirectory;
|
|
1627
|
-
|
|
1628
|
-
if (currentView === 'list') {
|
|
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
|
-
/>
|
|
1639
|
-
<div class="file-icon">${icon}</div>
|
|
1640
|
-
<div class="file-info">
|
|
1641
|
-
<div class="file-name">${escapeHtml(file.name)}</div>
|
|
1642
|
-
<div class="file-meta">
|
|
1643
|
-
${file.isDirectory ? '<span>目录</span>' : `<span>${size}</span>`}
|
|
1644
|
-
<span>${mtime}</span>
|
|
1645
|
-
<span class="birthtime" style="display: ${showBirthtime ? 'inline-block' : 'none'};">${birthtime}</span>
|
|
1646
|
-
</div>
|
|
1647
|
-
</div>
|
|
1648
|
-
<div class="action-buttons">
|
|
1649
|
-
<button class="copy-btn" data-action="copyPath" data-path="${file.path}" title="复制路径">📋</button>
|
|
1650
|
-
<button class="action-btn open" data-action="openInExplorer" data-path="${
|
|
1651
|
-
file.path
|
|
1652
|
-
}" title="在文件管理器中打开">📂</button>
|
|
1653
|
-
${
|
|
1654
|
-
!file.isDirectory
|
|
1655
|
-
? `<button class="action-btn download" data-action="download" data-path="${
|
|
1656
|
-
file.path
|
|
1657
|
-
}" data-filename="${escapeHtml(file.name)}" title="下载">⬇️</button>`
|
|
1658
|
-
: ''
|
|
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>
|
|
1666
|
-
</div>
|
|
1667
|
-
`;
|
|
1668
|
-
} else {
|
|
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
|
-
/>
|
|
1679
|
-
<div class="file-icon">${icon}</div>
|
|
1680
|
-
<div class="file-info">
|
|
1681
|
-
<div class="file-name">${escapeHtml(file.name)}</div>
|
|
1682
|
-
<div class="file-meta">
|
|
1683
|
-
${file.isDirectory ? '目录' : size}
|
|
1684
|
-
</div>
|
|
1685
|
-
</div>
|
|
1686
|
-
`;
|
|
1687
|
-
}
|
|
1815
|
+
function scheduleVirtualRender() {
|
|
1816
|
+
if (virtualRenderFrame || !virtualState) return;
|
|
1817
|
+
virtualRenderFrame = requestAnimationFrame(() => {
|
|
1818
|
+
virtualRenderFrame = null;
|
|
1819
|
+
renderVirtualWindow();
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1688
1822
|
|
|
1689
|
-
|
|
1823
|
+
function getListRowHeight(fileGrid) {
|
|
1824
|
+
const rowHeight = Number.parseFloat(getComputedStyle(fileGrid).getPropertyValue('--list-row-height'));
|
|
1825
|
+
return Number.isFinite(rowHeight) ? rowHeight : LIST_ROW_HEIGHT;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function renderVirtualWindow() {
|
|
1829
|
+
if (!virtualState) return;
|
|
1830
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
1831
|
+
const {
|
|
1832
|
+
files,
|
|
1833
|
+
columnCount,
|
|
1834
|
+
rowHeight,
|
|
1835
|
+
gridItemWidth,
|
|
1836
|
+
isListView,
|
|
1837
|
+
content,
|
|
1838
|
+
itemPool,
|
|
1839
|
+
freeItems,
|
|
1840
|
+
renderedItems
|
|
1841
|
+
} = virtualState;
|
|
1842
|
+
const firstVisibleRow = Math.floor(fileGrid.scrollTop / rowHeight);
|
|
1843
|
+
const visibleRows = Math.ceil(fileGrid.clientHeight / rowHeight);
|
|
1844
|
+
const startRow = Math.max(0, firstVisibleRow - VIRTUAL_BUFFER_ROWS);
|
|
1845
|
+
const endRow = Math.min(
|
|
1846
|
+
Math.ceil(files.length / columnCount),
|
|
1847
|
+
firstVisibleRow + visibleRows + VIRTUAL_BUFFER_ROWS
|
|
1848
|
+
);
|
|
1849
|
+
const startIndex = startRow * columnCount;
|
|
1850
|
+
const endIndex = Math.min(files.length, endRow * columnCount);
|
|
1851
|
+
|
|
1852
|
+
if (virtualState.startRow === startRow && virtualState.endRow === endRow) return;
|
|
1853
|
+
virtualState.startRow = startRow;
|
|
1854
|
+
virtualState.endRow = endRow;
|
|
1855
|
+
renderedItems.forEach((fileItem, fileIndex) => {
|
|
1856
|
+
if (fileIndex < startIndex || fileIndex >= endIndex) {
|
|
1857
|
+
renderedItems.delete(fileIndex);
|
|
1858
|
+
fileItem.hidden = true;
|
|
1859
|
+
freeItems.push(fileItem);
|
|
1860
|
+
}
|
|
1690
1861
|
});
|
|
1691
1862
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1863
|
+
for (let fileIndex = startIndex; fileIndex < endIndex; fileIndex++) {
|
|
1864
|
+
if (renderedItems.has(fileIndex)) continue;
|
|
1865
|
+
|
|
1866
|
+
let fileItem = freeItems.pop();
|
|
1867
|
+
if (!fileItem) {
|
|
1868
|
+
fileItem = createFileItemElement(isListView);
|
|
1869
|
+
itemPool.push(fileItem);
|
|
1870
|
+
content.appendChild(fileItem);
|
|
1871
|
+
}
|
|
1872
|
+
updateFileItem(fileItem, files[fileIndex]);
|
|
1873
|
+
positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView);
|
|
1874
|
+
fileItem.hidden = false;
|
|
1875
|
+
renderedItems.set(fileIndex, fileItem);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView) {
|
|
1880
|
+
fileItem.style.transform = '';
|
|
1881
|
+
if (isListView) {
|
|
1882
|
+
fileItem.style.left = '0';
|
|
1883
|
+
fileItem.style.right = '0';
|
|
1884
|
+
fileItem.style.top = `${fileIndex * rowHeight}px`;
|
|
1885
|
+
fileItem.style.width = '';
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const row = Math.floor(fileIndex / columnCount);
|
|
1890
|
+
const column = fileIndex % columnCount;
|
|
1891
|
+
fileItem.style.left = `${8 + column * (gridItemWidth + 8)}px`;
|
|
1892
|
+
fileItem.style.right = 'auto';
|
|
1893
|
+
fileItem.style.top = `${8 + row * rowHeight}px`;
|
|
1894
|
+
fileItem.style.width = `${gridItemWidth}px`;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function createFileItemElement(isListView) {
|
|
1898
|
+
const fileItem = document.createElement('div');
|
|
1899
|
+
fileItem.className = 'file-item';
|
|
1900
|
+
const select = document.createElement('input');
|
|
1901
|
+
select.className = 'file-select edit-only edit-inline-block';
|
|
1902
|
+
select.type = 'checkbox';
|
|
1903
|
+
select.dataset.action = 'toggleSelect';
|
|
1904
|
+
fileItem.appendChild(select);
|
|
1905
|
+
|
|
1906
|
+
const icon = createFileIconElement();
|
|
1907
|
+
fileItem.appendChild(icon.wrapper);
|
|
1908
|
+
|
|
1909
|
+
const info = document.createElement('div');
|
|
1910
|
+
info.className = 'file-info';
|
|
1911
|
+
const name = document.createElement('div');
|
|
1912
|
+
name.className = 'file-name';
|
|
1913
|
+
info.appendChild(name);
|
|
1914
|
+
const meta = document.createElement('div');
|
|
1915
|
+
meta.className = 'file-meta';
|
|
1916
|
+
const type = document.createElement('span');
|
|
1917
|
+
meta.appendChild(type);
|
|
1918
|
+
info.appendChild(meta);
|
|
1919
|
+
fileItem.appendChild(info);
|
|
1920
|
+
|
|
1921
|
+
const refs = { select, icon, name, type };
|
|
1922
|
+
|
|
1923
|
+
if (isListView) {
|
|
1924
|
+
const mtime = createDateMeta();
|
|
1925
|
+
const birthtime = createDateMeta('birthtime');
|
|
1926
|
+
meta.append(mtime.container, birthtime.container);
|
|
1927
|
+
const more = createActionButton('more-actions-btn', 'more', '更多操作', '⋮');
|
|
1928
|
+
more.setAttribute('aria-label', '更多操作');
|
|
1929
|
+
fileItem.appendChild(more);
|
|
1930
|
+
Object.assign(refs, { mtime, birthtime });
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
fileItem._refs = refs;
|
|
1934
|
+
return fileItem;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function createFileIconElement() {
|
|
1938
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
1939
|
+
const wrapper = document.createElement('div');
|
|
1940
|
+
wrapper.className = 'file-icon';
|
|
1941
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
1942
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1943
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
1944
|
+
const use = document.createElementNS(namespace, 'use');
|
|
1945
|
+
svg.appendChild(use);
|
|
1946
|
+
wrapper.appendChild(svg);
|
|
1947
|
+
return { wrapper, svg, use };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function createDateMeta(className = '') {
|
|
1951
|
+
const container = document.createElement('span');
|
|
1952
|
+
if (className) container.className = className;
|
|
1953
|
+
const full = document.createElement('span');
|
|
1954
|
+
full.className = 'date-full';
|
|
1955
|
+
const compact = document.createElement('span');
|
|
1956
|
+
compact.className = 'date-compact';
|
|
1957
|
+
container.append(full, compact);
|
|
1958
|
+
return { container, full, compact };
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function createActionButton(className, action, title, label) {
|
|
1962
|
+
const button = document.createElement('button');
|
|
1963
|
+
button.className = className;
|
|
1964
|
+
button.dataset.action = action;
|
|
1965
|
+
button.title = title;
|
|
1966
|
+
button.textContent = label;
|
|
1967
|
+
return button;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function updateFileItem(fileItem, file) {
|
|
1971
|
+
const refs = fileItem._refs;
|
|
1972
|
+
const view = getFileView(file);
|
|
1973
|
+
const isSelected = selectedPaths.has(file.path);
|
|
1974
|
+
fileItem.className = `file-item${file.isHidden ? ' hidden-file' : ''}${isSelected ? ' selected' : ''}`;
|
|
1975
|
+
fileItem.dataset.path = file.path;
|
|
1976
|
+
fileItem.dataset.isDirectory = file.isDirectory;
|
|
1977
|
+
refs.select.dataset.path = file.path;
|
|
1978
|
+
refs.select.checked = isSelected;
|
|
1979
|
+
refs.select.title = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
1980
|
+
updateFileIcon(refs.icon, view.iconInfo);
|
|
1981
|
+
refs.name.textContent = file.name;
|
|
1982
|
+
refs.type.textContent = view.typeLabel;
|
|
1983
|
+
|
|
1984
|
+
if (!refs.mtime) return;
|
|
1985
|
+
|
|
1986
|
+
refs.mtime.full.textContent = view.mtime;
|
|
1987
|
+
refs.mtime.compact.textContent = view.compactMtime;
|
|
1988
|
+
refs.birthtime.full.textContent = view.birthtime;
|
|
1989
|
+
refs.birthtime.compact.textContent = view.compactBirthtime;
|
|
1990
|
+
refs.birthtime.container.classList.toggle('visible', showBirthtime);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function getFileView(file) {
|
|
1994
|
+
if (!file._view) {
|
|
1995
|
+
file._view = {
|
|
1996
|
+
typeLabel: file.isDirectory ? '目录' : formatSize(file.size),
|
|
1997
|
+
mtime: formatDate(file.mtime),
|
|
1998
|
+
compactMtime: formatDate(file.mtime, false),
|
|
1999
|
+
birthtime: formatDate(file.birthtime),
|
|
2000
|
+
compactBirthtime: `创建 ${formatDate(file.birthtime, false)}`,
|
|
2001
|
+
iconInfo: getFileIconInfo(file)
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
return file._view;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function updateFileIcon(icon, iconInfo) {
|
|
2008
|
+
icon.use.setAttribute('href', iconInfo.symbol);
|
|
2009
|
+
icon.svg.style.color = iconInfo.color;
|
|
1695
2010
|
}
|
|
1696
2011
|
|
|
1697
2012
|
function handleItemClick(path, isDirectory) {
|
|
@@ -1942,7 +2257,7 @@
|
|
|
1942
2257
|
}
|
|
1943
2258
|
|
|
1944
2259
|
function renderBinaryPreview(blob, filename) {
|
|
1945
|
-
|
|
2260
|
+
currentPreviewObjectUrl = URL.createObjectURL(blob);
|
|
1946
2261
|
return `
|
|
1947
2262
|
<div class="preview-container">
|
|
1948
2263
|
<div class="binary-info">
|
|
@@ -1952,7 +2267,7 @@
|
|
|
1952
2267
|
</div>
|
|
1953
2268
|
<div class="empty-state">
|
|
1954
2269
|
<p>该文件类型暂不支持预览</p>
|
|
1955
|
-
<p><a href="${
|
|
2270
|
+
<p><a href="${currentPreviewObjectUrl}" download="${escapeAttribute(filename)}">点击下载</a></p>
|
|
1956
2271
|
</div>
|
|
1957
2272
|
</div>
|
|
1958
2273
|
`;
|
|
@@ -1964,6 +2279,7 @@
|
|
|
1964
2279
|
const body = document.getElementById('previewBody');
|
|
1965
2280
|
|
|
1966
2281
|
title.textContent = path.split('/').pop();
|
|
2282
|
+
releasePreviewObjectUrl();
|
|
1967
2283
|
body.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
|
|
1968
2284
|
modal.classList.add('active');
|
|
1969
2285
|
|
|
@@ -2129,6 +2445,14 @@
|
|
|
2129
2445
|
|
|
2130
2446
|
function closeModal() {
|
|
2131
2447
|
document.getElementById('previewModal').classList.remove('active');
|
|
2448
|
+
releasePreviewObjectUrl();
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function releasePreviewObjectUrl() {
|
|
2452
|
+
if (currentPreviewObjectUrl) {
|
|
2453
|
+
URL.revokeObjectURL(currentPreviewObjectUrl);
|
|
2454
|
+
currentPreviewObjectUrl = null;
|
|
2455
|
+
}
|
|
2132
2456
|
}
|
|
2133
2457
|
|
|
2134
2458
|
function goBack() {
|
|
@@ -2161,14 +2485,16 @@
|
|
|
2161
2485
|
renderFiles(currentFiles);
|
|
2162
2486
|
}
|
|
2163
2487
|
|
|
2164
|
-
|
|
2488
|
+
const fileIconInfoCache = new Map();
|
|
2489
|
+
|
|
2490
|
+
function getFileIconInfo(file) {
|
|
2165
2491
|
if (file.isDirectory) {
|
|
2166
|
-
return
|
|
2167
|
-
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
2168
|
-
</svg>`;
|
|
2492
|
+
return { symbol: '#icon-folder', color: '#667eea' };
|
|
2169
2493
|
}
|
|
2170
2494
|
|
|
2171
2495
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
2496
|
+
const cached = fileIconInfoCache.get(ext);
|
|
2497
|
+
if (cached) return cached;
|
|
2172
2498
|
const iconColors = {
|
|
2173
2499
|
js: '#f7df1e',
|
|
2174
2500
|
json: '#292929',
|
|
@@ -2246,12 +2572,9 @@
|
|
|
2246
2572
|
webm: '#2196f3'
|
|
2247
2573
|
};
|
|
2248
2574
|
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
return
|
|
2252
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
2253
|
-
<polyline points="14 2 14 8 20 8"></polyline>
|
|
2254
|
-
</svg>`;
|
|
2575
|
+
const iconInfo = { symbol: '#icon-file', color: iconColors[ext] || '#95a5a6' };
|
|
2576
|
+
fileIconInfoCache.set(ext, iconInfo);
|
|
2577
|
+
return iconInfo;
|
|
2255
2578
|
}
|
|
2256
2579
|
|
|
2257
2580
|
function formatSize(bytes) {
|
|
@@ -2262,9 +2585,15 @@
|
|
|
2262
2585
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
2263
2586
|
}
|
|
2264
2587
|
|
|
2265
|
-
function formatDate(
|
|
2266
|
-
const date = new Date(
|
|
2267
|
-
|
|
2588
|
+
function formatDate(value, includeSeconds = true) {
|
|
2589
|
+
const date = new Date(value);
|
|
2590
|
+
const year = date.getFullYear();
|
|
2591
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2592
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
2593
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
2594
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
2595
|
+
const base = `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
2596
|
+
return includeSeconds ? `${base}:${String(date.getSeconds()).padStart(2, '0')}` : base;
|
|
2268
2597
|
}
|
|
2269
2598
|
|
|
2270
2599
|
function escapeHtml(text) {
|
|
@@ -2272,6 +2601,20 @@
|
|
|
2272
2601
|
div.textContent = text;
|
|
2273
2602
|
return div.innerHTML;
|
|
2274
2603
|
}
|
|
2604
|
+
|
|
2605
|
+
function escapeAttribute(value) {
|
|
2606
|
+
return String(value).replace(
|
|
2607
|
+
/[&<>"']/g,
|
|
2608
|
+
char =>
|
|
2609
|
+
({
|
|
2610
|
+
'&': '&',
|
|
2611
|
+
'<': '<',
|
|
2612
|
+
'>': '>',
|
|
2613
|
+
'"': '"',
|
|
2614
|
+
"'": '''
|
|
2615
|
+
})[char]
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2275
2618
|
</script>
|
|
2276
2619
|
</body>
|
|
2277
2620
|
</html>
|