mock-service-cli-ultra 4.4.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.
- package/LICENSE +21 -0
- package/README.md +307 -0
- package/bin/mock-service-cli +3 -0
- package/dist/api-overview.html +685 -0
- package/dist/asyncTaskQueue.js +1 -0
- package/dist/cli.js +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 +60 -0
- package/dist/file-explorer.html +3003 -0
- package/dist/fileExplorerServer.js +1 -0
- package/dist/manageMockFiles.js +1 -0
- package/dist/meta.json +10001 -0
- package/dist/mockServer.js +1 -0
- package/dist/packageInfo.js +1 -0
- package/dist/runtime.js +156 -0
- package/dist/staticServer.js +1 -0
- package/dist/utils.js +1 -0
- package/docs/static-server.config.example.json +28 -0
- package/package.json +58 -0
|
@@ -0,0 +1,3003 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>文件浏览器 - File Explorer</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon-file-explorer.svg" />
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.icon-sprite {
|
|
16
|
+
position: absolute;
|
|
17
|
+
width: 0;
|
|
18
|
+
height: 0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
24
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
padding: 12px;
|
|
27
|
+
margin: 0;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.container {
|
|
32
|
+
max-width: 1200px;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
height: calc(100vh - 24px);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.header {
|
|
40
|
+
background: white;
|
|
41
|
+
padding: 12px 16px;
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
44
|
+
margin-bottom: 12px;
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.header h1 {
|
|
49
|
+
color: #333;
|
|
50
|
+
font-size: 20px;
|
|
51
|
+
margin-bottom: 4px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.breadcrumb {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 6px;
|
|
58
|
+
flex-wrap: wrap;
|
|
59
|
+
margin-top: 8px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.breadcrumb-item {
|
|
63
|
+
padding: 4px 8px;
|
|
64
|
+
background: #f0f0f0;
|
|
65
|
+
border-radius: 4px;
|
|
66
|
+
font-size: 12px;
|
|
67
|
+
color: #666;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
transition: background 0.2s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.breadcrumb-item:hover {
|
|
73
|
+
background: #e0e0e0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.breadcrumb-item.current {
|
|
77
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
78
|
+
color: white;
|
|
79
|
+
cursor: default;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.breadcrumb-separator {
|
|
83
|
+
color: #999;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.file-container {
|
|
87
|
+
background: white;
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
flex: 1;
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
min-height: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.toolbar {
|
|
98
|
+
padding: 10px 12px;
|
|
99
|
+
border-bottom: 1px solid #eee;
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
align-items: center;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.stats-info {
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
color: #666;
|
|
110
|
+
padding: 4px 8px;
|
|
111
|
+
background: #f8f8f8;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.search-box {
|
|
116
|
+
flex: 1;
|
|
117
|
+
min-width: 150px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.search-box input {
|
|
121
|
+
width: 100%;
|
|
122
|
+
padding: 6px 12px;
|
|
123
|
+
border: 1px solid #ddd;
|
|
124
|
+
border-radius: 4px;
|
|
125
|
+
font-size: 12px;
|
|
126
|
+
outline: none;
|
|
127
|
+
transition: border-color 0.2s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.search-box input:focus {
|
|
131
|
+
border-color: #667eea;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.sort-controls {
|
|
135
|
+
display: flex;
|
|
136
|
+
gap: 6px;
|
|
137
|
+
align-items: center;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.sort-select {
|
|
141
|
+
padding: 6px 10px;
|
|
142
|
+
border: 1px solid #ddd;
|
|
143
|
+
border-radius: 4px;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
outline: none;
|
|
147
|
+
transition: border-color 0.2s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.sort-select:focus {
|
|
151
|
+
border-color: #667eea;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.sort-order-btn {
|
|
155
|
+
padding: 6px 10px;
|
|
156
|
+
background: #f0f0f0;
|
|
157
|
+
border: none;
|
|
158
|
+
border-radius: 4px;
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
transition: background 0.2s;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.sort-order-btn:hover {
|
|
165
|
+
background: #e0e0e0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.toolbar-action-btn {
|
|
169
|
+
padding: 6px 10px;
|
|
170
|
+
background: #f0f0f0;
|
|
171
|
+
border: none;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
font-size: 12px;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
transition: all 0.2s;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.toolbar-action-btn:hover:not(:disabled) {
|
|
179
|
+
background: #e0e0e0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.toolbar-action-btn.danger:hover:not(:disabled) {
|
|
183
|
+
background: #dc3545;
|
|
184
|
+
color: white;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.toolbar-action-btn:disabled {
|
|
188
|
+
opacity: 0.45;
|
|
189
|
+
cursor: not-allowed;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
body:not(.editing) .edit-only {
|
|
193
|
+
display: none !important;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
body.editing .edit-inline {
|
|
197
|
+
display: inline-flex !important;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
body.editing .edit-inline-block {
|
|
201
|
+
display: inline-block !important;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
body.editing .edit-flex {
|
|
205
|
+
display: flex !important;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
body.editing .edit-block {
|
|
209
|
+
display: block !important;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.back-btn {
|
|
213
|
+
padding: 6px 12px;
|
|
214
|
+
background: #f0f0f0;
|
|
215
|
+
border: none;
|
|
216
|
+
border-radius: 4px;
|
|
217
|
+
font-size: 12px;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 4px;
|
|
222
|
+
transition: background 0.2s;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.back-btn:hover:not(:disabled) {
|
|
226
|
+
background: #e0e0e0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.back-btn:disabled {
|
|
230
|
+
opacity: 0.5;
|
|
231
|
+
cursor: not-allowed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.view-toggle {
|
|
235
|
+
display: flex;
|
|
236
|
+
background: #f0f0f0;
|
|
237
|
+
border-radius: 4px;
|
|
238
|
+
padding: 3px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.view-btn {
|
|
242
|
+
padding: 4px 10px;
|
|
243
|
+
background: transparent;
|
|
244
|
+
border: none;
|
|
245
|
+
border-radius: 3px;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
font-size: 12px;
|
|
248
|
+
transition: all 0.2s;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.view-btn.active {
|
|
252
|
+
background: white;
|
|
253
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.refresh-btn {
|
|
257
|
+
padding: 6px 10px;
|
|
258
|
+
background: #f0f0f0;
|
|
259
|
+
border: none;
|
|
260
|
+
border-radius: 4px;
|
|
261
|
+
font-size: 14px;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
transition: background 0.2s;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.refresh-btn:hover {
|
|
267
|
+
background: #e0e0e0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.toggle-hidden-btn {
|
|
271
|
+
padding: 6px 10px;
|
|
272
|
+
background: #f0f0f0;
|
|
273
|
+
border: none;
|
|
274
|
+
border-radius: 4px;
|
|
275
|
+
font-size: 14px;
|
|
276
|
+
cursor: pointer;
|
|
277
|
+
transition: background 0.2s;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.toggle-hidden-btn:hover {
|
|
281
|
+
background: #e0e0e0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.toggle-hidden-btn.active {
|
|
285
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
286
|
+
color: white;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.context-menu {
|
|
290
|
+
position: fixed;
|
|
291
|
+
background: white;
|
|
292
|
+
border-radius: 8px;
|
|
293
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
294
|
+
padding: 8px 0;
|
|
295
|
+
z-index: 10000;
|
|
296
|
+
min-width: 160px;
|
|
297
|
+
display: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.context-menu.active {
|
|
301
|
+
display: block;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.context-menu-item {
|
|
305
|
+
padding: 10px 16px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
font-size: 14px;
|
|
308
|
+
color: #333;
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 8px;
|
|
312
|
+
transition: background 0.2s;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.context-menu-item:hover {
|
|
316
|
+
background: #f0f0f0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.context-menu-item.disabled {
|
|
320
|
+
opacity: 0.5;
|
|
321
|
+
cursor: not-allowed;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.context-menu-divider {
|
|
325
|
+
height: 1px;
|
|
326
|
+
background: #eee;
|
|
327
|
+
margin: 4px 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.copy-btn {
|
|
331
|
+
padding: 4px 8px;
|
|
332
|
+
background: #f0f0f0;
|
|
333
|
+
border: none;
|
|
334
|
+
border-radius: 4px;
|
|
335
|
+
font-size: 12px;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
transition: all 0.2s;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.copy-btn:hover {
|
|
341
|
+
background: #ffc107;
|
|
342
|
+
color: #333;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.file-grid {
|
|
346
|
+
--list-row-height: 37px;
|
|
347
|
+
position: relative;
|
|
348
|
+
overflow: auto;
|
|
349
|
+
flex: 1;
|
|
350
|
+
min-height: 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.virtual-spacer {
|
|
354
|
+
width: 1px;
|
|
355
|
+
pointer-events: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.virtual-content {
|
|
359
|
+
position: absolute;
|
|
360
|
+
top: 0;
|
|
361
|
+
left: 0;
|
|
362
|
+
right: 0;
|
|
363
|
+
overflow-anchor: none;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.virtual-content.grid-view {
|
|
367
|
+
display: block;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.virtual-content.list-view {
|
|
371
|
+
display: block;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.virtual-content .file-item {
|
|
375
|
+
position: absolute;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.file-item {
|
|
379
|
+
display: flex;
|
|
380
|
+
flex-direction: column;
|
|
381
|
+
align-items: center;
|
|
382
|
+
padding: 10px 8px;
|
|
383
|
+
border-radius: 6px;
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
transition: background-color 0.2s;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.grid-view .file-item {
|
|
389
|
+
height: 124px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.file-item:hover {
|
|
393
|
+
background: #f8f9fa;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.file-item.selected {
|
|
397
|
+
background: #eef2ff;
|
|
398
|
+
outline: 1px solid #667eea;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.list-view .file-item {
|
|
402
|
+
flex-direction: row;
|
|
403
|
+
padding: 6px 12px;
|
|
404
|
+
border-bottom: 1px solid #f0f0f0;
|
|
405
|
+
border-radius: 0;
|
|
406
|
+
min-height: auto;
|
|
407
|
+
height: var(--list-row-height);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.file-icon {
|
|
411
|
+
width: 40px;
|
|
412
|
+
height: 40px;
|
|
413
|
+
margin-bottom: 6px;
|
|
414
|
+
display: flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
justify-content: center;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.list-view .file-icon {
|
|
420
|
+
width: 20px;
|
|
421
|
+
height: 20px;
|
|
422
|
+
margin-bottom: 0;
|
|
423
|
+
margin-right: 8px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.file-icon svg {
|
|
427
|
+
width: 100%;
|
|
428
|
+
height: 100%;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.file-select {
|
|
432
|
+
align-self: flex-start;
|
|
433
|
+
margin-bottom: 6px;
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.list-view .file-select {
|
|
438
|
+
align-self: center;
|
|
439
|
+
margin: 0 8px 0 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.file-info {
|
|
443
|
+
text-align: center;
|
|
444
|
+
word-break: break-all;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.list-view .file-info {
|
|
448
|
+
display: flex;
|
|
449
|
+
align-items: center;
|
|
450
|
+
gap: 10px;
|
|
451
|
+
text-align: left;
|
|
452
|
+
flex: 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.file-name {
|
|
456
|
+
font-size: 12px;
|
|
457
|
+
color: #333;
|
|
458
|
+
font-weight: 500;
|
|
459
|
+
margin-bottom: 2px;
|
|
460
|
+
line-height: 1.3;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.grid-view .file-name {
|
|
464
|
+
display: -webkit-box;
|
|
465
|
+
-webkit-box-orient: vertical;
|
|
466
|
+
-webkit-line-clamp: 2;
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.list-view .file-name {
|
|
471
|
+
margin-bottom: 0;
|
|
472
|
+
flex: 1;
|
|
473
|
+
font-size: 12px;
|
|
474
|
+
min-width: 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.file-meta {
|
|
478
|
+
font-size: 10px;
|
|
479
|
+
color: #999;
|
|
480
|
+
}
|
|
481
|
+
.birthtime {
|
|
482
|
+
padding-right: 4px;
|
|
483
|
+
display: none;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.birthtime.visible {
|
|
487
|
+
display: inline-block;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.list-view .file-meta {
|
|
491
|
+
display: flex;
|
|
492
|
+
gap: 10px;
|
|
493
|
+
font-size: 10px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.date-compact,
|
|
497
|
+
.more-actions-btn {
|
|
498
|
+
display: none;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.hidden-file {
|
|
502
|
+
opacity: 0.6;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.action-buttons {
|
|
506
|
+
display: none;
|
|
507
|
+
gap: 6px;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.list-view .action-buttons {
|
|
511
|
+
display: none;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.list-view .more-actions-btn {
|
|
515
|
+
display: inline-flex;
|
|
516
|
+
flex: 0 0 32px;
|
|
517
|
+
align-items: center;
|
|
518
|
+
justify-content: center;
|
|
519
|
+
align-self: center;
|
|
520
|
+
width: 32px;
|
|
521
|
+
height: 28px;
|
|
522
|
+
margin-left: 8px;
|
|
523
|
+
padding: 0;
|
|
524
|
+
border: 1px solid transparent;
|
|
525
|
+
border-radius: 5px;
|
|
526
|
+
background: #f1f3f5;
|
|
527
|
+
color: #555;
|
|
528
|
+
font-size: 20px;
|
|
529
|
+
line-height: 1;
|
|
530
|
+
cursor: pointer;
|
|
531
|
+
touch-action: manipulation;
|
|
532
|
+
-webkit-tap-highlight-color: transparent;
|
|
533
|
+
transition:
|
|
534
|
+
background-color 120ms ease,
|
|
535
|
+
border-color 120ms ease,
|
|
536
|
+
color 120ms ease,
|
|
537
|
+
transform 120ms ease;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.list-view .more-actions-btn:hover {
|
|
541
|
+
border-color: #d9dce1;
|
|
542
|
+
background: #e5e7eb;
|
|
543
|
+
color: #222;
|
|
544
|
+
transform: scale(1.06);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.list-view .more-actions-btn:active {
|
|
548
|
+
background: #d9dce1;
|
|
549
|
+
transform: scale(0.92);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.list-view .more-actions-btn:focus-visible {
|
|
553
|
+
outline: 2px solid #667eea;
|
|
554
|
+
outline-offset: 1px;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.action-btn {
|
|
558
|
+
padding: 4px 8px;
|
|
559
|
+
background: #f0f0f0;
|
|
560
|
+
border: none;
|
|
561
|
+
border-radius: 4px;
|
|
562
|
+
font-size: 12px;
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
transition: all 0.2s;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.action-btn:hover {
|
|
568
|
+
background: #e0e0e0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.action-btn.open:hover {
|
|
572
|
+
background: #667eea;
|
|
573
|
+
color: white;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.action-btn.download:hover {
|
|
577
|
+
background: #28a745;
|
|
578
|
+
color: white;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.action-btn.rename:hover {
|
|
582
|
+
background: #ffc107;
|
|
583
|
+
color: #333;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.action-btn.delete:hover {
|
|
587
|
+
background: #dc3545;
|
|
588
|
+
color: white;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
@media (max-width: 767px) {
|
|
592
|
+
.file-grid.list-view {
|
|
593
|
+
--list-row-height: 48px;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.file-grid.list-view.show-birthtime {
|
|
597
|
+
--list-row-height: 65px;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.list-view .file-item {
|
|
601
|
+
align-items: flex-start;
|
|
602
|
+
padding: 7px 10px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.list-view .file-icon {
|
|
606
|
+
flex: 0 0 20px;
|
|
607
|
+
margin-top: 2px;
|
|
608
|
+
margin-right: 7px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.list-view .file-select {
|
|
612
|
+
flex: 0 0 auto;
|
|
613
|
+
margin: 3px 7px 0 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.list-view .file-info {
|
|
617
|
+
display: flex;
|
|
618
|
+
flex: 1;
|
|
619
|
+
flex-direction: column;
|
|
620
|
+
align-items: stretch;
|
|
621
|
+
gap: 3px;
|
|
622
|
+
min-width: 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.list-view .file-name {
|
|
626
|
+
width: 100%;
|
|
627
|
+
overflow: hidden;
|
|
628
|
+
text-overflow: ellipsis;
|
|
629
|
+
white-space: nowrap;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.list-view .file-meta {
|
|
633
|
+
display: flex;
|
|
634
|
+
width: 100%;
|
|
635
|
+
gap: 3px;
|
|
636
|
+
flex-wrap: wrap;
|
|
637
|
+
overflow: hidden;
|
|
638
|
+
white-space: nowrap;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.list-view .file-meta > span {
|
|
642
|
+
overflow: hidden;
|
|
643
|
+
text-overflow: ellipsis;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.date-full {
|
|
647
|
+
display: none;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.date-compact {
|
|
651
|
+
display: inline;
|
|
652
|
+
vertical-align: middle;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.list-view .birthtime.visible {
|
|
656
|
+
flex-basis: 100%;
|
|
657
|
+
display: block;
|
|
658
|
+
padding-right: 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.list-view .action-buttons {
|
|
662
|
+
display: none !important;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.list-view .more-actions-btn {
|
|
666
|
+
align-self: flex-start;
|
|
667
|
+
margin: 10px 0 0 6px;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.file-grid.list-view.show-birthtime .more-actions-btn {
|
|
671
|
+
margin-top: 17px;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.modal {
|
|
676
|
+
display: none;
|
|
677
|
+
position: fixed;
|
|
678
|
+
top: 0;
|
|
679
|
+
left: 0;
|
|
680
|
+
right: 0;
|
|
681
|
+
bottom: 0;
|
|
682
|
+
background: rgba(0, 0, 0, 0.8);
|
|
683
|
+
z-index: 1000;
|
|
684
|
+
padding: 20px;
|
|
685
|
+
overflow: auto;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.modal.active {
|
|
689
|
+
display: flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
justify-content: center;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.modal-content {
|
|
695
|
+
background: white;
|
|
696
|
+
border-radius: 12px;
|
|
697
|
+
max-width: 95vw;
|
|
698
|
+
max-height: 95vh;
|
|
699
|
+
width: 100%;
|
|
700
|
+
height: 100%;
|
|
701
|
+
overflow: hidden;
|
|
702
|
+
display: flex;
|
|
703
|
+
flex-direction: column;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.modal-header {
|
|
707
|
+
display: flex;
|
|
708
|
+
justify-content: space-between;
|
|
709
|
+
align-items: center;
|
|
710
|
+
padding: 16px 20px;
|
|
711
|
+
border-bottom: 1px solid #eee;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.modal-title {
|
|
715
|
+
font-size: 18px;
|
|
716
|
+
font-weight: 600;
|
|
717
|
+
color: #333;
|
|
718
|
+
word-break: break-all;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.close-btn {
|
|
722
|
+
width: 32px;
|
|
723
|
+
height: 32px;
|
|
724
|
+
border: none;
|
|
725
|
+
background: #f0f0f0;
|
|
726
|
+
border-radius: 6px;
|
|
727
|
+
cursor: pointer;
|
|
728
|
+
font-size: 18px;
|
|
729
|
+
color: #666;
|
|
730
|
+
display: flex;
|
|
731
|
+
align-items: center;
|
|
732
|
+
justify-content: center;
|
|
733
|
+
transition: background 0.2s;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.close-btn:hover {
|
|
737
|
+
background: #e0e0e0;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.modal-body {
|
|
741
|
+
padding: 20px;
|
|
742
|
+
overflow: auto;
|
|
743
|
+
flex: 1;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.image-preview {
|
|
747
|
+
max-width: 100%;
|
|
748
|
+
max-height: 70vh;
|
|
749
|
+
display: block;
|
|
750
|
+
margin: 0 auto;
|
|
751
|
+
transition: transform 0.3s ease;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.image-preview-container {
|
|
755
|
+
position: relative;
|
|
756
|
+
display: flex;
|
|
757
|
+
flex-direction: column;
|
|
758
|
+
align-items: center;
|
|
759
|
+
justify-content: flex-start;
|
|
760
|
+
height: 100%;
|
|
761
|
+
overflow: auto;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.image-preview-controls {
|
|
765
|
+
display: flex;
|
|
766
|
+
gap: 12px;
|
|
767
|
+
margin-bottom: 16px;
|
|
768
|
+
align-items: center;
|
|
769
|
+
position: sticky;
|
|
770
|
+
top: 0;
|
|
771
|
+
background: white;
|
|
772
|
+
padding: 12px;
|
|
773
|
+
border-radius: 8px;
|
|
774
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
775
|
+
z-index: 10;
|
|
776
|
+
flex-wrap: wrap;
|
|
777
|
+
justify-content: center;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.nav-btn {
|
|
781
|
+
padding: 12px 20px;
|
|
782
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
783
|
+
color: white;
|
|
784
|
+
border: none;
|
|
785
|
+
border-radius: 8px;
|
|
786
|
+
font-size: 16px;
|
|
787
|
+
cursor: pointer;
|
|
788
|
+
transition: all 0.2s;
|
|
789
|
+
display: flex;
|
|
790
|
+
align-items: center;
|
|
791
|
+
gap: 8px;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.nav-btn:hover:not(:disabled) {
|
|
795
|
+
transform: translateY(-2px);
|
|
796
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.nav-btn:disabled {
|
|
800
|
+
opacity: 0.5;
|
|
801
|
+
cursor: not-allowed;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.nav-container {
|
|
805
|
+
display: flex;
|
|
806
|
+
justify-content: space-between;
|
|
807
|
+
align-items: center;
|
|
808
|
+
width: 100%;
|
|
809
|
+
margin-bottom: 16px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.zoom-btn {
|
|
813
|
+
padding: 8px 16px;
|
|
814
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
815
|
+
color: white;
|
|
816
|
+
border: none;
|
|
817
|
+
border-radius: 6px;
|
|
818
|
+
font-size: 14px;
|
|
819
|
+
cursor: pointer;
|
|
820
|
+
transition: all 0.2s;
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
gap: 6px;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.zoom-btn:hover {
|
|
827
|
+
transform: translateY(-1px);
|
|
828
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.zoom-level {
|
|
832
|
+
font-size: 14px;
|
|
833
|
+
color: #666;
|
|
834
|
+
font-weight: 500;
|
|
835
|
+
min-width: 60px;
|
|
836
|
+
text-align: center;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.rotate-btn {
|
|
840
|
+
padding: 8px 16px;
|
|
841
|
+
background: #f0f0f0;
|
|
842
|
+
color: #333;
|
|
843
|
+
border: none;
|
|
844
|
+
border-radius: 6px;
|
|
845
|
+
font-size: 14px;
|
|
846
|
+
cursor: pointer;
|
|
847
|
+
transition: all 0.2s;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.rotate-btn:hover {
|
|
851
|
+
background: #e0e0e0;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.binary-preview {
|
|
855
|
+
background: #1e1e1e;
|
|
856
|
+
color: #d4d4d4;
|
|
857
|
+
padding: 20px;
|
|
858
|
+
border-radius: 8px;
|
|
859
|
+
max-height: 70vh;
|
|
860
|
+
overflow: auto;
|
|
861
|
+
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
|
|
862
|
+
font-size: 12px;
|
|
863
|
+
line-height: 1.5;
|
|
864
|
+
white-space: pre-wrap;
|
|
865
|
+
word-wrap: break-word;
|
|
866
|
+
margin: 0;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.binary-info {
|
|
870
|
+
margin-bottom: 12px;
|
|
871
|
+
padding: 12px;
|
|
872
|
+
background: #2d2d2d;
|
|
873
|
+
border-radius: 6px;
|
|
874
|
+
color: #9cdcfe;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.text-preview {
|
|
878
|
+
background: #1e1e1e;
|
|
879
|
+
color: #d4d4d4;
|
|
880
|
+
padding: 20px;
|
|
881
|
+
border-radius: 8px;
|
|
882
|
+
max-height: 70vh;
|
|
883
|
+
overflow: auto;
|
|
884
|
+
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
|
|
885
|
+
font-size: 13px;
|
|
886
|
+
line-height: 1.6;
|
|
887
|
+
white-space: pre-wrap;
|
|
888
|
+
word-wrap: break-word;
|
|
889
|
+
margin: 0;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.html-rendered {
|
|
893
|
+
background: white;
|
|
894
|
+
padding: 20px;
|
|
895
|
+
border-radius: 8px;
|
|
896
|
+
max-height: 70vh;
|
|
897
|
+
overflow: auto;
|
|
898
|
+
border: 1px solid #eee;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.html-source .text-preview {
|
|
902
|
+
margin: 0;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.preview-container {
|
|
906
|
+
display: flex;
|
|
907
|
+
flex-direction: column;
|
|
908
|
+
height: 100%;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.preview-header {
|
|
912
|
+
display: flex;
|
|
913
|
+
justify-content: flex-end;
|
|
914
|
+
margin-bottom: 12px;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.preview-header .copy-btn {
|
|
918
|
+
padding: 8px 16px;
|
|
919
|
+
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
|
920
|
+
color: white;
|
|
921
|
+
border: none;
|
|
922
|
+
border-radius: 6px;
|
|
923
|
+
font-size: 14px;
|
|
924
|
+
cursor: pointer;
|
|
925
|
+
transition: all 0.2s;
|
|
926
|
+
display: flex;
|
|
927
|
+
align-items: center;
|
|
928
|
+
gap: 6px;
|
|
929
|
+
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.preview-header .copy-btn:hover {
|
|
933
|
+
transform: translateY(-1px);
|
|
934
|
+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
|
|
935
|
+
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.preview-header .copy-btn:active {
|
|
939
|
+
transform: translateY(0);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.empty-state {
|
|
943
|
+
text-align: center;
|
|
944
|
+
padding: 60px 20px;
|
|
945
|
+
color: #999;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.empty-state svg {
|
|
949
|
+
width: 100px;
|
|
950
|
+
height: 100px;
|
|
951
|
+
margin-bottom: 20px;
|
|
952
|
+
opacity: 0.5;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.loading {
|
|
956
|
+
text-align: center;
|
|
957
|
+
padding: 60px 20px;
|
|
958
|
+
color: #666;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.spinner {
|
|
962
|
+
display: inline-block;
|
|
963
|
+
width: 40px;
|
|
964
|
+
height: 40px;
|
|
965
|
+
border: 3px solid #f0f0f0;
|
|
966
|
+
border-radius: 50%;
|
|
967
|
+
border-top-color: #667eea;
|
|
968
|
+
animation: spin 0.8s linear infinite;
|
|
969
|
+
margin-bottom: 16px;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@keyframes spin {
|
|
973
|
+
to {
|
|
974
|
+
transform: rotate(360deg);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
</style>
|
|
978
|
+
</head>
|
|
979
|
+
<body>
|
|
980
|
+
<svg class="icon-sprite" aria-hidden="true" focusable="false">
|
|
981
|
+
<symbol id="icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
982
|
+
<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>
|
|
983
|
+
</symbol>
|
|
984
|
+
<symbol id="icon-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
985
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
986
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
987
|
+
</symbol>
|
|
988
|
+
</svg>
|
|
989
|
+
<div class="container">
|
|
990
|
+
<div class="header">
|
|
991
|
+
<h1>📁 文件浏览器</h1>
|
|
992
|
+
<div class="breadcrumb" id="breadcrumb"></div>
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<div class="file-container">
|
|
996
|
+
<div class="toolbar">
|
|
997
|
+
<button class="back-btn" id="backBtn" onclick="goBack()">
|
|
998
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
999
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
1000
|
+
</svg>
|
|
1001
|
+
返回上级
|
|
1002
|
+
</button>
|
|
1003
|
+
<button class="refresh-btn" onclick="loadDirectory(currentPath)" title="刷新">🔄</button>
|
|
1004
|
+
<button
|
|
1005
|
+
class="toolbar-action-btn edit-only edit-inline"
|
|
1006
|
+
onclick="createEntry('directory')"
|
|
1007
|
+
title="新建文件夹"
|
|
1008
|
+
>
|
|
1009
|
+
新建文件夹
|
|
1010
|
+
</button>
|
|
1011
|
+
<button class="toolbar-action-btn edit-only edit-inline" onclick="createEntry('file')" title="新建文件">
|
|
1012
|
+
新建文件
|
|
1013
|
+
</button>
|
|
1014
|
+
<button
|
|
1015
|
+
class="toolbar-action-btn edit-only edit-inline"
|
|
1016
|
+
onclick="document.getElementById('uploadFilesInput').click()"
|
|
1017
|
+
title="上传文件"
|
|
1018
|
+
>
|
|
1019
|
+
上传文件
|
|
1020
|
+
</button>
|
|
1021
|
+
<button
|
|
1022
|
+
class="toolbar-action-btn edit-only edit-inline"
|
|
1023
|
+
onclick="document.getElementById('uploadFolderInput').click()"
|
|
1024
|
+
title="上传文件夹"
|
|
1025
|
+
>
|
|
1026
|
+
上传文件夹
|
|
1027
|
+
</button>
|
|
1028
|
+
<input id="uploadFilesInput" type="file" multiple hidden onchange="uploadSelectedFiles(this.files)" />
|
|
1029
|
+
<input
|
|
1030
|
+
id="uploadFolderInput"
|
|
1031
|
+
type="file"
|
|
1032
|
+
webkitdirectory
|
|
1033
|
+
directory
|
|
1034
|
+
hidden
|
|
1035
|
+
onchange="uploadSelectedFiles(this.files)"
|
|
1036
|
+
/>
|
|
1037
|
+
<button
|
|
1038
|
+
class="toolbar-action-btn danger edit-only edit-inline"
|
|
1039
|
+
id="deleteSelectedBtn"
|
|
1040
|
+
onclick="deleteSelectedPaths()"
|
|
1041
|
+
title="删除选中项"
|
|
1042
|
+
disabled
|
|
1043
|
+
>
|
|
1044
|
+
删除选中
|
|
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>
|
|
1055
|
+
<button
|
|
1056
|
+
class="toggle-hidden-btn"
|
|
1057
|
+
id="toggleHiddenBtn"
|
|
1058
|
+
onclick="toggleHiddenFiles()"
|
|
1059
|
+
title="显示/隐藏隐藏文件"
|
|
1060
|
+
>
|
|
1061
|
+
👁️
|
|
1062
|
+
</button>
|
|
1063
|
+
<button
|
|
1064
|
+
class="toggle-hidden-btn"
|
|
1065
|
+
id="toggleBirthtimeBtn"
|
|
1066
|
+
onclick="toggleBirthtimeFiles()"
|
|
1067
|
+
title="显示/隐藏创建时间"
|
|
1068
|
+
>
|
|
1069
|
+
⌛️
|
|
1070
|
+
</button>
|
|
1071
|
+
<div class="stats-info" id="statsInfo"></div>
|
|
1072
|
+
<div class="search-box">
|
|
1073
|
+
<input type="text" id="searchInput" placeholder="搜索文件名..." oninput="handleSearch()" />
|
|
1074
|
+
</div>
|
|
1075
|
+
<div class="sort-controls">
|
|
1076
|
+
<select class="sort-select" id="sortSelect" onchange="handleSort()">
|
|
1077
|
+
<option value="name">名称</option>
|
|
1078
|
+
<option value="time">时间</option>
|
|
1079
|
+
<option value="size">大小</option>
|
|
1080
|
+
</select>
|
|
1081
|
+
<button class="sort-order-btn" id="sortOrderBtn" onclick="toggleSortOrder()">
|
|
1082
|
+
<span id="sortOrderIcon">↑</span>
|
|
1083
|
+
</button>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="view-toggle">
|
|
1086
|
+
<button class="view-btn active" id="gridViewBtn" onclick="setView('grid')">网格</button>
|
|
1087
|
+
<button class="view-btn" id="listViewBtn" onclick="setView('list')">列表</button>
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
<div class="file-grid" id="fileGrid"></div>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
|
|
1094
|
+
<div class="modal" id="previewModal">
|
|
1095
|
+
<div class="modal-content">
|
|
1096
|
+
<div class="modal-header">
|
|
1097
|
+
<div class="modal-title" id="previewTitle"></div>
|
|
1098
|
+
<button class="close-btn" onclick="closeModal()">×</button>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div class="modal-body" id="previewBody"></div>
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
<div class="context-menu" id="contextMenu">
|
|
1105
|
+
<div class="context-menu-item" id="ctxOpen" onclick="handleContextMenuAction('open')">📂 打开</div>
|
|
1106
|
+
<div class="context-menu-item" id="ctxCopyPath" onclick="handleContextMenuAction('copyPath')">📋 复制路径</div>
|
|
1107
|
+
<div class="context-menu-divider"></div>
|
|
1108
|
+
<div class="context-menu-item" id="ctxOpenInExplorer" onclick="handleContextMenuAction('openInExplorer')">
|
|
1109
|
+
🖥️ 在文件管理器中打开
|
|
1110
|
+
</div>
|
|
1111
|
+
<div class="context-menu-item" id="ctxDownload" onclick="handleContextMenuAction('download')">⬇️ 下载</div>
|
|
1112
|
+
<div class="context-menu-divider edit-only edit-block"></div>
|
|
1113
|
+
<div class="context-menu-item edit-only edit-flex" id="ctxRename" onclick="handleContextMenuAction('rename')">
|
|
1114
|
+
✏️ 重命名
|
|
1115
|
+
</div>
|
|
1116
|
+
<div class="context-menu-item edit-only edit-flex" id="ctxDelete" onclick="handleContextMenuAction('delete')">
|
|
1117
|
+
🗑️ 删除
|
|
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>
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<script>
|
|
1132
|
+
let currentPath = '/';
|
|
1133
|
+
let currentView = 'grid';
|
|
1134
|
+
let currentFiles = [];
|
|
1135
|
+
let allFiles = [];
|
|
1136
|
+
let searchQuery = '';
|
|
1137
|
+
let sortBy = 'name';
|
|
1138
|
+
let sortAscending = true;
|
|
1139
|
+
let showHiddenFiles = false;
|
|
1140
|
+
let showBirthtime = false;
|
|
1141
|
+
let currentPreviewContent = '';
|
|
1142
|
+
let currentPreviewObjectUrl = null;
|
|
1143
|
+
let currentZoom = 1;
|
|
1144
|
+
let currentRotation = 0;
|
|
1145
|
+
let currentContextMenuFile = null;
|
|
1146
|
+
let currentImageIndex = -1;
|
|
1147
|
+
let imageFiles = [];
|
|
1148
|
+
let selectedPaths = new Set();
|
|
1149
|
+
let editMode = false;
|
|
1150
|
+
let directoryRequestController = null;
|
|
1151
|
+
let directoryRequestId = 0;
|
|
1152
|
+
let virtualRenderFrame = null;
|
|
1153
|
+
let virtualState = null;
|
|
1154
|
+
let fileGridResizeObserver = null;
|
|
1155
|
+
let healthCheckTimer = null;
|
|
1156
|
+
let serviceWasUnavailable = false;
|
|
1157
|
+
let explorerInitializationPromise = null;
|
|
1158
|
+
let explorerLifecycleVersion = 0;
|
|
1159
|
+
let currentArchiveJobId = null;
|
|
1160
|
+
let archiveCapabilities = { createFormats: [], readExtensions: [] };
|
|
1161
|
+
|
|
1162
|
+
const EXPLORER_AUTH_STORAGE_KEY = 'mock-service-cli.file-explorer.password';
|
|
1163
|
+
const EXPLORER_PATH_STORAGE_KEY = 'mock-service-cli.file-explorer.path';
|
|
1164
|
+
|
|
1165
|
+
const LIST_ROW_HEIGHT = 37;
|
|
1166
|
+
const GRID_ROW_HEIGHT = 132;
|
|
1167
|
+
const GRID_MIN_ITEM_WIDTH = 100;
|
|
1168
|
+
const VIRTUAL_BUFFER_ROWS = 6;
|
|
1169
|
+
const MAX_UPLOAD_FILE_SIZE = 2 * 1024 * 1024 * 1024;
|
|
1170
|
+
const MAX_UPLOAD_BATCH_SIZE = 1024 * 1024 * 1024;
|
|
1171
|
+
const MAX_UPLOAD_BATCH_FILE_COUNT = 100;
|
|
1172
|
+
|
|
1173
|
+
let eventHandlers = [];
|
|
1174
|
+
let searchDebounceTimer = null;
|
|
1175
|
+
|
|
1176
|
+
function debounce(func, wait) {
|
|
1177
|
+
return function (...args) {
|
|
1178
|
+
clearTimeout(searchDebounceTimer);
|
|
1179
|
+
searchDebounceTimer = setTimeout(() => func.apply(this, args), wait);
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function addEventListenerWithCleanup(element, event, handler, options) {
|
|
1184
|
+
element.addEventListener(event, handler, options);
|
|
1185
|
+
eventHandlers.push({ element, event, handler, options });
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function cleanupEventListeners() {
|
|
1189
|
+
if (searchDebounceTimer) {
|
|
1190
|
+
clearTimeout(searchDebounceTimer);
|
|
1191
|
+
searchDebounceTimer = null;
|
|
1192
|
+
}
|
|
1193
|
+
eventHandlers.forEach(({ element, event, handler, options }) => {
|
|
1194
|
+
element.removeEventListener(event, handler, options);
|
|
1195
|
+
});
|
|
1196
|
+
eventHandlers = [];
|
|
1197
|
+
if (virtualRenderFrame) {
|
|
1198
|
+
cancelAnimationFrame(virtualRenderFrame);
|
|
1199
|
+
virtualRenderFrame = null;
|
|
1200
|
+
}
|
|
1201
|
+
if (directoryRequestController) {
|
|
1202
|
+
directoryRequestController.abort();
|
|
1203
|
+
directoryRequestController = null;
|
|
1204
|
+
}
|
|
1205
|
+
if (fileGridResizeObserver) {
|
|
1206
|
+
fileGridResizeObserver.disconnect();
|
|
1207
|
+
fileGridResizeObserver = null;
|
|
1208
|
+
}
|
|
1209
|
+
if (healthCheckTimer) {
|
|
1210
|
+
clearInterval(healthCheckTimer);
|
|
1211
|
+
healthCheckTimer = null;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function handlePageHide() {
|
|
1216
|
+
explorerLifecycleVersion += 1;
|
|
1217
|
+
explorerInitializationPromise = null;
|
|
1218
|
+
cleanupEventListeners();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function redirectToLogin() {
|
|
1222
|
+
sessionStorage.removeItem(EXPLORER_AUTH_STORAGE_KEY);
|
|
1223
|
+
window.location.replace('/__login');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function explorerFetch(url, options = {}) {
|
|
1227
|
+
const headers = new Headers(options.headers || {});
|
|
1228
|
+
const password = sessionStorage.getItem(EXPLORER_AUTH_STORAGE_KEY);
|
|
1229
|
+
if (password) headers.set('X-File-Explorer-Password', password);
|
|
1230
|
+
const response = await fetch(url, { ...options, headers });
|
|
1231
|
+
if (response.status === 401) redirectToLogin();
|
|
1232
|
+
return response;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
async function verifyExplorerAuth() {
|
|
1236
|
+
const password = sessionStorage.getItem(EXPLORER_AUTH_STORAGE_KEY);
|
|
1237
|
+
try {
|
|
1238
|
+
const response = await explorerFetch('/__api/auth/verify', {
|
|
1239
|
+
method: 'POST',
|
|
1240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1241
|
+
body: JSON.stringify({ password })
|
|
1242
|
+
});
|
|
1243
|
+
if (!response.ok) return false;
|
|
1244
|
+
return Boolean((await response.json()).success);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
console.error('Failed to verify file explorer authentication:', error);
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function startHealthCheck() {
|
|
1252
|
+
if (healthCheckTimer) return;
|
|
1253
|
+
healthCheckTimer = setInterval(async () => {
|
|
1254
|
+
try {
|
|
1255
|
+
const response = await explorerFetch('/__api/health');
|
|
1256
|
+
if (!response.ok) throw new Error(`Unexpected status ${response.status}`);
|
|
1257
|
+
if (serviceWasUnavailable) window.location.reload();
|
|
1258
|
+
serviceWasUnavailable = false;
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
serviceWasUnavailable = true;
|
|
1261
|
+
}
|
|
1262
|
+
}, 2000);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function syncEditModeUI() {
|
|
1266
|
+
document.body.classList.toggle('editing', editMode);
|
|
1267
|
+
|
|
1268
|
+
if (!editMode) {
|
|
1269
|
+
selectedPaths.clear();
|
|
1270
|
+
hideContextMenu();
|
|
1271
|
+
currentContextMenuFile = null;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
updateSelectionToolbar();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async function loadExplorerConfig() {
|
|
1278
|
+
try {
|
|
1279
|
+
const response = await explorerFetch('/__api/config');
|
|
1280
|
+
const config = await response.json();
|
|
1281
|
+
if (!response.ok || config.error) throw new Error(config.error || 'Failed to load configuration');
|
|
1282
|
+
editMode = Boolean(config.editMode);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
console.warn('Failed to load file explorer configuration:', error);
|
|
1285
|
+
editMode = false;
|
|
1286
|
+
}
|
|
1287
|
+
syncEditModeUI();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function loadArchiveCapabilities() {
|
|
1291
|
+
archiveCapabilities = { createFormats: [], readExtensions: [] };
|
|
1292
|
+
if (!editMode) return;
|
|
1293
|
+
try {
|
|
1294
|
+
const capabilities = await requestJson('/__api/archive/capabilities');
|
|
1295
|
+
archiveCapabilities = {
|
|
1296
|
+
createFormats: Array.isArray(capabilities.createFormats) ? capabilities.createFormats : [],
|
|
1297
|
+
readExtensions: Array.isArray(capabilities.readExtensions) ? capabilities.readExtensions : []
|
|
1298
|
+
};
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
console.warn('Failed to load archive capabilities:', error);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function handleFileGridClick(e) {
|
|
1305
|
+
const actionBtn = e.target.closest('[data-action]');
|
|
1306
|
+
if (actionBtn) {
|
|
1307
|
+
e.stopPropagation();
|
|
1308
|
+
const action = actionBtn.dataset.action;
|
|
1309
|
+
const path = actionBtn.dataset.path;
|
|
1310
|
+
const filename = actionBtn.dataset.filename;
|
|
1311
|
+
|
|
1312
|
+
switch (action) {
|
|
1313
|
+
case 'copyPath':
|
|
1314
|
+
copyToClipboard(path);
|
|
1315
|
+
break;
|
|
1316
|
+
case 'openInExplorer':
|
|
1317
|
+
openInExplorer(path);
|
|
1318
|
+
break;
|
|
1319
|
+
case 'download':
|
|
1320
|
+
downloadFile(path, filename);
|
|
1321
|
+
break;
|
|
1322
|
+
case 'rename':
|
|
1323
|
+
renamePath(path, filename);
|
|
1324
|
+
break;
|
|
1325
|
+
case 'delete':
|
|
1326
|
+
deletePath(path, filename);
|
|
1327
|
+
break;
|
|
1328
|
+
case 'toggleSelect':
|
|
1329
|
+
toggleSelectedPath(path, actionBtn.checked, actionBtn.closest('.file-item'));
|
|
1330
|
+
break;
|
|
1331
|
+
case 'more': {
|
|
1332
|
+
const fileItem = actionBtn.closest('.file-item');
|
|
1333
|
+
const rect = actionBtn.getBoundingClientRect();
|
|
1334
|
+
showContextMenu(rect.right, rect.bottom, fileItem.dataset.path, fileItem.dataset.isDirectory === 'true');
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const fileItem = e.target.closest('.file-item');
|
|
1342
|
+
if (fileItem) {
|
|
1343
|
+
const filePath = fileItem.dataset.path;
|
|
1344
|
+
const isDirectory = fileItem.dataset.isDirectory === 'true';
|
|
1345
|
+
handleItemClick(filePath, isDirectory);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function handleFileGridContextmenu(e) {
|
|
1350
|
+
const fileItem = e.target.closest('.file-item');
|
|
1351
|
+
if (fileItem) {
|
|
1352
|
+
e.preventDefault();
|
|
1353
|
+
const filePath = fileItem.dataset.path;
|
|
1354
|
+
const isDirectory = fileItem.dataset.isDirectory === 'true';
|
|
1355
|
+
showContextMenu(e.clientX, e.clientY, filePath, isDirectory);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function handleDocumentClick() {
|
|
1360
|
+
hideContextMenu();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function handleDocumentScroll() {
|
|
1364
|
+
hideContextMenu();
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function handleFileGridScroll() {
|
|
1368
|
+
hideContextMenu();
|
|
1369
|
+
scheduleVirtualRender();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function handleModalClick(e) {
|
|
1373
|
+
if (e.target === e.currentTarget) {
|
|
1374
|
+
closeModal();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function handleModalWheel(e) {
|
|
1379
|
+
const img = document.getElementById('previewImage');
|
|
1380
|
+
if (!img) return;
|
|
1381
|
+
|
|
1382
|
+
e.preventDefault();
|
|
1383
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
1384
|
+
zoomImage(delta);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function handleDocumentKeydown(e) {
|
|
1388
|
+
if (e.key === 'Escape') {
|
|
1389
|
+
closeModal();
|
|
1390
|
+
}
|
|
1391
|
+
if (e.key === 'Backspace' && document.activeElement.tagName !== 'INPUT') {
|
|
1392
|
+
e.preventDefault();
|
|
1393
|
+
goBack();
|
|
1394
|
+
}
|
|
1395
|
+
const img = document.getElementById('previewImage');
|
|
1396
|
+
if (img) {
|
|
1397
|
+
if (e.key === '+' || e.key === '=') {
|
|
1398
|
+
zoomImage(0.1);
|
|
1399
|
+
} else if (e.key === '-') {
|
|
1400
|
+
zoomImage(-0.1);
|
|
1401
|
+
} else if (e.key === '0') {
|
|
1402
|
+
resetImage();
|
|
1403
|
+
} else if (e.key === '[' || e.key === '{') {
|
|
1404
|
+
rotateImage(-90);
|
|
1405
|
+
} else if (e.key === ']' || e.key === '}') {
|
|
1406
|
+
rotateImage(90);
|
|
1407
|
+
} else if (e.key === 'ArrowLeft') {
|
|
1408
|
+
navigateImage(-1);
|
|
1409
|
+
} else if (e.key === 'ArrowRight') {
|
|
1410
|
+
navigateImage(1);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
async function initializeExplorer(isPageRestore = false) {
|
|
1416
|
+
if (explorerInitializationPromise) return explorerInitializationPromise;
|
|
1417
|
+
|
|
1418
|
+
const lifecycleVersion = explorerLifecycleVersion;
|
|
1419
|
+
const initializationPromise = (async () => {
|
|
1420
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
1421
|
+
const previewModal = document.getElementById('previewModal');
|
|
1422
|
+
|
|
1423
|
+
addEventListenerWithCleanup(fileGrid, 'click', handleFileGridClick);
|
|
1424
|
+
addEventListenerWithCleanup(fileGrid, 'contextmenu', handleFileGridContextmenu);
|
|
1425
|
+
addEventListenerWithCleanup(fileGrid, 'scroll', handleFileGridScroll, { passive: true });
|
|
1426
|
+
addEventListenerWithCleanup(document, 'click', handleDocumentClick);
|
|
1427
|
+
addEventListenerWithCleanup(document, 'scroll', handleDocumentScroll);
|
|
1428
|
+
addEventListenerWithCleanup(previewModal, 'click', handleModalClick);
|
|
1429
|
+
addEventListenerWithCleanup(previewModal, 'wheel', handleModalWheel, { passive: false });
|
|
1430
|
+
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
1431
|
+
|
|
1432
|
+
fileGridResizeObserver = new ResizeObserver(() => {
|
|
1433
|
+
if (virtualState) {
|
|
1434
|
+
renderFiles(currentFiles, false);
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
fileGridResizeObserver.observe(fileGrid);
|
|
1438
|
+
|
|
1439
|
+
if (!(await verifyExplorerAuth()) || lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1440
|
+
await loadExplorerConfig();
|
|
1441
|
+
if (lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1442
|
+
await loadArchiveCapabilities();
|
|
1443
|
+
if (lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1444
|
+
startHealthCheck();
|
|
1445
|
+
await loadDirectory(isPageRestore ? currentPath : sessionStorage.getItem(EXPLORER_PATH_STORAGE_KEY) || '/');
|
|
1446
|
+
})();
|
|
1447
|
+
explorerInitializationPromise = initializationPromise;
|
|
1448
|
+
|
|
1449
|
+
try {
|
|
1450
|
+
await initializationPromise;
|
|
1451
|
+
} finally {
|
|
1452
|
+
if (explorerInitializationPromise === initializationPromise) {
|
|
1453
|
+
explorerInitializationPromise = null;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
document.addEventListener('DOMContentLoaded', () => initializeExplorer());
|
|
1459
|
+
|
|
1460
|
+
window.addEventListener('pagehide', handlePageHide);
|
|
1461
|
+
window.addEventListener('pageshow', event => {
|
|
1462
|
+
if (event.persisted) initializeExplorer(true);
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
function showContextMenu(x, y, filePath, isDirectory) {
|
|
1466
|
+
currentContextMenuFile = { path: filePath, isDirectory };
|
|
1467
|
+
|
|
1468
|
+
const menu = document.getElementById('contextMenu');
|
|
1469
|
+
const ctxDownload = document.getElementById('ctxDownload');
|
|
1470
|
+
const isArchive = !isDirectory && isArchiveFile(filePath);
|
|
1471
|
+
const ctxExtract = document.getElementById('ctxExtract');
|
|
1472
|
+
const ctxArchivePreview = document.getElementById('ctxArchivePreview');
|
|
1473
|
+
|
|
1474
|
+
if (isDirectory) {
|
|
1475
|
+
ctxDownload.classList.add('disabled');
|
|
1476
|
+
} else {
|
|
1477
|
+
ctxDownload.classList.remove('disabled');
|
|
1478
|
+
}
|
|
1479
|
+
ctxExtract.classList.toggle('disabled', !isArchive);
|
|
1480
|
+
ctxArchivePreview.classList.toggle('disabled', !isArchive);
|
|
1481
|
+
|
|
1482
|
+
menu.style.left = `${x}px`;
|
|
1483
|
+
menu.style.top = `${y}px`;
|
|
1484
|
+
menu.classList.add('active');
|
|
1485
|
+
|
|
1486
|
+
const rect = menu.getBoundingClientRect();
|
|
1487
|
+
if (rect.right > window.innerWidth) {
|
|
1488
|
+
menu.style.left = `${x - rect.width}px`;
|
|
1489
|
+
}
|
|
1490
|
+
if (rect.bottom > window.innerHeight) {
|
|
1491
|
+
menu.style.top = `${y - rect.height}px`;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function hideContextMenu() {
|
|
1496
|
+
const menu = document.getElementById('contextMenu');
|
|
1497
|
+
menu.classList.remove('active');
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function handleContextMenuAction(action) {
|
|
1501
|
+
if (!currentContextMenuFile) return;
|
|
1502
|
+
|
|
1503
|
+
const { path, isDirectory } = currentContextMenuFile;
|
|
1504
|
+
|
|
1505
|
+
switch (action) {
|
|
1506
|
+
case 'open':
|
|
1507
|
+
handleItemClick(path, isDirectory);
|
|
1508
|
+
break;
|
|
1509
|
+
case 'copyPath':
|
|
1510
|
+
copyToClipboard(path);
|
|
1511
|
+
break;
|
|
1512
|
+
case 'openInExplorer':
|
|
1513
|
+
openInExplorer(path);
|
|
1514
|
+
break;
|
|
1515
|
+
case 'download':
|
|
1516
|
+
if (!isDirectory) {
|
|
1517
|
+
downloadFile(path, path.split('/').pop());
|
|
1518
|
+
}
|
|
1519
|
+
break;
|
|
1520
|
+
case 'rename':
|
|
1521
|
+
renamePath(path, path.split('/').pop());
|
|
1522
|
+
break;
|
|
1523
|
+
case 'delete':
|
|
1524
|
+
deletePath(path, path.split('/').pop());
|
|
1525
|
+
break;
|
|
1526
|
+
case 'compress':
|
|
1527
|
+
createArchiveFromSelection([path]);
|
|
1528
|
+
break;
|
|
1529
|
+
case 'extract':
|
|
1530
|
+
if (!isDirectory && isArchiveFile(path)) extractArchive(path);
|
|
1531
|
+
break;
|
|
1532
|
+
case 'archivePreview':
|
|
1533
|
+
if (!isDirectory && isArchiveFile(path)) previewArchive(path);
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
hideContextMenu();
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
async function loadDirectory(path) {
|
|
1541
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
1542
|
+
const oldCurrentPath = currentPath;
|
|
1543
|
+
const requestId = ++directoryRequestId;
|
|
1544
|
+
|
|
1545
|
+
if (directoryRequestController) {
|
|
1546
|
+
directoryRequestController.abort();
|
|
1547
|
+
}
|
|
1548
|
+
directoryRequestController = new AbortController();
|
|
1549
|
+
|
|
1550
|
+
allFiles = [];
|
|
1551
|
+
currentFiles = [];
|
|
1552
|
+
selectedPaths.clear();
|
|
1553
|
+
updateSelectionToolbar();
|
|
1554
|
+
|
|
1555
|
+
fileGrid.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
|
|
1556
|
+
|
|
1557
|
+
try {
|
|
1558
|
+
const response = await explorerFetch(`/__api/list?path=${encodeURIComponent(path)}`, {
|
|
1559
|
+
signal: directoryRequestController.signal
|
|
1560
|
+
});
|
|
1561
|
+
const data = await response.json();
|
|
1562
|
+
|
|
1563
|
+
if (!response.ok || data.error) {
|
|
1564
|
+
throw new Error(data.error);
|
|
1565
|
+
}
|
|
1566
|
+
if (requestId !== directoryRequestId) return;
|
|
1567
|
+
|
|
1568
|
+
currentPath = data.currentPath;
|
|
1569
|
+
sessionStorage.setItem(EXPLORER_PATH_STORAGE_KEY, currentPath);
|
|
1570
|
+
allFiles = data.files || [];
|
|
1571
|
+
|
|
1572
|
+
if (document.getElementById('searchInput')) {
|
|
1573
|
+
document.getElementById('searchInput').value = '';
|
|
1574
|
+
}
|
|
1575
|
+
searchQuery = '';
|
|
1576
|
+
|
|
1577
|
+
renderBreadcrumb(data.currentPath, data.parentPath);
|
|
1578
|
+
processAndRenderFiles();
|
|
1579
|
+
updateBackButton(data.parentPath);
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
if (error.name === 'AbortError' || requestId !== directoryRequestId) return;
|
|
1582
|
+
console.error('Error loading directory:', error);
|
|
1583
|
+
currentPath = oldCurrentPath;
|
|
1584
|
+
const parentPath = currentPath === '/' ? null : currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
|
1585
|
+
updateBackButton(parentPath);
|
|
1586
|
+
fileGrid.innerHTML = `<div class="empty-state"><p>错误: ${error.message}</p></div>`;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function updateStatsInfo(files) {
|
|
1591
|
+
const statsInfo = document.getElementById('statsInfo');
|
|
1592
|
+
if (!statsInfo) return;
|
|
1593
|
+
let fileTotalSize = 0;
|
|
1594
|
+
const dirCount = files.filter(f => {
|
|
1595
|
+
if (!f.isDirectory) {
|
|
1596
|
+
fileTotalSize += f.size;
|
|
1597
|
+
}
|
|
1598
|
+
return f.isDirectory;
|
|
1599
|
+
}).length;
|
|
1600
|
+
const fileCount = files.length - dirCount;
|
|
1601
|
+
const totalSize = formatSize(fileTotalSize);
|
|
1602
|
+
statsInfo.textContent = `${dirCount} 个文件夹, ${fileCount} 个文件 (${totalSize})`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function processAndRenderFiles() {
|
|
1606
|
+
// console.log('processAndRenderFiles called, allFiles:', allFiles);
|
|
1607
|
+
|
|
1608
|
+
if (!allFiles) {
|
|
1609
|
+
console.error('allFiles is null or undefined');
|
|
1610
|
+
renderFiles([]);
|
|
1611
|
+
updateStatsInfo([]);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
let files = [...allFiles];
|
|
1616
|
+
|
|
1617
|
+
// 隐藏文件过滤
|
|
1618
|
+
if (!showHiddenFiles) {
|
|
1619
|
+
files = files.filter(file => !file.isHidden);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// 搜索过滤
|
|
1623
|
+
if (searchQuery) {
|
|
1624
|
+
const query = searchQuery.toLowerCase();
|
|
1625
|
+
files = files.filter(file => file.name.toLowerCase().includes(query));
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// 排序
|
|
1629
|
+
files.sort((a, b) => {
|
|
1630
|
+
let result = 0;
|
|
1631
|
+
|
|
1632
|
+
// 目录始终排在前面
|
|
1633
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
1634
|
+
return a.isDirectory ? -1 : 1;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
switch (sortBy) {
|
|
1638
|
+
case 'name':
|
|
1639
|
+
result = a.name.localeCompare(b.name);
|
|
1640
|
+
break;
|
|
1641
|
+
case 'time':
|
|
1642
|
+
result = new Date(a.mtime) - new Date(b.mtime);
|
|
1643
|
+
break;
|
|
1644
|
+
case 'size':
|
|
1645
|
+
result = a.size - b.size;
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
return sortAscending ? result : -result;
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
currentFiles = files;
|
|
1653
|
+
// console.log('Rendering', files.length, 'files');
|
|
1654
|
+
updateStatsInfo(files);
|
|
1655
|
+
renderFiles(files);
|
|
1656
|
+
}
|
|
1657
|
+
function toggleBirthtimeFiles() {
|
|
1658
|
+
showBirthtime = !showBirthtime;
|
|
1659
|
+
const btn = document.getElementById('toggleBirthtimeBtn');
|
|
1660
|
+
if (!showBirthtime) {
|
|
1661
|
+
btn.classList.remove('active');
|
|
1662
|
+
btn.title = '显示创建时间';
|
|
1663
|
+
} else {
|
|
1664
|
+
btn.classList.add('active');
|
|
1665
|
+
btn.title = '隐藏创建时间';
|
|
1666
|
+
}
|
|
1667
|
+
processAndRenderFiles();
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function toggleHiddenFiles() {
|
|
1671
|
+
showHiddenFiles = !showHiddenFiles;
|
|
1672
|
+
const btn = document.getElementById('toggleHiddenBtn');
|
|
1673
|
+
if (showHiddenFiles) {
|
|
1674
|
+
btn.classList.add('active');
|
|
1675
|
+
btn.innerHTML = '👁️';
|
|
1676
|
+
btn.title = '隐藏隐藏文件';
|
|
1677
|
+
} else {
|
|
1678
|
+
btn.classList.remove('active');
|
|
1679
|
+
btn.innerHTML = '👁️🗨️';
|
|
1680
|
+
btn.title = '显示隐藏文件';
|
|
1681
|
+
}
|
|
1682
|
+
processAndRenderFiles();
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function copyToClipboard(text) {
|
|
1686
|
+
try {
|
|
1687
|
+
await navigator.clipboard.writeText(text);
|
|
1688
|
+
alert('路径已复制到剪贴板: ' + text);
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
console.error('复制失败:', error);
|
|
1691
|
+
alert('复制失败: ' + error.message);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const handleSearch = debounce(() => {
|
|
1696
|
+
searchQuery = document.getElementById('searchInput').value;
|
|
1697
|
+
processAndRenderFiles();
|
|
1698
|
+
}, 200);
|
|
1699
|
+
|
|
1700
|
+
function handleSort() {
|
|
1701
|
+
sortBy = document.getElementById('sortSelect').value;
|
|
1702
|
+
processAndRenderFiles();
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function toggleSortOrder() {
|
|
1706
|
+
sortAscending = !sortAscending;
|
|
1707
|
+
document.getElementById('sortOrderIcon').textContent = sortAscending ? '↑' : '↓';
|
|
1708
|
+
processAndRenderFiles();
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
async function openInExplorer(path) {
|
|
1712
|
+
try {
|
|
1713
|
+
const response = await explorerFetch('/__api/open-in-explorer', {
|
|
1714
|
+
method: 'POST',
|
|
1715
|
+
headers: {
|
|
1716
|
+
'Content-Type': 'application/json'
|
|
1717
|
+
},
|
|
1718
|
+
body: JSON.stringify({ path: path })
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
const data = await response.json();
|
|
1722
|
+
|
|
1723
|
+
if (!data.success) {
|
|
1724
|
+
alert('打开失败: ' + (data.error || '未知错误'));
|
|
1725
|
+
}
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
console.error('打开失败:', error);
|
|
1728
|
+
alert('打开失败: ' + error.message);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
async function requestJson(url, options) {
|
|
1733
|
+
const response = await explorerFetch(url, options);
|
|
1734
|
+
const data = await response.json();
|
|
1735
|
+
if (!response.ok || data.error) {
|
|
1736
|
+
throw new Error(data.error || '请求失败');
|
|
1737
|
+
}
|
|
1738
|
+
return data;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function isArchiveFile(filePath) {
|
|
1742
|
+
const lowerPath = String(filePath || '').toLowerCase();
|
|
1743
|
+
return archiveCapabilities.readExtensions.some(extension => lowerPath.endsWith(`.${String(extension).toLowerCase()}`));
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
async function createArchiveFromSelection(sourceOverride) {
|
|
1747
|
+
const sources = sourceOverride || [...selectedPaths];
|
|
1748
|
+
if (!sources.length) {
|
|
1749
|
+
showToast('请先选择要压缩的文件或文件夹');
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const defaultName = sources.length === 1 ? sources[0].split('/').pop().replace(/\.[^.]+$/, '') : 'archive';
|
|
1753
|
+
const name = window.prompt('压缩包名称', defaultName);
|
|
1754
|
+
if (name === null || !name.trim()) return;
|
|
1755
|
+
const formats = archiveCapabilities.createFormats;
|
|
1756
|
+
if (!formats.length) {
|
|
1757
|
+
showToast('当前版本未提供压缩格式');
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const format = window.prompt(`压缩格式(${formats.join(' 或 ')})`, formats[0]);
|
|
1761
|
+
if (format === null) return;
|
|
1762
|
+
if (!formats.includes(format.trim())) {
|
|
1763
|
+
alert(`仅支持 ${formats.join(' 或 ')}`);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
await startArchiveJob({
|
|
1767
|
+
operation: 'create',
|
|
1768
|
+
sources,
|
|
1769
|
+
destinationPath: currentPath,
|
|
1770
|
+
name: name.trim(),
|
|
1771
|
+
format: format.trim()
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
async function extractArchive(filePath) {
|
|
1776
|
+
const confirmed = window.confirm(`将 “${filePath.split('/').pop()}” 解压到当前目录吗?已有同名文件将导致整个操作失败。`);
|
|
1777
|
+
if (!confirmed) return;
|
|
1778
|
+
await startArchiveJob({ operation: 'extract', path: filePath, destinationPath: currentPath });
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
async function startArchiveJob(payload) {
|
|
1782
|
+
try {
|
|
1783
|
+
const job = await requestJson('/__api/archive/jobs', {
|
|
1784
|
+
method: 'POST',
|
|
1785
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1786
|
+
body: JSON.stringify(payload)
|
|
1787
|
+
});
|
|
1788
|
+
currentArchiveJobId = job.id;
|
|
1789
|
+
monitorArchiveJob(job.id);
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
alert(`归档操作无法启动: ${error.message}`);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
async function monitorArchiveJob(id) {
|
|
1796
|
+
const modal = document.getElementById('previewModal');
|
|
1797
|
+
const title = document.getElementById('previewTitle');
|
|
1798
|
+
const body = document.getElementById('previewBody');
|
|
1799
|
+
title.textContent = '归档任务';
|
|
1800
|
+
modal.classList.add('active');
|
|
1801
|
+
try {
|
|
1802
|
+
const job = await requestJson(`/__api/archive/jobs/${encodeURIComponent(id)}`);
|
|
1803
|
+
const progress = job.progress || {};
|
|
1804
|
+
body.innerHTML = `
|
|
1805
|
+
<div class="preview-container">
|
|
1806
|
+
<p>状态:${escapeHtml(job.status)}</p>
|
|
1807
|
+
<p>进度:${Number(progress.processedEntries || 0)} / ${Number(progress.totalEntries || 0)} 项</p>
|
|
1808
|
+
${job.status === 'running' || job.status === 'queued' ? '<button class="toolbar-action-btn danger" onclick="cancelArchiveJob()">取消任务</button>' : ''}
|
|
1809
|
+
${job.error ? `<p style="color:#d32f2f">${escapeHtml(job.error)}</p>` : ''}
|
|
1810
|
+
</div>
|
|
1811
|
+
`;
|
|
1812
|
+
if (job.status === 'running' || job.status === 'queued') {
|
|
1813
|
+
setTimeout(() => {
|
|
1814
|
+
if (currentArchiveJobId === id) monitorArchiveJob(id);
|
|
1815
|
+
}, 500);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
currentArchiveJobId = null;
|
|
1819
|
+
if (job.status === 'completed') {
|
|
1820
|
+
showToast('归档任务已完成');
|
|
1821
|
+
await loadDirectory(currentPath);
|
|
1822
|
+
}
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
currentArchiveJobId = null;
|
|
1825
|
+
body.innerHTML = `<div class="empty-state"><p>归档任务失败: ${escapeHtml(error.message)}</p></div>`;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function cancelArchiveJob() {
|
|
1830
|
+
if (!currentArchiveJobId) return;
|
|
1831
|
+
try {
|
|
1832
|
+
await requestJson(`/__api/archive/jobs/${encodeURIComponent(currentArchiveJobId)}`, { method: 'DELETE' });
|
|
1833
|
+
showToast('正在取消归档任务');
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
alert(`取消任务失败: ${error.message}`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
async function previewArchive(filePath) {
|
|
1840
|
+
const modal = document.getElementById('previewModal');
|
|
1841
|
+
const title = document.getElementById('previewTitle');
|
|
1842
|
+
const body = document.getElementById('previewBody');
|
|
1843
|
+
title.textContent = `${filePath.split('/').pop()} - 压缩包内容`;
|
|
1844
|
+
body.innerHTML = '<div class="loading"><div class="spinner"></div><p>读取压缩包目录...</p></div>';
|
|
1845
|
+
modal.classList.add('active');
|
|
1846
|
+
try {
|
|
1847
|
+
const preview = await requestJson(`/__api/archive/preview?path=${encodeURIComponent(filePath)}`);
|
|
1848
|
+
const entries = preview.entries || [];
|
|
1849
|
+
body.innerHTML = `
|
|
1850
|
+
<div class="preview-container">
|
|
1851
|
+
<div class="binary-info">${entries.length} 项 · 解压后 ${formatSize(preview.totalSize || 0)} · 包内数据 ${formatSize(preview.totalPackedSize || 0)}</div>
|
|
1852
|
+
<div class="text-preview">${entries
|
|
1853
|
+
.map(entry => {
|
|
1854
|
+
const depth = entry.path.split('/').length - 1;
|
|
1855
|
+
const label = `${entry.isDirectory ? '📁' : '📄'} ${entry.path.split('/').pop()}${entry.isDirectory ? '/' : ` (${formatSize(entry.size || 0)})`}`;
|
|
1856
|
+
return `<div style="padding-left:${depth * 16}px">${escapeHtml(label)}</div>`;
|
|
1857
|
+
})
|
|
1858
|
+
.join('')}</div>
|
|
1859
|
+
</div>
|
|
1860
|
+
`;
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
body.innerHTML = `<div class="empty-state"><p>读取压缩包失败: ${escapeHtml(error.message)}</p></div>`;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function createUploadBatches(files) {
|
|
1867
|
+
const batches = [];
|
|
1868
|
+
const failed = [];
|
|
1869
|
+
let batch = [];
|
|
1870
|
+
let batchSize = 0;
|
|
1871
|
+
|
|
1872
|
+
Array.from(files).forEach(file => {
|
|
1873
|
+
if (file.size > MAX_UPLOAD_FILE_SIZE) {
|
|
1874
|
+
failed.push({ name: file.webkitRelativePath || file.name, error: '单文件超过 2GB 限制' });
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const exceedsBatchLimit =
|
|
1878
|
+
batch.length >= MAX_UPLOAD_BATCH_FILE_COUNT || batchSize + file.size > MAX_UPLOAD_BATCH_SIZE;
|
|
1879
|
+
if (batch.length && exceedsBatchLimit) {
|
|
1880
|
+
batches.push(batch);
|
|
1881
|
+
batch = [];
|
|
1882
|
+
batchSize = 0;
|
|
1883
|
+
}
|
|
1884
|
+
batch.push(file);
|
|
1885
|
+
batchSize += file.size;
|
|
1886
|
+
});
|
|
1887
|
+
if (batch.length) batches.push(batch);
|
|
1888
|
+
return { batches, failed };
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
async function uploadBatch(files) {
|
|
1892
|
+
const formData = new FormData();
|
|
1893
|
+
formData.append('parentPath', currentPath);
|
|
1894
|
+
files.forEach(file => {
|
|
1895
|
+
formData.append('files', file, file.webkitRelativePath || file.name);
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
const response = await explorerFetch('/__api/upload', { method: 'POST', body: formData });
|
|
1899
|
+
const result = await response.json();
|
|
1900
|
+
if (!response.ok && response.status !== 207) throw new Error(result.error || '上传失败');
|
|
1901
|
+
return result;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
async function uploadSelectedFiles(files) {
|
|
1905
|
+
const input = document.activeElement;
|
|
1906
|
+
if (!files || files.length === 0) return;
|
|
1907
|
+
const { batches, failed } = createUploadBatches(files);
|
|
1908
|
+
const uploaded = [];
|
|
1909
|
+
|
|
1910
|
+
try {
|
|
1911
|
+
for (let index = 0; index < batches.length; index++) {
|
|
1912
|
+
const batch = batches[index];
|
|
1913
|
+
showToast(`正在上传第 ${index + 1}/${batches.length} 批(${batch.length} 项)`);
|
|
1914
|
+
try {
|
|
1915
|
+
const result = await uploadBatch(batch);
|
|
1916
|
+
uploaded.push(...result.uploaded);
|
|
1917
|
+
failed.push(...result.failed);
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
batch.forEach(file => failed.push({ name: file.webkitRelativePath || file.name, error: error.message }));
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const messages = [];
|
|
1924
|
+
if (uploaded.length) messages.push(`已上传 ${uploaded.length} 项`);
|
|
1925
|
+
if (failed.length) messages.push(`${failed.length} 项未上传`);
|
|
1926
|
+
showToast(messages.join(';'));
|
|
1927
|
+
if (failed.length) console.warn('上传失败项:', failed);
|
|
1928
|
+
if (uploaded.length) await loadDirectory(currentPath);
|
|
1929
|
+
} finally {
|
|
1930
|
+
document.getElementById('uploadFilesInput').value = '';
|
|
1931
|
+
document.getElementById('uploadFolderInput').value = '';
|
|
1932
|
+
if (input && input.blur) input.blur();
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
async function createEntry(type) {
|
|
1937
|
+
const label = type === 'directory' ? '文件夹' : '文件';
|
|
1938
|
+
const name = window.prompt(`请输入${label}名称`);
|
|
1939
|
+
if (name === null) return;
|
|
1940
|
+
|
|
1941
|
+
const trimmedName = name.trim();
|
|
1942
|
+
if (!trimmedName) {
|
|
1943
|
+
alert(`${label}名称不能为空`);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
try {
|
|
1948
|
+
await requestJson('/__api/path', {
|
|
1949
|
+
method: 'POST',
|
|
1950
|
+
headers: {
|
|
1951
|
+
'Content-Type': 'application/json'
|
|
1952
|
+
},
|
|
1953
|
+
body: JSON.stringify({
|
|
1954
|
+
parentPath: currentPath,
|
|
1955
|
+
name: trimmedName,
|
|
1956
|
+
type
|
|
1957
|
+
})
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
showToast(`新建${label}成功`);
|
|
1961
|
+
await loadDirectory(currentPath);
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
console.error(`新建${label}失败:`, error);
|
|
1964
|
+
alert(`新建${label}失败: ` + error.message);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
async function renamePath(filePath, fileName) {
|
|
1969
|
+
const nextName = window.prompt('请输入新名称', fileName || filePath.split('/').pop());
|
|
1970
|
+
if (nextName === null) return;
|
|
1971
|
+
|
|
1972
|
+
const trimmedName = nextName.trim();
|
|
1973
|
+
if (!trimmedName) {
|
|
1974
|
+
alert('名称不能为空');
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
try {
|
|
1979
|
+
await requestJson('/__api/path', {
|
|
1980
|
+
method: 'PATCH',
|
|
1981
|
+
headers: {
|
|
1982
|
+
'Content-Type': 'application/json'
|
|
1983
|
+
},
|
|
1984
|
+
body: JSON.stringify({
|
|
1985
|
+
path: filePath,
|
|
1986
|
+
name: trimmedName
|
|
1987
|
+
})
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
showToast('重命名成功');
|
|
1991
|
+
await loadDirectory(currentPath);
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
console.error('重命名失败:', error);
|
|
1994
|
+
alert('重命名失败: ' + error.message);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
async function downloadFile(filePath, fileName) {
|
|
1999
|
+
try {
|
|
2000
|
+
const response = await explorerFetch(`/__api/file?path=${encodeURIComponent(filePath)}&download=1`);
|
|
2001
|
+
if (!response.ok) throw new Error('下载失败');
|
|
2002
|
+
const objectUrl = URL.createObjectURL(await response.blob());
|
|
2003
|
+
const a = document.createElement('a');
|
|
2004
|
+
a.href = objectUrl;
|
|
2005
|
+
a.download = fileName || '';
|
|
2006
|
+
a.style.display = 'none';
|
|
2007
|
+
document.body.appendChild(a);
|
|
2008
|
+
a.click();
|
|
2009
|
+
a.remove();
|
|
2010
|
+
URL.revokeObjectURL(objectUrl);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
console.error('下载失败:', error);
|
|
2013
|
+
alert(`下载失败: ${error.message}`);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
async function deletePath(filePath, fileName) {
|
|
2018
|
+
const label = fileName || filePath.split('/').pop() || filePath;
|
|
2019
|
+
const confirmed = window.confirm(`确定要删除 "${label}" 吗?此操作不可恢复。`);
|
|
2020
|
+
|
|
2021
|
+
if (!confirmed) return;
|
|
2022
|
+
|
|
2023
|
+
try {
|
|
2024
|
+
const response = await explorerFetch('/__api/path', {
|
|
2025
|
+
method: 'DELETE',
|
|
2026
|
+
headers: {
|
|
2027
|
+
'Content-Type': 'application/json'
|
|
2028
|
+
},
|
|
2029
|
+
body: JSON.stringify({ path: filePath })
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
const data = await response.json();
|
|
2033
|
+
if (!response.ok || !data.success) {
|
|
2034
|
+
throw new Error(data.error || '删除失败');
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
closeModal();
|
|
2038
|
+
showToast('删除成功');
|
|
2039
|
+
await loadDirectory(currentPath);
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
console.error('删除失败:', error);
|
|
2042
|
+
alert('删除失败: ' + error.message);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
async function deleteSelectedPaths() {
|
|
2047
|
+
const paths = Array.from(selectedPaths);
|
|
2048
|
+
if (paths.length === 0) return;
|
|
2049
|
+
|
|
2050
|
+
const confirmed = window.confirm(`确定要删除选中的 ${paths.length} 项吗?此操作不可恢复。`);
|
|
2051
|
+
if (!confirmed) return;
|
|
2052
|
+
|
|
2053
|
+
try {
|
|
2054
|
+
await requestJson('/__api/paths', {
|
|
2055
|
+
method: 'DELETE',
|
|
2056
|
+
headers: {
|
|
2057
|
+
'Content-Type': 'application/json'
|
|
2058
|
+
},
|
|
2059
|
+
body: JSON.stringify({ paths })
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
selectedPaths.clear();
|
|
2063
|
+
updateSelectionToolbar();
|
|
2064
|
+
showToast('批量删除成功');
|
|
2065
|
+
await loadDirectory(currentPath);
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
console.error('批量删除失败:', error);
|
|
2068
|
+
alert('批量删除失败: ' + error.message);
|
|
2069
|
+
await loadDirectory(currentPath);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function toggleSelectedPath(filePath, checked, fileItem) {
|
|
2074
|
+
if (checked) {
|
|
2075
|
+
selectedPaths.add(filePath);
|
|
2076
|
+
} else {
|
|
2077
|
+
selectedPaths.delete(filePath);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
if (fileItem) {
|
|
2081
|
+
fileItem.classList.toggle('selected', checked);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
updateSelectionToolbar();
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function updateSelectionToolbar() {
|
|
2088
|
+
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
|
2089
|
+
const archiveSelectedBtn = document.getElementById('archiveSelectedBtn');
|
|
2090
|
+
if (!deleteSelectedBtn) return;
|
|
2091
|
+
|
|
2092
|
+
const count = selectedPaths.size;
|
|
2093
|
+
deleteSelectedBtn.disabled = count === 0;
|
|
2094
|
+
deleteSelectedBtn.textContent = count ? `删除选中(${count})` : '删除选中';
|
|
2095
|
+
if (archiveSelectedBtn) {
|
|
2096
|
+
archiveSelectedBtn.disabled = count === 0;
|
|
2097
|
+
archiveSelectedBtn.textContent = count ? `压缩选中(${count})` : '压缩选中';
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function renderBreadcrumb(currentPath) {
|
|
2102
|
+
const breadcrumb = document.getElementById('breadcrumb');
|
|
2103
|
+
const parts = currentPath.split('/').filter(Boolean);
|
|
2104
|
+
const fragment = document.createDocumentFragment();
|
|
2105
|
+
const root = document.createElement('div');
|
|
2106
|
+
root.className = 'breadcrumb-item';
|
|
2107
|
+
root.textContent = '根目录';
|
|
2108
|
+
root.addEventListener('click', () => loadDirectory('/'));
|
|
2109
|
+
fragment.appendChild(root);
|
|
2110
|
+
let current = '';
|
|
2111
|
+
parts.forEach((part, index) => {
|
|
2112
|
+
current += '/' + part;
|
|
2113
|
+
const isLast = index === parts.length - 1;
|
|
2114
|
+
const separator = document.createElement('span');
|
|
2115
|
+
separator.className = 'breadcrumb-separator';
|
|
2116
|
+
separator.textContent = '/';
|
|
2117
|
+
fragment.appendChild(separator);
|
|
2118
|
+
const item = document.createElement('div');
|
|
2119
|
+
item.className = `breadcrumb-item${isLast ? ' current' : ''}`;
|
|
2120
|
+
item.textContent = part;
|
|
2121
|
+
if (isLast) {
|
|
2122
|
+
} else {
|
|
2123
|
+
const targetPath = current;
|
|
2124
|
+
item.addEventListener('click', () => loadDirectory(targetPath));
|
|
2125
|
+
}
|
|
2126
|
+
fragment.appendChild(item);
|
|
2127
|
+
});
|
|
2128
|
+
breadcrumb.replaceChildren(fragment);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function renderFiles(files, resetScroll = true) {
|
|
2132
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
2133
|
+
const toggleBirthtimeBtn = document.getElementById('toggleBirthtimeBtn');
|
|
2134
|
+
|
|
2135
|
+
if (currentView === 'list') {
|
|
2136
|
+
fileGrid.classList.add('list-view');
|
|
2137
|
+
fileGrid.classList.toggle('show-birthtime', showBirthtime);
|
|
2138
|
+
toggleBirthtimeBtn.style.display = 'inline-block';
|
|
2139
|
+
} else {
|
|
2140
|
+
toggleBirthtimeBtn.style.display = 'none';
|
|
2141
|
+
fileGrid.classList.remove('list-view');
|
|
2142
|
+
fileGrid.classList.remove('show-birthtime');
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
if (!files || files.length === 0) {
|
|
2146
|
+
virtualState = null;
|
|
2147
|
+
fileGrid.innerHTML = '<div class="empty-state"><p>该目录为空</p></div>';
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
const isListView = currentView === 'list';
|
|
2152
|
+
const columnCount = isListView
|
|
2153
|
+
? 1
|
|
2154
|
+
: Math.max(
|
|
2155
|
+
1,
|
|
2156
|
+
Math.floor((Math.max(fileGrid.clientWidth, GRID_MIN_ITEM_WIDTH + 16) - 8) / (GRID_MIN_ITEM_WIDTH + 8))
|
|
2157
|
+
);
|
|
2158
|
+
const rowHeight = isListView ? getListRowHeight(fileGrid) : GRID_ROW_HEIGHT;
|
|
2159
|
+
const totalRows = Math.ceil(files.length / columnCount);
|
|
2160
|
+
const gridItemWidth = isListView ? null : (fileGrid.clientWidth - 16 - (columnCount - 1) * 8) / columnCount;
|
|
2161
|
+
const spacer = document.createElement('div');
|
|
2162
|
+
spacer.className = 'virtual-spacer';
|
|
2163
|
+
spacer.style.height = `${totalRows * rowHeight + (isListView ? 0 : 8)}px`;
|
|
2164
|
+
const content = document.createElement('div');
|
|
2165
|
+
content.className = `virtual-content ${isListView ? 'list-view' : 'grid-view'}`;
|
|
2166
|
+
|
|
2167
|
+
virtualState = {
|
|
2168
|
+
files,
|
|
2169
|
+
columnCount,
|
|
2170
|
+
rowHeight,
|
|
2171
|
+
gridItemWidth,
|
|
2172
|
+
isListView,
|
|
2173
|
+
spacer,
|
|
2174
|
+
content,
|
|
2175
|
+
itemPool: [],
|
|
2176
|
+
freeItems: [],
|
|
2177
|
+
renderedItems: new Map(),
|
|
2178
|
+
startRow: -1,
|
|
2179
|
+
endRow: -1
|
|
2180
|
+
};
|
|
2181
|
+
fileGrid.replaceChildren(spacer, content);
|
|
2182
|
+
if (resetScroll) fileGrid.scrollTop = 0;
|
|
2183
|
+
renderVirtualWindow();
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
function scheduleVirtualRender() {
|
|
2187
|
+
if (virtualRenderFrame || !virtualState) return;
|
|
2188
|
+
virtualRenderFrame = requestAnimationFrame(() => {
|
|
2189
|
+
virtualRenderFrame = null;
|
|
2190
|
+
renderVirtualWindow();
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
function getListRowHeight(fileGrid) {
|
|
2195
|
+
const rowHeight = Number.parseFloat(getComputedStyle(fileGrid).getPropertyValue('--list-row-height'));
|
|
2196
|
+
return Number.isFinite(rowHeight) ? rowHeight : LIST_ROW_HEIGHT;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function renderVirtualWindow() {
|
|
2200
|
+
if (!virtualState) return;
|
|
2201
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
2202
|
+
const {
|
|
2203
|
+
files,
|
|
2204
|
+
columnCount,
|
|
2205
|
+
rowHeight,
|
|
2206
|
+
gridItemWidth,
|
|
2207
|
+
isListView,
|
|
2208
|
+
content,
|
|
2209
|
+
itemPool,
|
|
2210
|
+
freeItems,
|
|
2211
|
+
renderedItems
|
|
2212
|
+
} = virtualState;
|
|
2213
|
+
const firstVisibleRow = Math.floor(fileGrid.scrollTop / rowHeight);
|
|
2214
|
+
const visibleRows = Math.ceil(fileGrid.clientHeight / rowHeight);
|
|
2215
|
+
const startRow = Math.max(0, firstVisibleRow - VIRTUAL_BUFFER_ROWS);
|
|
2216
|
+
const endRow = Math.min(
|
|
2217
|
+
Math.ceil(files.length / columnCount),
|
|
2218
|
+
firstVisibleRow + visibleRows + VIRTUAL_BUFFER_ROWS
|
|
2219
|
+
);
|
|
2220
|
+
const startIndex = startRow * columnCount;
|
|
2221
|
+
const endIndex = Math.min(files.length, endRow * columnCount);
|
|
2222
|
+
|
|
2223
|
+
if (virtualState.startRow === startRow && virtualState.endRow === endRow) return;
|
|
2224
|
+
virtualState.startRow = startRow;
|
|
2225
|
+
virtualState.endRow = endRow;
|
|
2226
|
+
renderedItems.forEach((fileItem, fileIndex) => {
|
|
2227
|
+
if (fileIndex < startIndex || fileIndex >= endIndex) {
|
|
2228
|
+
renderedItems.delete(fileIndex);
|
|
2229
|
+
fileItem.hidden = true;
|
|
2230
|
+
freeItems.push(fileItem);
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
for (let fileIndex = startIndex; fileIndex < endIndex; fileIndex++) {
|
|
2235
|
+
if (renderedItems.has(fileIndex)) continue;
|
|
2236
|
+
|
|
2237
|
+
let fileItem = freeItems.pop();
|
|
2238
|
+
if (!fileItem) {
|
|
2239
|
+
fileItem = createFileItemElement(isListView);
|
|
2240
|
+
itemPool.push(fileItem);
|
|
2241
|
+
content.appendChild(fileItem);
|
|
2242
|
+
}
|
|
2243
|
+
updateFileItem(fileItem, files[fileIndex]);
|
|
2244
|
+
positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView);
|
|
2245
|
+
fileItem.hidden = false;
|
|
2246
|
+
renderedItems.set(fileIndex, fileItem);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
function positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView) {
|
|
2251
|
+
fileItem.style.transform = '';
|
|
2252
|
+
if (isListView) {
|
|
2253
|
+
fileItem.style.left = '0';
|
|
2254
|
+
fileItem.style.right = '0';
|
|
2255
|
+
fileItem.style.top = `${fileIndex * rowHeight}px`;
|
|
2256
|
+
fileItem.style.width = '';
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const row = Math.floor(fileIndex / columnCount);
|
|
2261
|
+
const column = fileIndex % columnCount;
|
|
2262
|
+
fileItem.style.left = `${8 + column * (gridItemWidth + 8)}px`;
|
|
2263
|
+
fileItem.style.right = 'auto';
|
|
2264
|
+
fileItem.style.top = `${8 + row * rowHeight}px`;
|
|
2265
|
+
fileItem.style.width = `${gridItemWidth}px`;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
function createFileItemElement(isListView) {
|
|
2269
|
+
const fileItem = document.createElement('div');
|
|
2270
|
+
fileItem.className = 'file-item';
|
|
2271
|
+
const select = document.createElement('input');
|
|
2272
|
+
select.className = 'file-select edit-only edit-inline-block';
|
|
2273
|
+
select.type = 'checkbox';
|
|
2274
|
+
select.dataset.action = 'toggleSelect';
|
|
2275
|
+
fileItem.appendChild(select);
|
|
2276
|
+
|
|
2277
|
+
const icon = createFileIconElement();
|
|
2278
|
+
fileItem.appendChild(icon.wrapper);
|
|
2279
|
+
|
|
2280
|
+
const info = document.createElement('div');
|
|
2281
|
+
info.className = 'file-info';
|
|
2282
|
+
const name = document.createElement('div');
|
|
2283
|
+
name.className = 'file-name';
|
|
2284
|
+
info.appendChild(name);
|
|
2285
|
+
const meta = document.createElement('div');
|
|
2286
|
+
meta.className = 'file-meta';
|
|
2287
|
+
const type = document.createElement('span');
|
|
2288
|
+
meta.appendChild(type);
|
|
2289
|
+
info.appendChild(meta);
|
|
2290
|
+
fileItem.appendChild(info);
|
|
2291
|
+
|
|
2292
|
+
const refs = { select, icon, name, type };
|
|
2293
|
+
|
|
2294
|
+
if (isListView) {
|
|
2295
|
+
const mtime = createDateMeta();
|
|
2296
|
+
const birthtime = createDateMeta('birthtime');
|
|
2297
|
+
meta.append(mtime.container, birthtime.container);
|
|
2298
|
+
const more = createActionButton('more-actions-btn', 'more', '更多操作', '⋮');
|
|
2299
|
+
more.setAttribute('aria-label', '更多操作');
|
|
2300
|
+
fileItem.appendChild(more);
|
|
2301
|
+
Object.assign(refs, { mtime, birthtime });
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
fileItem._refs = refs;
|
|
2305
|
+
return fileItem;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function createFileIconElement() {
|
|
2309
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
2310
|
+
const wrapper = document.createElement('div');
|
|
2311
|
+
wrapper.className = 'file-icon';
|
|
2312
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
2313
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
2314
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
2315
|
+
const use = document.createElementNS(namespace, 'use');
|
|
2316
|
+
svg.appendChild(use);
|
|
2317
|
+
wrapper.appendChild(svg);
|
|
2318
|
+
return { wrapper, svg, use };
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
function createDateMeta(className = '') {
|
|
2322
|
+
const container = document.createElement('span');
|
|
2323
|
+
if (className) container.className = className;
|
|
2324
|
+
const full = document.createElement('span');
|
|
2325
|
+
full.className = 'date-full';
|
|
2326
|
+
const compact = document.createElement('span');
|
|
2327
|
+
compact.className = 'date-compact';
|
|
2328
|
+
container.append(full, compact);
|
|
2329
|
+
return { container, full, compact };
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
function createActionButton(className, action, title, label) {
|
|
2333
|
+
const button = document.createElement('button');
|
|
2334
|
+
button.className = className;
|
|
2335
|
+
button.dataset.action = action;
|
|
2336
|
+
button.title = title;
|
|
2337
|
+
button.textContent = label;
|
|
2338
|
+
return button;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function updateFileItem(fileItem, file) {
|
|
2342
|
+
const refs = fileItem._refs;
|
|
2343
|
+
const view = getFileView(file);
|
|
2344
|
+
const isSelected = selectedPaths.has(file.path);
|
|
2345
|
+
fileItem.className = `file-item${file.isHidden ? ' hidden-file' : ''}${isSelected ? ' selected' : ''}`;
|
|
2346
|
+
fileItem.dataset.path = file.path;
|
|
2347
|
+
fileItem.dataset.isDirectory = file.isDirectory;
|
|
2348
|
+
refs.select.dataset.path = file.path;
|
|
2349
|
+
refs.select.checked = isSelected;
|
|
2350
|
+
refs.select.title = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
2351
|
+
updateFileIcon(refs.icon, view.iconInfo);
|
|
2352
|
+
refs.name.textContent = file.name;
|
|
2353
|
+
refs.type.textContent = view.typeLabel;
|
|
2354
|
+
|
|
2355
|
+
if (!refs.mtime) return;
|
|
2356
|
+
|
|
2357
|
+
refs.mtime.full.textContent = view.mtime;
|
|
2358
|
+
refs.mtime.compact.textContent = view.compactMtime;
|
|
2359
|
+
refs.birthtime.full.textContent = view.birthtime;
|
|
2360
|
+
refs.birthtime.compact.textContent = view.compactBirthtime;
|
|
2361
|
+
refs.birthtime.container.classList.toggle('visible', showBirthtime);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function getFileView(file) {
|
|
2365
|
+
if (!file._view) {
|
|
2366
|
+
file._view = {
|
|
2367
|
+
typeLabel: file.isDirectory ? '目录' : formatSize(file.size),
|
|
2368
|
+
mtime: formatDate(file.mtime),
|
|
2369
|
+
compactMtime: formatDate(file.mtime, false),
|
|
2370
|
+
birthtime: formatDate(file.birthtime),
|
|
2371
|
+
compactBirthtime: `创建 ${formatDate(file.birthtime, false)}`,
|
|
2372
|
+
iconInfo: getFileIconInfo(file)
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
return file._view;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function updateFileIcon(icon, iconInfo) {
|
|
2379
|
+
icon.use.setAttribute('href', iconInfo.symbol);
|
|
2380
|
+
icon.svg.style.color = iconInfo.color;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function handleItemClick(path, isDirectory) {
|
|
2384
|
+
if (isDirectory) {
|
|
2385
|
+
loadDirectory(path);
|
|
2386
|
+
} else {
|
|
2387
|
+
previewFile(path);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
async function copyText(text) {
|
|
2392
|
+
try {
|
|
2393
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2394
|
+
await navigator.clipboard.writeText(text);
|
|
2395
|
+
showToast('已复制到剪贴板');
|
|
2396
|
+
} else {
|
|
2397
|
+
const textarea = document.createElement('textarea');
|
|
2398
|
+
textarea.value = text;
|
|
2399
|
+
textarea.style.position = 'fixed';
|
|
2400
|
+
textarea.style.top = '-9999px';
|
|
2401
|
+
document.body.appendChild(textarea);
|
|
2402
|
+
textarea.select();
|
|
2403
|
+
document.execCommand('copy');
|
|
2404
|
+
document.body.removeChild(textarea);
|
|
2405
|
+
showToast('已复制到剪贴板');
|
|
2406
|
+
}
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
console.error('复制失败:', error);
|
|
2409
|
+
showToast('复制失败');
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
function copyPreviewContent() {
|
|
2414
|
+
if (currentPreviewContent !== '') {
|
|
2415
|
+
copyText(currentPreviewContent);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function showToast(message) {
|
|
2420
|
+
const toast = document.createElement('div');
|
|
2421
|
+
toast.className = 'toast';
|
|
2422
|
+
toast.textContent = message;
|
|
2423
|
+
toast.style.cssText = `
|
|
2424
|
+
position: fixed;
|
|
2425
|
+
top: 20px;
|
|
2426
|
+
right: 20px;
|
|
2427
|
+
background: rgba(139, 92, 246, 0.95);
|
|
2428
|
+
color: white;
|
|
2429
|
+
padding: 12px 24px;
|
|
2430
|
+
border-radius: 8px;
|
|
2431
|
+
font-size: 14px;
|
|
2432
|
+
z-index: 10000;
|
|
2433
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
2434
|
+
animation: fadeInOut 2s ease-in-out;
|
|
2435
|
+
`;
|
|
2436
|
+
document.body.appendChild(toast);
|
|
2437
|
+
|
|
2438
|
+
const style = document.createElement('style');
|
|
2439
|
+
style.textContent = `
|
|
2440
|
+
@keyframes fadeInOut {
|
|
2441
|
+
0% { opacity: 0; transform: translateY(-20px); }
|
|
2442
|
+
15% { opacity: 1; transform: translateY(0); }
|
|
2443
|
+
85% { opacity: 1; transform: translateY(0); }
|
|
2444
|
+
100% { opacity: 0; transform: translateY(-20px); }
|
|
2445
|
+
}
|
|
2446
|
+
`;
|
|
2447
|
+
document.head.appendChild(style);
|
|
2448
|
+
|
|
2449
|
+
setTimeout(() => {
|
|
2450
|
+
if (toast.parentNode) {
|
|
2451
|
+
document.body.removeChild(toast);
|
|
2452
|
+
}
|
|
2453
|
+
if (style.parentNode) {
|
|
2454
|
+
document.head.removeChild(style);
|
|
2455
|
+
}
|
|
2456
|
+
}, 2000);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function getImageFilesList() {
|
|
2460
|
+
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
|
|
2461
|
+
return currentFiles.filter(file => {
|
|
2462
|
+
if (file.isDirectory) return false;
|
|
2463
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
2464
|
+
return imageExts.includes(ext);
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function findCurrentImageIndex(path) {
|
|
2469
|
+
return imageFiles.findIndex(file => file.path === path);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
async function navigateImage(direction) {
|
|
2473
|
+
if (imageFiles.length === 0) return;
|
|
2474
|
+
|
|
2475
|
+
let newIndex = currentImageIndex + direction;
|
|
2476
|
+
if (newIndex < 0) newIndex = imageFiles.length - 1;
|
|
2477
|
+
if (newIndex >= imageFiles.length) newIndex = 0;
|
|
2478
|
+
|
|
2479
|
+
if (imageFiles[newIndex]) {
|
|
2480
|
+
previewFile(imageFiles[newIndex].path);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
async function getFileInfo(path) {
|
|
2485
|
+
try {
|
|
2486
|
+
const file = allFiles.find(f => f.path === path);
|
|
2487
|
+
if (file) {
|
|
2488
|
+
return {
|
|
2489
|
+
size: file.size,
|
|
2490
|
+
mtime: file.mtime,
|
|
2491
|
+
birthtime: file.birthtime,
|
|
2492
|
+
name: file.name
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
} catch (e) {
|
|
2496
|
+
console.error('Error getting file info:', e);
|
|
2497
|
+
}
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB
|
|
2502
|
+
const IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
|
|
2503
|
+
const HTML_EXTS = ['html', 'htm'];
|
|
2504
|
+
const BINARY_EXTS = [
|
|
2505
|
+
'exe',
|
|
2506
|
+
'dll',
|
|
2507
|
+
'so',
|
|
2508
|
+
'dylib',
|
|
2509
|
+
'bin',
|
|
2510
|
+
'dat',
|
|
2511
|
+
'class',
|
|
2512
|
+
'pyc',
|
|
2513
|
+
'pyd',
|
|
2514
|
+
'obj',
|
|
2515
|
+
'o',
|
|
2516
|
+
'a',
|
|
2517
|
+
'lib',
|
|
2518
|
+
'la',
|
|
2519
|
+
'lo',
|
|
2520
|
+
'elf',
|
|
2521
|
+
'mach-o',
|
|
2522
|
+
'coff',
|
|
2523
|
+
'pe',
|
|
2524
|
+
'msi',
|
|
2525
|
+
'sys',
|
|
2526
|
+
'drv',
|
|
2527
|
+
'vxd',
|
|
2528
|
+
'ocx',
|
|
2529
|
+
'cpl',
|
|
2530
|
+
'scr',
|
|
2531
|
+
'com',
|
|
2532
|
+
'bat',
|
|
2533
|
+
'cmd',
|
|
2534
|
+
'ps1',
|
|
2535
|
+
'psm1',
|
|
2536
|
+
'psd1',
|
|
2537
|
+
'jar',
|
|
2538
|
+
'war',
|
|
2539
|
+
'ear',
|
|
2540
|
+
'nar',
|
|
2541
|
+
'sar',
|
|
2542
|
+
'rar',
|
|
2543
|
+
'zip',
|
|
2544
|
+
'7z',
|
|
2545
|
+
'tar',
|
|
2546
|
+
'gz',
|
|
2547
|
+
'bz2',
|
|
2548
|
+
'xz',
|
|
2549
|
+
'lz',
|
|
2550
|
+
'lzma',
|
|
2551
|
+
'z',
|
|
2552
|
+
'Z',
|
|
2553
|
+
'cab',
|
|
2554
|
+
'iso',
|
|
2555
|
+
'img',
|
|
2556
|
+
'dmg',
|
|
2557
|
+
'pkg',
|
|
2558
|
+
'deb',
|
|
2559
|
+
'rpm',
|
|
2560
|
+
'apk',
|
|
2561
|
+
'msix',
|
|
2562
|
+
'appx',
|
|
2563
|
+
'snap',
|
|
2564
|
+
'flatpak',
|
|
2565
|
+
'appimage',
|
|
2566
|
+
'swf',
|
|
2567
|
+
'flv',
|
|
2568
|
+
'pdf',
|
|
2569
|
+
'doc',
|
|
2570
|
+
'docx',
|
|
2571
|
+
'xls',
|
|
2572
|
+
'xlsx',
|
|
2573
|
+
'ppt',
|
|
2574
|
+
'pptx',
|
|
2575
|
+
'odt',
|
|
2576
|
+
'ods',
|
|
2577
|
+
'odp',
|
|
2578
|
+
'odg',
|
|
2579
|
+
'odf',
|
|
2580
|
+
'rtf',
|
|
2581
|
+
'wps',
|
|
2582
|
+
'wks',
|
|
2583
|
+
'wdb',
|
|
2584
|
+
'mdb',
|
|
2585
|
+
'accdb',
|
|
2586
|
+
'pub',
|
|
2587
|
+
'vsd',
|
|
2588
|
+
'vsdx',
|
|
2589
|
+
'mpp',
|
|
2590
|
+
'mpx',
|
|
2591
|
+
'one',
|
|
2592
|
+
'onetoc2',
|
|
2593
|
+
'msg',
|
|
2594
|
+
'eml',
|
|
2595
|
+
'pst',
|
|
2596
|
+
'ost',
|
|
2597
|
+
'ics',
|
|
2598
|
+
'vcf',
|
|
2599
|
+
'dwg',
|
|
2600
|
+
'dxf',
|
|
2601
|
+
'dgn',
|
|
2602
|
+
'stl',
|
|
2603
|
+
'3ds',
|
|
2604
|
+
'max',
|
|
2605
|
+
'ma',
|
|
2606
|
+
'mb',
|
|
2607
|
+
'blend'
|
|
2608
|
+
];
|
|
2609
|
+
let currentPreviewContentData = '';
|
|
2610
|
+
let isHtmlRenderMode = false;
|
|
2611
|
+
|
|
2612
|
+
function toggleHtmlPreview() {
|
|
2613
|
+
isHtmlRenderMode = !isHtmlRenderMode;
|
|
2614
|
+
const contentDiv = document.getElementById('htmlPreviewContent');
|
|
2615
|
+
const toggleBtn = document.getElementById('toggleHtmlBtn');
|
|
2616
|
+
|
|
2617
|
+
if (!contentDiv) return;
|
|
2618
|
+
|
|
2619
|
+
if (isHtmlRenderMode) {
|
|
2620
|
+
contentDiv.innerHTML = currentPreviewContentData;
|
|
2621
|
+
contentDiv.className = 'html-rendered';
|
|
2622
|
+
toggleBtn.textContent = '📝 查看源码';
|
|
2623
|
+
} else {
|
|
2624
|
+
contentDiv.innerHTML = `<pre class="text-preview">${escapeHtml(currentPreviewContentData)}</pre>`;
|
|
2625
|
+
contentDiv.className = 'html-source';
|
|
2626
|
+
toggleBtn.textContent = '🌐 渲染预览';
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
function renderBinaryPreview(blob, filename) {
|
|
2631
|
+
currentPreviewObjectUrl = URL.createObjectURL(blob);
|
|
2632
|
+
return `
|
|
2633
|
+
<div class="preview-container">
|
|
2634
|
+
<div class="binary-info">
|
|
2635
|
+
<strong>文件信息</strong><br>
|
|
2636
|
+
文件大小: ${formatSize(blob.size)}<br>
|
|
2637
|
+
文件类型: ${blob.type || 'Unknown'}
|
|
2638
|
+
</div>
|
|
2639
|
+
<div class="empty-state">
|
|
2640
|
+
<p>该文件类型暂不支持预览</p>
|
|
2641
|
+
<p><a href="${currentPreviewObjectUrl}" download="${escapeAttribute(filename)}">点击下载</a></p>
|
|
2642
|
+
</div>
|
|
2643
|
+
</div>
|
|
2644
|
+
`;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
async function previewFile(path) {
|
|
2648
|
+
const modal = document.getElementById('previewModal');
|
|
2649
|
+
const title = document.getElementById('previewTitle');
|
|
2650
|
+
const body = document.getElementById('previewBody');
|
|
2651
|
+
|
|
2652
|
+
title.textContent = path.split('/').pop();
|
|
2653
|
+
releasePreviewObjectUrl();
|
|
2654
|
+
body.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
|
|
2655
|
+
modal.classList.add('active');
|
|
2656
|
+
|
|
2657
|
+
currentZoom = 1;
|
|
2658
|
+
currentRotation = 0;
|
|
2659
|
+
currentPreviewContentData = '';
|
|
2660
|
+
isHtmlRenderMode = false;
|
|
2661
|
+
|
|
2662
|
+
imageFiles = getImageFilesList();
|
|
2663
|
+
currentImageIndex = findCurrentImageIndex(path);
|
|
2664
|
+
|
|
2665
|
+
try {
|
|
2666
|
+
if (isArchiveFile(path)) {
|
|
2667
|
+
await previewArchive(path);
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
const ext = path.split('.').pop().toLowerCase();
|
|
2671
|
+
|
|
2672
|
+
const fileInfo = await getFileInfo(path);
|
|
2673
|
+
const isLargeFile = fileInfo && fileInfo.size > LARGE_FILE_THRESHOLD;
|
|
2674
|
+
|
|
2675
|
+
if (IMAGE_EXTS.includes(ext)) {
|
|
2676
|
+
const hasPrev = imageFiles.length > 0 && currentImageIndex > 0;
|
|
2677
|
+
const hasNext = imageFiles.length > 0 && currentImageIndex < imageFiles.length - 1;
|
|
2678
|
+
|
|
2679
|
+
body.innerHTML = `
|
|
2680
|
+
<div class="image-preview-container">
|
|
2681
|
+
<div class="nav-container">
|
|
2682
|
+
<button class="nav-btn" onclick="navigateImage(-1)" ${!hasPrev ? 'disabled' : ''}>
|
|
2683
|
+
◀ 上一张
|
|
2684
|
+
</button>
|
|
2685
|
+
<span style="font-size: 14px; color: #666;">
|
|
2686
|
+
${imageFiles.length > 0 ? `${currentImageIndex + 1}/${imageFiles.length}` : ''}
|
|
2687
|
+
${fileInfo ? ` | ${formatSize(fileInfo.size)}` : ''}
|
|
2688
|
+
</span>
|
|
2689
|
+
<button class="nav-btn" onclick="navigateImage(1)" ${!hasNext ? 'disabled' : ''}>
|
|
2690
|
+
下一张 ▶
|
|
2691
|
+
</button>
|
|
2692
|
+
</div>
|
|
2693
|
+
<div class="image-preview-controls">
|
|
2694
|
+
<button class="zoom-btn" onclick="zoomImage(-0.1)">➖</button>
|
|
2695
|
+
<div class="zoom-level" id="zoomLevel">100%</div>
|
|
2696
|
+
<button class="zoom-btn" onclick="zoomImage(0.1)">➕</button>
|
|
2697
|
+
<button class="rotate-btn" onclick="rotateImage(-90)">↺</button>
|
|
2698
|
+
<button class="rotate-btn" onclick="rotateImage(90)">↻</button>
|
|
2699
|
+
<button class="zoom-btn" onclick="resetImage()">🔄 重置</button>
|
|
2700
|
+
</div>
|
|
2701
|
+
<img
|
|
2702
|
+
class="image-preview"
|
|
2703
|
+
id="previewImage"
|
|
2704
|
+
alt="Preview"
|
|
2705
|
+
style="transform: scale(1) rotate(0deg)"
|
|
2706
|
+
>
|
|
2707
|
+
</div>
|
|
2708
|
+
`;
|
|
2709
|
+
const imageResponse = await explorerFetch(`/__api/file?path=${encodeURIComponent(path)}`);
|
|
2710
|
+
if (!imageResponse.ok) throw new Error('图片加载失败');
|
|
2711
|
+
currentPreviewObjectUrl = URL.createObjectURL(await imageResponse.blob());
|
|
2712
|
+
document.getElementById('previewImage').src = currentPreviewObjectUrl;
|
|
2713
|
+
} else if (BINARY_EXTS.includes(ext)) {
|
|
2714
|
+
const fileSize = fileInfo ? formatSize(fileInfo.size) : '未知大小';
|
|
2715
|
+
|
|
2716
|
+
body.innerHTML = `
|
|
2717
|
+
<div class="preview-container">
|
|
2718
|
+
<div class="binary-info">
|
|
2719
|
+
<strong>二进制/特殊文件</strong><br>
|
|
2720
|
+
文件大小: ${fileSize}<br>
|
|
2721
|
+
文件类型: ${ext.toUpperCase()}
|
|
2722
|
+
</div>
|
|
2723
|
+
<div class="empty-state">
|
|
2724
|
+
<p>此文件类型无法预览内容</p>
|
|
2725
|
+
<p><button class="toolbar-action-btn" id="previewDownloadBtn">点击下载</button></p>
|
|
2726
|
+
</div>
|
|
2727
|
+
</div>
|
|
2728
|
+
`;
|
|
2729
|
+
document
|
|
2730
|
+
.getElementById('previewDownloadBtn')
|
|
2731
|
+
.addEventListener('click', () => downloadFile(path, path.split('/').pop()));
|
|
2732
|
+
} else {
|
|
2733
|
+
if (isLargeFile) {
|
|
2734
|
+
body.innerHTML = `
|
|
2735
|
+
<div class="preview-container">
|
|
2736
|
+
<div class="binary-info">
|
|
2737
|
+
<strong>大文件</strong><br>
|
|
2738
|
+
文件大小: ${formatSize(fileInfo.size)}<br>
|
|
2739
|
+
<span style="color: #ff9800;">⚠️ 文件较大,为避免性能问题,请下载后查看</span>
|
|
2740
|
+
</div>
|
|
2741
|
+
<div class="empty-state">
|
|
2742
|
+
<p>该文件超过 ${formatSize(LARGE_FILE_THRESHOLD)},不建议在线预览</p>
|
|
2743
|
+
<p><button class="toolbar-action-btn" id="previewDownloadBtn">点击下载</button></p>
|
|
2744
|
+
</div>
|
|
2745
|
+
</div>
|
|
2746
|
+
`;
|
|
2747
|
+
document
|
|
2748
|
+
.getElementById('previewDownloadBtn')
|
|
2749
|
+
.addEventListener('click', () => downloadFile(path, path.split('/').pop()));
|
|
2750
|
+
} else {
|
|
2751
|
+
const response = await explorerFetch(`/__api/file?path=${encodeURIComponent(path)}`);
|
|
2752
|
+
const contentType = response.headers.get('content-type');
|
|
2753
|
+
|
|
2754
|
+
if (contentType && contentType.includes('application/json')) {
|
|
2755
|
+
const data = await response.json();
|
|
2756
|
+
if (data.type === 'text') {
|
|
2757
|
+
currentPreviewContent = data.content;
|
|
2758
|
+
currentPreviewContentData = data.content;
|
|
2759
|
+
|
|
2760
|
+
if (HTML_EXTS.includes(ext)) {
|
|
2761
|
+
body.innerHTML = `
|
|
2762
|
+
<div class="preview-container">
|
|
2763
|
+
<div class="preview-header">
|
|
2764
|
+
<button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
|
|
2765
|
+
<button class="copy-btn" id="toggleHtmlBtn" onclick="toggleHtmlPreview()" style="margin-left: 8px;">🌐 渲染预览</button>
|
|
2766
|
+
</div>
|
|
2767
|
+
<div id="htmlPreviewContent" class="html-source">
|
|
2768
|
+
<pre class="text-preview">${escapeHtml(data.content)}</pre>
|
|
2769
|
+
</div>
|
|
2770
|
+
</div>
|
|
2771
|
+
`;
|
|
2772
|
+
} else {
|
|
2773
|
+
body.innerHTML = `
|
|
2774
|
+
<div class="preview-container">
|
|
2775
|
+
<div class="preview-header">
|
|
2776
|
+
<button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
|
|
2777
|
+
</div>
|
|
2778
|
+
<pre class="text-preview">${escapeHtml(data.content)}</pre>
|
|
2779
|
+
</div>
|
|
2780
|
+
`;
|
|
2781
|
+
}
|
|
2782
|
+
} else {
|
|
2783
|
+
const blob = await response.blob();
|
|
2784
|
+
body.innerHTML = renderBinaryPreview(blob, path.split('/').pop());
|
|
2785
|
+
}
|
|
2786
|
+
} else {
|
|
2787
|
+
const blob = await response.blob();
|
|
2788
|
+
body.innerHTML = renderBinaryPreview(blob, path.split('/').pop());
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
} catch (error) {
|
|
2793
|
+
body.innerHTML = `<div class="empty-state"><p>预览失败: ${error.message}</p></div>`;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function zoomImage(delta) {
|
|
2798
|
+
const img = document.getElementById('previewImage');
|
|
2799
|
+
if (!img) return;
|
|
2800
|
+
|
|
2801
|
+
currentZoom = Math.max(0.1, Math.min(5, currentZoom + delta));
|
|
2802
|
+
updateImageTransform();
|
|
2803
|
+
document.getElementById('zoomLevel').textContent = Math.round(currentZoom * 100) + '%';
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
function rotateImage(degrees) {
|
|
2807
|
+
const img = document.getElementById('previewImage');
|
|
2808
|
+
if (!img) return;
|
|
2809
|
+
|
|
2810
|
+
currentRotation = (currentRotation + degrees) % 360;
|
|
2811
|
+
updateImageTransform();
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function resetImage() {
|
|
2815
|
+
currentZoom = 1;
|
|
2816
|
+
currentRotation = 0;
|
|
2817
|
+
updateImageTransform();
|
|
2818
|
+
document.getElementById('zoomLevel').textContent = '100%';
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function updateImageTransform() {
|
|
2822
|
+
const img = document.getElementById('previewImage');
|
|
2823
|
+
if (!img) return;
|
|
2824
|
+
|
|
2825
|
+
img.style.transform = `scale(${currentZoom}) rotate(${currentRotation}deg)`;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function closeModal() {
|
|
2829
|
+
document.getElementById('previewModal').classList.remove('active');
|
|
2830
|
+
currentArchiveJobId = null;
|
|
2831
|
+
releasePreviewObjectUrl();
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
function releasePreviewObjectUrl() {
|
|
2835
|
+
if (currentPreviewObjectUrl) {
|
|
2836
|
+
URL.revokeObjectURL(currentPreviewObjectUrl);
|
|
2837
|
+
currentPreviewObjectUrl = null;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
function goBack() {
|
|
2842
|
+
if (currentPath !== '/') {
|
|
2843
|
+
const dirname = currentPath.split('/').slice(0, -1).join('/') || '/';
|
|
2844
|
+
loadDirectory(dirname);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function updateBackButton(parentPath) {
|
|
2849
|
+
document.getElementById('backBtn').disabled = !parentPath;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
function setView(view) {
|
|
2853
|
+
currentView = view;
|
|
2854
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
2855
|
+
const gridBtn = document.getElementById('gridViewBtn');
|
|
2856
|
+
const listBtn = document.getElementById('listViewBtn');
|
|
2857
|
+
|
|
2858
|
+
if (view === 'list') {
|
|
2859
|
+
fileGrid.classList.add('list-view');
|
|
2860
|
+
listBtn.classList.add('active');
|
|
2861
|
+
gridBtn.classList.remove('active');
|
|
2862
|
+
} else {
|
|
2863
|
+
fileGrid.classList.remove('list-view');
|
|
2864
|
+
gridBtn.classList.add('active');
|
|
2865
|
+
listBtn.classList.remove('active');
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
renderFiles(currentFiles);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
const fileIconInfoCache = new Map();
|
|
2872
|
+
|
|
2873
|
+
function getFileIconInfo(file) {
|
|
2874
|
+
if (file.isDirectory) {
|
|
2875
|
+
return { symbol: '#icon-folder', color: '#667eea' };
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
2879
|
+
const cached = fileIconInfoCache.get(ext);
|
|
2880
|
+
if (cached) return cached;
|
|
2881
|
+
const iconColors = {
|
|
2882
|
+
js: '#f7df1e',
|
|
2883
|
+
json: '#292929',
|
|
2884
|
+
html: '#e34f26',
|
|
2885
|
+
css: '#264de4',
|
|
2886
|
+
md: '#083fa1',
|
|
2887
|
+
jpg: '#ff6b6b',
|
|
2888
|
+
jpeg: '#ff6b6b',
|
|
2889
|
+
png: '#ff6b6b',
|
|
2890
|
+
gif: '#ff6b6b',
|
|
2891
|
+
svg: '#ff6b6b',
|
|
2892
|
+
webp: '#ff6b6b',
|
|
2893
|
+
bmp: '#ff6b6b',
|
|
2894
|
+
ico: '#ff6b6b',
|
|
2895
|
+
zip: '#4ecdc4',
|
|
2896
|
+
'7z': '#4ecdc4',
|
|
2897
|
+
rar: '#4ecdc4',
|
|
2898
|
+
tar: '#4ecdc4',
|
|
2899
|
+
gz: '#4ecdc4',
|
|
2900
|
+
tgz: '#4ecdc4',
|
|
2901
|
+
xz: '#4ecdc4',
|
|
2902
|
+
txt: '#95a5a6',
|
|
2903
|
+
log: '#95a5a6',
|
|
2904
|
+
py: '#3776ab',
|
|
2905
|
+
java: '#007396',
|
|
2906
|
+
go: '#00add8',
|
|
2907
|
+
rs: '#dea584',
|
|
2908
|
+
ts: '#3178c6',
|
|
2909
|
+
vue: '#42b883',
|
|
2910
|
+
react: '#61dafb',
|
|
2911
|
+
php: '#777bb4',
|
|
2912
|
+
rb: '#cc342d',
|
|
2913
|
+
c: '#555555',
|
|
2914
|
+
cpp: '#00599c',
|
|
2915
|
+
h: '#555555',
|
|
2916
|
+
hpp: '#00599c',
|
|
2917
|
+
cs: '#239120',
|
|
2918
|
+
swift: '#fa7343',
|
|
2919
|
+
kotlin: '#a97bff',
|
|
2920
|
+
dart: '#0175c2',
|
|
2921
|
+
sql: '#003b57',
|
|
2922
|
+
sh: '#4eaa25',
|
|
2923
|
+
bash: '#4eaa25',
|
|
2924
|
+
ps1: '#012456',
|
|
2925
|
+
bat: '#4caf50',
|
|
2926
|
+
cmd: '#4caf50',
|
|
2927
|
+
exe: '#ff4444',
|
|
2928
|
+
dll: '#ff4444',
|
|
2929
|
+
so: '#ff4444',
|
|
2930
|
+
dylib: '#ff4444',
|
|
2931
|
+
bin: '#ff4444',
|
|
2932
|
+
dat: '#ff4444',
|
|
2933
|
+
class: '#ea2d2e',
|
|
2934
|
+
jar: '#ea2d2e',
|
|
2935
|
+
war: '#ea2d2e',
|
|
2936
|
+
pyc: '#3776ab',
|
|
2937
|
+
pyd: '#3776ab',
|
|
2938
|
+
obj: '#555555',
|
|
2939
|
+
o: '#555555',
|
|
2940
|
+
pdf: '#f40f02',
|
|
2941
|
+
doc: '#2b579a',
|
|
2942
|
+
docx: '#2b579a',
|
|
2943
|
+
xls: '#217346',
|
|
2944
|
+
xlsx: '#217346',
|
|
2945
|
+
ppt: '#d24726',
|
|
2946
|
+
pptx: '#d24726',
|
|
2947
|
+
mp3: '#ff5722',
|
|
2948
|
+
wav: '#ff5722',
|
|
2949
|
+
flac: '#ff5722',
|
|
2950
|
+
aac: '#ff5722',
|
|
2951
|
+
mp4: '#2196f3',
|
|
2952
|
+
avi: '#2196f3',
|
|
2953
|
+
mov: '#2196f3',
|
|
2954
|
+
mkv: '#2196f3',
|
|
2955
|
+
webm: '#2196f3'
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
const iconInfo = { symbol: '#icon-file', color: iconColors[ext] || '#95a5a6' };
|
|
2959
|
+
fileIconInfoCache.set(ext, iconInfo);
|
|
2960
|
+
return iconInfo;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
function formatSize(bytes) {
|
|
2964
|
+
if (bytes === 0) return '0 B';
|
|
2965
|
+
const k = 1024;
|
|
2966
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
2967
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2968
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
function formatDate(value, includeSeconds = true) {
|
|
2972
|
+
const date = new Date(value);
|
|
2973
|
+
const year = date.getFullYear();
|
|
2974
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2975
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
2976
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
2977
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
2978
|
+
const base = `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
2979
|
+
return includeSeconds ? `${base}:${String(date.getSeconds()).padStart(2, '0')}` : base;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
function escapeHtml(text) {
|
|
2983
|
+
const div = document.createElement('div');
|
|
2984
|
+
div.textContent = text;
|
|
2985
|
+
return div.innerHTML;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
function escapeAttribute(value) {
|
|
2989
|
+
return String(value).replace(
|
|
2990
|
+
/[&<>"']/g,
|
|
2991
|
+
char =>
|
|
2992
|
+
({
|
|
2993
|
+
'&': '&',
|
|
2994
|
+
'<': '<',
|
|
2995
|
+
'>': '>',
|
|
2996
|
+
'"': '"',
|
|
2997
|
+
"'": '''
|
|
2998
|
+
})[char]
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
</script>
|
|
3002
|
+
</body>
|
|
3003
|
+
</html>
|