mock-service-cli 3.6.1 → 3.6.2
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/lib/file-explorer.html +795 -93
- package/lib/fileExplorerServer.js +53 -17
- package/package.json +1 -1
package/lib/file-explorer.html
CHANGED
|
@@ -15,41 +15,47 @@
|
|
|
15
15
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
16
16
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
17
|
min-height: 100vh;
|
|
18
|
-
padding:
|
|
18
|
+
padding: 12px;
|
|
19
|
+
margin: 0;
|
|
20
|
+
box-sizing: border-box;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
.container {
|
|
22
24
|
max-width: 1200px;
|
|
23
25
|
margin: 0 auto;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
height: calc(100vh - 24px);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
.header {
|
|
27
32
|
background: white;
|
|
28
|
-
padding:
|
|
29
|
-
border-radius:
|
|
33
|
+
padding: 12px 16px;
|
|
34
|
+
border-radius: 8px;
|
|
30
35
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
31
|
-
margin-bottom:
|
|
36
|
+
margin-bottom: 12px;
|
|
37
|
+
flex-shrink: 0;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
.header h1 {
|
|
35
41
|
color: #333;
|
|
36
|
-
font-size:
|
|
37
|
-
margin-bottom:
|
|
42
|
+
font-size: 20px;
|
|
43
|
+
margin-bottom: 4px;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
.breadcrumb {
|
|
41
47
|
display: flex;
|
|
42
48
|
align-items: center;
|
|
43
|
-
gap:
|
|
49
|
+
gap: 6px;
|
|
44
50
|
flex-wrap: wrap;
|
|
45
|
-
margin-top:
|
|
51
|
+
margin-top: 8px;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
.breadcrumb-item {
|
|
49
|
-
padding:
|
|
55
|
+
padding: 4px 8px;
|
|
50
56
|
background: #f0f0f0;
|
|
51
|
-
border-radius:
|
|
52
|
-
font-size:
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
font-size: 12px;
|
|
53
59
|
color: #666;
|
|
54
60
|
cursor: pointer;
|
|
55
61
|
transition: background 0.2s;
|
|
@@ -71,31 +77,43 @@
|
|
|
71
77
|
|
|
72
78
|
.file-container {
|
|
73
79
|
background: white;
|
|
74
|
-
border-radius:
|
|
80
|
+
border-radius: 8px;
|
|
75
81
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
76
82
|
overflow: hidden;
|
|
83
|
+
flex: 1;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
.toolbar {
|
|
80
|
-
padding:
|
|
89
|
+
padding: 10px 12px;
|
|
81
90
|
border-bottom: 1px solid #eee;
|
|
82
91
|
display: flex;
|
|
83
|
-
gap:
|
|
92
|
+
gap: 8px;
|
|
84
93
|
align-items: center;
|
|
85
94
|
flex-wrap: wrap;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stats-info {
|
|
99
|
+
font-size: 12px;
|
|
100
|
+
color: #666;
|
|
101
|
+
padding: 4px 8px;
|
|
102
|
+
background: #f8f8f8;
|
|
103
|
+
border-radius: 4px;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
.search-box {
|
|
89
107
|
flex: 1;
|
|
90
|
-
min-width:
|
|
108
|
+
min-width: 150px;
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
.search-box input {
|
|
94
112
|
width: 100%;
|
|
95
|
-
padding:
|
|
113
|
+
padding: 6px 12px;
|
|
96
114
|
border: 1px solid #ddd;
|
|
97
|
-
border-radius:
|
|
98
|
-
font-size:
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
font-size: 12px;
|
|
99
117
|
outline: none;
|
|
100
118
|
transition: border-color 0.2s;
|
|
101
119
|
}
|
|
@@ -106,15 +124,15 @@
|
|
|
106
124
|
|
|
107
125
|
.sort-controls {
|
|
108
126
|
display: flex;
|
|
109
|
-
gap:
|
|
127
|
+
gap: 6px;
|
|
110
128
|
align-items: center;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
.sort-select {
|
|
114
|
-
padding:
|
|
132
|
+
padding: 6px 10px;
|
|
115
133
|
border: 1px solid #ddd;
|
|
116
|
-
border-radius:
|
|
117
|
-
font-size:
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
font-size: 12px;
|
|
118
136
|
cursor: pointer;
|
|
119
137
|
outline: none;
|
|
120
138
|
transition: border-color 0.2s;
|
|
@@ -125,11 +143,11 @@
|
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
.sort-order-btn {
|
|
128
|
-
padding:
|
|
146
|
+
padding: 6px 10px;
|
|
129
147
|
background: #f0f0f0;
|
|
130
148
|
border: none;
|
|
131
|
-
border-radius:
|
|
132
|
-
font-size:
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
font-size: 12px;
|
|
133
151
|
cursor: pointer;
|
|
134
152
|
transition: background 0.2s;
|
|
135
153
|
}
|
|
@@ -139,15 +157,15 @@
|
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
.back-btn {
|
|
142
|
-
padding:
|
|
160
|
+
padding: 6px 12px;
|
|
143
161
|
background: #f0f0f0;
|
|
144
162
|
border: none;
|
|
145
|
-
border-radius:
|
|
146
|
-
font-size:
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
font-size: 12px;
|
|
147
165
|
cursor: pointer;
|
|
148
166
|
display: flex;
|
|
149
167
|
align-items: center;
|
|
150
|
-
gap:
|
|
168
|
+
gap: 4px;
|
|
151
169
|
transition: background 0.2s;
|
|
152
170
|
}
|
|
153
171
|
|
|
@@ -163,31 +181,31 @@
|
|
|
163
181
|
.view-toggle {
|
|
164
182
|
display: flex;
|
|
165
183
|
background: #f0f0f0;
|
|
166
|
-
border-radius:
|
|
167
|
-
padding:
|
|
184
|
+
border-radius: 4px;
|
|
185
|
+
padding: 3px;
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
.view-btn {
|
|
171
|
-
padding:
|
|
189
|
+
padding: 4px 10px;
|
|
172
190
|
background: transparent;
|
|
173
191
|
border: none;
|
|
174
|
-
border-radius:
|
|
192
|
+
border-radius: 3px;
|
|
175
193
|
cursor: pointer;
|
|
176
|
-
font-size:
|
|
194
|
+
font-size: 12px;
|
|
177
195
|
transition: all 0.2s;
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
.view-btn.active {
|
|
181
199
|
background: white;
|
|
182
|
-
box-shadow: 0
|
|
200
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
183
201
|
}
|
|
184
202
|
|
|
185
203
|
.refresh-btn {
|
|
186
|
-
padding:
|
|
204
|
+
padding: 6px 10px;
|
|
187
205
|
background: #f0f0f0;
|
|
188
206
|
border: none;
|
|
189
|
-
border-radius:
|
|
190
|
-
font-size:
|
|
207
|
+
border-radius: 4px;
|
|
208
|
+
font-size: 14px;
|
|
191
209
|
cursor: pointer;
|
|
192
210
|
transition: background 0.2s;
|
|
193
211
|
}
|
|
@@ -197,10 +215,10 @@
|
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
.toggle-hidden-btn {
|
|
200
|
-
padding:
|
|
218
|
+
padding: 6px 10px;
|
|
201
219
|
background: #f0f0f0;
|
|
202
220
|
border: none;
|
|
203
|
-
border-radius:
|
|
221
|
+
border-radius: 4px;
|
|
204
222
|
font-size: 14px;
|
|
205
223
|
cursor: pointer;
|
|
206
224
|
transition: background 0.2s;
|
|
@@ -215,6 +233,47 @@
|
|
|
215
233
|
color: white;
|
|
216
234
|
}
|
|
217
235
|
|
|
236
|
+
.context-menu {
|
|
237
|
+
position: fixed;
|
|
238
|
+
background: white;
|
|
239
|
+
border-radius: 8px;
|
|
240
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
241
|
+
padding: 8px 0;
|
|
242
|
+
z-index: 10000;
|
|
243
|
+
min-width: 160px;
|
|
244
|
+
display: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.context-menu.active {
|
|
248
|
+
display: block;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.context-menu-item {
|
|
252
|
+
padding: 10px 16px;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
font-size: 14px;
|
|
255
|
+
color: #333;
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 8px;
|
|
259
|
+
transition: background 0.2s;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.context-menu-item:hover {
|
|
263
|
+
background: #f0f0f0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.context-menu-item.disabled {
|
|
267
|
+
opacity: 0.5;
|
|
268
|
+
cursor: not-allowed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.context-menu-divider {
|
|
272
|
+
height: 1px;
|
|
273
|
+
background: #eee;
|
|
274
|
+
margin: 4px 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
218
277
|
.copy-btn {
|
|
219
278
|
padding: 4px 8px;
|
|
220
279
|
background: #f0f0f0;
|
|
@@ -231,23 +290,26 @@
|
|
|
231
290
|
}
|
|
232
291
|
|
|
233
292
|
.file-grid {
|
|
234
|
-
padding:
|
|
293
|
+
padding: 8px;
|
|
235
294
|
display: grid;
|
|
236
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
237
|
-
gap:
|
|
295
|
+
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
296
|
+
gap: 8px;
|
|
297
|
+
overflow: auto;
|
|
298
|
+
flex: 1;
|
|
238
299
|
}
|
|
239
300
|
|
|
240
301
|
.file-grid.list-view {
|
|
241
302
|
grid-template-columns: 1fr;
|
|
242
303
|
gap: 0;
|
|
304
|
+
padding: 4px 0;
|
|
243
305
|
}
|
|
244
306
|
|
|
245
307
|
.file-item {
|
|
246
308
|
display: flex;
|
|
247
309
|
flex-direction: column;
|
|
248
310
|
align-items: center;
|
|
249
|
-
padding:
|
|
250
|
-
border-radius:
|
|
311
|
+
padding: 10px 8px;
|
|
312
|
+
border-radius: 6px;
|
|
251
313
|
cursor: pointer;
|
|
252
314
|
transition: all 0.2s;
|
|
253
315
|
}
|
|
@@ -258,25 +320,26 @@
|
|
|
258
320
|
|
|
259
321
|
.list-view .file-item {
|
|
260
322
|
flex-direction: row;
|
|
261
|
-
padding: 12px
|
|
323
|
+
padding: 6px 12px;
|
|
262
324
|
border-bottom: 1px solid #f0f0f0;
|
|
263
325
|
border-radius: 0;
|
|
326
|
+
min-height: auto;
|
|
264
327
|
}
|
|
265
328
|
|
|
266
329
|
.file-icon {
|
|
267
|
-
width:
|
|
268
|
-
height:
|
|
269
|
-
margin-bottom:
|
|
330
|
+
width: 40px;
|
|
331
|
+
height: 40px;
|
|
332
|
+
margin-bottom: 6px;
|
|
270
333
|
display: flex;
|
|
271
334
|
align-items: center;
|
|
272
335
|
justify-content: center;
|
|
273
336
|
}
|
|
274
337
|
|
|
275
338
|
.list-view .file-icon {
|
|
276
|
-
width:
|
|
277
|
-
height:
|
|
339
|
+
width: 20px;
|
|
340
|
+
height: 20px;
|
|
278
341
|
margin-bottom: 0;
|
|
279
|
-
margin-right:
|
|
342
|
+
margin-right: 8px;
|
|
280
343
|
}
|
|
281
344
|
|
|
282
345
|
.file-icon svg {
|
|
@@ -292,31 +355,34 @@
|
|
|
292
355
|
.list-view .file-info {
|
|
293
356
|
display: flex;
|
|
294
357
|
align-items: center;
|
|
295
|
-
gap:
|
|
358
|
+
gap: 10px;
|
|
296
359
|
text-align: left;
|
|
297
360
|
flex: 1;
|
|
298
361
|
}
|
|
299
362
|
|
|
300
363
|
.file-name {
|
|
301
|
-
font-size:
|
|
364
|
+
font-size: 12px;
|
|
302
365
|
color: #333;
|
|
303
366
|
font-weight: 500;
|
|
304
|
-
margin-bottom:
|
|
367
|
+
margin-bottom: 2px;
|
|
368
|
+
line-height: 1.3;
|
|
305
369
|
}
|
|
306
370
|
|
|
307
371
|
.list-view .file-name {
|
|
308
372
|
margin-bottom: 0;
|
|
309
373
|
flex: 1;
|
|
374
|
+
font-size: 12px;
|
|
310
375
|
}
|
|
311
376
|
|
|
312
377
|
.file-meta {
|
|
313
|
-
font-size:
|
|
378
|
+
font-size: 10px;
|
|
314
379
|
color: #999;
|
|
315
380
|
}
|
|
316
381
|
|
|
317
382
|
.list-view .file-meta {
|
|
318
383
|
display: flex;
|
|
319
|
-
gap:
|
|
384
|
+
gap: 10px;
|
|
385
|
+
font-size: 10px;
|
|
320
386
|
}
|
|
321
387
|
|
|
322
388
|
.hidden-file {
|
|
@@ -378,9 +444,10 @@
|
|
|
378
444
|
.modal-content {
|
|
379
445
|
background: white;
|
|
380
446
|
border-radius: 12px;
|
|
381
|
-
max-width:
|
|
447
|
+
max-width: 95vw;
|
|
448
|
+
max-height: 95vh;
|
|
382
449
|
width: 100%;
|
|
383
|
-
|
|
450
|
+
height: 100%;
|
|
384
451
|
overflow: hidden;
|
|
385
452
|
display: flex;
|
|
386
453
|
flex-direction: column;
|
|
@@ -431,6 +498,130 @@
|
|
|
431
498
|
max-height: 70vh;
|
|
432
499
|
display: block;
|
|
433
500
|
margin: 0 auto;
|
|
501
|
+
transition: transform 0.3s ease;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.image-preview-container {
|
|
505
|
+
position: relative;
|
|
506
|
+
display: flex;
|
|
507
|
+
flex-direction: column;
|
|
508
|
+
align-items: center;
|
|
509
|
+
justify-content: flex-start;
|
|
510
|
+
height: 100%;
|
|
511
|
+
overflow: auto;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.image-preview-controls {
|
|
515
|
+
display: flex;
|
|
516
|
+
gap: 12px;
|
|
517
|
+
margin-bottom: 16px;
|
|
518
|
+
align-items: center;
|
|
519
|
+
position: sticky;
|
|
520
|
+
top: 0;
|
|
521
|
+
background: white;
|
|
522
|
+
padding: 12px;
|
|
523
|
+
border-radius: 8px;
|
|
524
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
525
|
+
z-index: 10;
|
|
526
|
+
flex-wrap: wrap;
|
|
527
|
+
justify-content: center;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.nav-btn {
|
|
531
|
+
padding: 12px 20px;
|
|
532
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
533
|
+
color: white;
|
|
534
|
+
border: none;
|
|
535
|
+
border-radius: 8px;
|
|
536
|
+
font-size: 16px;
|
|
537
|
+
cursor: pointer;
|
|
538
|
+
transition: all 0.2s;
|
|
539
|
+
display: flex;
|
|
540
|
+
align-items: center;
|
|
541
|
+
gap: 8px;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.nav-btn:hover:not(:disabled) {
|
|
545
|
+
transform: translateY(-2px);
|
|
546
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.nav-btn:disabled {
|
|
550
|
+
opacity: 0.5;
|
|
551
|
+
cursor: not-allowed;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.nav-container {
|
|
555
|
+
display: flex;
|
|
556
|
+
justify-content: space-between;
|
|
557
|
+
align-items: center;
|
|
558
|
+
width: 100%;
|
|
559
|
+
margin-bottom: 16px;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.zoom-btn {
|
|
563
|
+
padding: 8px 16px;
|
|
564
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
565
|
+
color: white;
|
|
566
|
+
border: none;
|
|
567
|
+
border-radius: 6px;
|
|
568
|
+
font-size: 14px;
|
|
569
|
+
cursor: pointer;
|
|
570
|
+
transition: all 0.2s;
|
|
571
|
+
display: flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
gap: 6px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.zoom-btn:hover {
|
|
577
|
+
transform: translateY(-1px);
|
|
578
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.zoom-level {
|
|
582
|
+
font-size: 14px;
|
|
583
|
+
color: #666;
|
|
584
|
+
font-weight: 500;
|
|
585
|
+
min-width: 60px;
|
|
586
|
+
text-align: center;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.rotate-btn {
|
|
590
|
+
padding: 8px 16px;
|
|
591
|
+
background: #f0f0f0;
|
|
592
|
+
color: #333;
|
|
593
|
+
border: none;
|
|
594
|
+
border-radius: 6px;
|
|
595
|
+
font-size: 14px;
|
|
596
|
+
cursor: pointer;
|
|
597
|
+
transition: all 0.2s;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.rotate-btn:hover {
|
|
601
|
+
background: #e0e0e0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.binary-preview {
|
|
605
|
+
background: #1e1e1e;
|
|
606
|
+
color: #d4d4d4;
|
|
607
|
+
padding: 20px;
|
|
608
|
+
border-radius: 8px;
|
|
609
|
+
max-height: 70vh;
|
|
610
|
+
overflow: auto;
|
|
611
|
+
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
|
|
612
|
+
font-size: 12px;
|
|
613
|
+
line-height: 1.5;
|
|
614
|
+
white-space: pre-wrap;
|
|
615
|
+
word-wrap: break-word;
|
|
616
|
+
margin: 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.binary-info {
|
|
620
|
+
margin-bottom: 12px;
|
|
621
|
+
padding: 12px;
|
|
622
|
+
background: #2d2d2d;
|
|
623
|
+
border-radius: 6px;
|
|
624
|
+
color: #9cdcfe;
|
|
434
625
|
}
|
|
435
626
|
|
|
436
627
|
.text-preview {
|
|
@@ -448,6 +639,19 @@
|
|
|
448
639
|
margin: 0;
|
|
449
640
|
}
|
|
450
641
|
|
|
642
|
+
.html-rendered {
|
|
643
|
+
background: white;
|
|
644
|
+
padding: 20px;
|
|
645
|
+
border-radius: 8px;
|
|
646
|
+
max-height: 70vh;
|
|
647
|
+
overflow: auto;
|
|
648
|
+
border: 1px solid #eee;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.html-source .text-preview {
|
|
652
|
+
margin: 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
451
655
|
.preview-container {
|
|
452
656
|
display: flex;
|
|
453
657
|
flex-direction: column;
|
|
@@ -532,7 +736,7 @@
|
|
|
532
736
|
<div class="file-container">
|
|
533
737
|
<div class="toolbar">
|
|
534
738
|
<button class="back-btn" id="backBtn" onclick="goBack()">
|
|
535
|
-
<svg width="
|
|
739
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
536
740
|
<polyline points="15 18 9 12 15 6"></polyline>
|
|
537
741
|
</svg>
|
|
538
742
|
返回上级
|
|
@@ -546,6 +750,7 @@
|
|
|
546
750
|
>
|
|
547
751
|
👁️
|
|
548
752
|
</button>
|
|
753
|
+
<div class="stats-info" id="statsInfo"></div>
|
|
549
754
|
<div class="search-box">
|
|
550
755
|
<input type="text" id="searchInput" placeholder="搜索文件名..." oninput="handleSearch()" />
|
|
551
756
|
</div>
|
|
@@ -578,6 +783,16 @@
|
|
|
578
783
|
</div>
|
|
579
784
|
</div>
|
|
580
785
|
|
|
786
|
+
<div class="context-menu" id="contextMenu">
|
|
787
|
+
<div class="context-menu-item" id="ctxOpen" onclick="handleContextMenuAction('open')">📂 打开</div>
|
|
788
|
+
<div class="context-menu-item" id="ctxCopyPath" onclick="handleContextMenuAction('copyPath')">📋 复制路径</div>
|
|
789
|
+
<div class="context-menu-divider"></div>
|
|
790
|
+
<div class="context-menu-item" id="ctxOpenInExplorer" onclick="handleContextMenuAction('openInExplorer')">
|
|
791
|
+
🖥️ 在文件管理器中打开
|
|
792
|
+
</div>
|
|
793
|
+
<div class="context-menu-item" id="ctxDownload" onclick="handleContextMenuAction('download')">⬇️ 下载</div>
|
|
794
|
+
</div>
|
|
795
|
+
|
|
581
796
|
<script>
|
|
582
797
|
let currentPath = '/';
|
|
583
798
|
let currentView = 'grid';
|
|
@@ -588,17 +803,98 @@
|
|
|
588
803
|
let sortAscending = true;
|
|
589
804
|
let showHiddenFiles = true;
|
|
590
805
|
let currentPreviewContent = '';
|
|
806
|
+
let currentZoom = 1;
|
|
807
|
+
let currentRotation = 0;
|
|
808
|
+
let currentContextMenuFile = null;
|
|
809
|
+
let currentImageIndex = -1;
|
|
810
|
+
let imageFiles = [];
|
|
591
811
|
|
|
592
812
|
document.addEventListener('DOMContentLoaded', () => {
|
|
593
813
|
loadDirectory('/');
|
|
814
|
+
|
|
815
|
+
const fileGrid = document.getElementById('fileGrid');
|
|
816
|
+
fileGrid.addEventListener('contextmenu', e => {
|
|
817
|
+
const fileItem = e.target.closest('.file-item');
|
|
818
|
+
if (fileItem) {
|
|
819
|
+
e.preventDefault();
|
|
820
|
+
const filePath = fileItem.dataset.path;
|
|
821
|
+
const isDirectory = fileItem.dataset.isDirectory === 'true';
|
|
822
|
+
showContextMenu(e.clientX, e.clientY, filePath, isDirectory);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
document.addEventListener('click', () => {
|
|
827
|
+
hideContextMenu();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
document.addEventListener('scroll', () => {
|
|
831
|
+
hideContextMenu();
|
|
832
|
+
});
|
|
594
833
|
});
|
|
595
834
|
|
|
835
|
+
function showContextMenu(x, y, filePath, isDirectory) {
|
|
836
|
+
currentContextMenuFile = { path: filePath, isDirectory };
|
|
837
|
+
|
|
838
|
+
const menu = document.getElementById('contextMenu');
|
|
839
|
+
const ctxDownload = document.getElementById('ctxDownload');
|
|
840
|
+
|
|
841
|
+
if (isDirectory) {
|
|
842
|
+
ctxDownload.classList.add('disabled');
|
|
843
|
+
} else {
|
|
844
|
+
ctxDownload.classList.remove('disabled');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
menu.style.left = `${x}px`;
|
|
848
|
+
menu.style.top = `${y}px`;
|
|
849
|
+
menu.classList.add('active');
|
|
850
|
+
|
|
851
|
+
const rect = menu.getBoundingClientRect();
|
|
852
|
+
if (rect.right > window.innerWidth) {
|
|
853
|
+
menu.style.left = `${x - rect.width}px`;
|
|
854
|
+
}
|
|
855
|
+
if (rect.bottom > window.innerHeight) {
|
|
856
|
+
menu.style.top = `${y - rect.height}px`;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function hideContextMenu() {
|
|
861
|
+
const menu = document.getElementById('contextMenu');
|
|
862
|
+
menu.classList.remove('active');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function handleContextMenuAction(action) {
|
|
866
|
+
if (!currentContextMenuFile) return;
|
|
867
|
+
|
|
868
|
+
const { path, isDirectory } = currentContextMenuFile;
|
|
869
|
+
|
|
870
|
+
switch (action) {
|
|
871
|
+
case 'open':
|
|
872
|
+
handleItemClick(path, isDirectory);
|
|
873
|
+
break;
|
|
874
|
+
case 'copyPath':
|
|
875
|
+
copyToClipboard(path);
|
|
876
|
+
break;
|
|
877
|
+
case 'openInExplorer':
|
|
878
|
+
openInExplorer(path);
|
|
879
|
+
break;
|
|
880
|
+
case 'download':
|
|
881
|
+
if (!isDirectory) {
|
|
882
|
+
downloadFile(path, path.split('/').pop());
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
hideContextMenu();
|
|
888
|
+
}
|
|
889
|
+
|
|
596
890
|
async function loadDirectory(path) {
|
|
597
891
|
// console.log('Loading directory:', path);
|
|
598
892
|
const fileGrid = document.getElementById('fileGrid');
|
|
599
893
|
|
|
894
|
+
// 保存旧路径,以便失败时可以回退
|
|
895
|
+
const oldCurrentPath = currentPath;
|
|
896
|
+
|
|
600
897
|
// 重置所有状态
|
|
601
|
-
currentPath = '/';
|
|
602
898
|
allFiles = [];
|
|
603
899
|
currentFiles = [];
|
|
604
900
|
|
|
@@ -637,16 +933,31 @@
|
|
|
637
933
|
searchQuery = '';
|
|
638
934
|
} catch (error) {
|
|
639
935
|
console.error('Error loading directory:', error);
|
|
936
|
+
// 回退到旧路径
|
|
937
|
+
currentPath = oldCurrentPath;
|
|
938
|
+
// 计算旧路径的父路径
|
|
939
|
+
const parentPath = currentPath === '/' ? null : currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
|
940
|
+
updateBackButton(parentPath);
|
|
640
941
|
fileGrid.innerHTML = `<div class="empty-state"><p>错误: ${error.message}</p></div>`;
|
|
641
942
|
}
|
|
642
943
|
}
|
|
643
944
|
|
|
945
|
+
function updateStatsInfo(files) {
|
|
946
|
+
const statsInfo = document.getElementById('statsInfo');
|
|
947
|
+
if (!statsInfo) return;
|
|
948
|
+
|
|
949
|
+
const dirCount = files.filter(f => f.isDirectory).length;
|
|
950
|
+
const fileCount = files.length - dirCount;
|
|
951
|
+
statsInfo.textContent = `${dirCount} 个文件夹, ${fileCount} 个文件`;
|
|
952
|
+
}
|
|
953
|
+
|
|
644
954
|
function processAndRenderFiles() {
|
|
645
955
|
// console.log('processAndRenderFiles called, allFiles:', allFiles);
|
|
646
956
|
|
|
647
957
|
if (!allFiles) {
|
|
648
958
|
console.error('allFiles is null or undefined');
|
|
649
959
|
renderFiles([]);
|
|
960
|
+
updateStatsInfo([]);
|
|
650
961
|
return;
|
|
651
962
|
}
|
|
652
963
|
|
|
@@ -689,6 +1000,7 @@
|
|
|
689
1000
|
|
|
690
1001
|
currentFiles = files;
|
|
691
1002
|
// console.log('Rendering', files.length, 'files');
|
|
1003
|
+
updateStatsInfo(files);
|
|
692
1004
|
renderFiles(files);
|
|
693
1005
|
}
|
|
694
1006
|
|
|
@@ -809,7 +1121,9 @@
|
|
|
809
1121
|
|
|
810
1122
|
if (currentView === 'list') {
|
|
811
1123
|
html += `
|
|
812
|
-
<div class="file-item ${hiddenClass}"
|
|
1124
|
+
<div class="file-item ${hiddenClass}" data-path="${file.path}" data-is-directory="${
|
|
1125
|
+
file.isDirectory
|
|
1126
|
+
}">
|
|
813
1127
|
<div class="file-icon" onclick="handleItemClick('${file.path}', ${
|
|
814
1128
|
file.isDirectory
|
|
815
1129
|
})">${icon}</div>
|
|
@@ -839,9 +1153,9 @@
|
|
|
839
1153
|
`;
|
|
840
1154
|
} else {
|
|
841
1155
|
html += `
|
|
842
|
-
<div class="file-item ${hiddenClass}"
|
|
1156
|
+
<div class="file-item ${hiddenClass}" data-path="${file.path}" data-is-directory="${
|
|
843
1157
|
file.isDirectory
|
|
844
|
-
})">
|
|
1158
|
+
}" onclick="handleItemClick('${file.path}', ${file.isDirectory})">
|
|
845
1159
|
<div class="file-icon">${icon}</div>
|
|
846
1160
|
<div class="file-info">
|
|
847
1161
|
<div class="file-name">${escapeHtml(file.name)}</div>
|
|
@@ -934,6 +1248,193 @@
|
|
|
934
1248
|
}, 2000);
|
|
935
1249
|
}
|
|
936
1250
|
|
|
1251
|
+
function getImageFilesList() {
|
|
1252
|
+
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
|
|
1253
|
+
return currentFiles.filter(file => {
|
|
1254
|
+
if (file.isDirectory) return false;
|
|
1255
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
1256
|
+
return imageExts.includes(ext);
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function findCurrentImageIndex(path) {
|
|
1261
|
+
return imageFiles.findIndex(file => file.path === path);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async function navigateImage(direction) {
|
|
1265
|
+
if (imageFiles.length === 0) return;
|
|
1266
|
+
|
|
1267
|
+
let newIndex = currentImageIndex + direction;
|
|
1268
|
+
if (newIndex < 0) newIndex = imageFiles.length - 1;
|
|
1269
|
+
if (newIndex >= imageFiles.length) newIndex = 0;
|
|
1270
|
+
|
|
1271
|
+
if (imageFiles[newIndex]) {
|
|
1272
|
+
previewFile(imageFiles[newIndex].path);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function getFileInfo(path) {
|
|
1277
|
+
try {
|
|
1278
|
+
const file = allFiles.find(f => f.path === path);
|
|
1279
|
+
if (file) {
|
|
1280
|
+
return {
|
|
1281
|
+
size: file.size,
|
|
1282
|
+
mtime: file.mtime,
|
|
1283
|
+
name: file.name
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
console.error('Error getting file info:', e);
|
|
1288
|
+
}
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB
|
|
1293
|
+
const IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
|
|
1294
|
+
const HTML_EXTS = ['html', 'htm'];
|
|
1295
|
+
const BINARY_EXTS = [
|
|
1296
|
+
'exe',
|
|
1297
|
+
'dll',
|
|
1298
|
+
'so',
|
|
1299
|
+
'dylib',
|
|
1300
|
+
'bin',
|
|
1301
|
+
'dat',
|
|
1302
|
+
'class',
|
|
1303
|
+
'pyc',
|
|
1304
|
+
'pyd',
|
|
1305
|
+
'obj',
|
|
1306
|
+
'o',
|
|
1307
|
+
'a',
|
|
1308
|
+
'lib',
|
|
1309
|
+
'la',
|
|
1310
|
+
'lo',
|
|
1311
|
+
'elf',
|
|
1312
|
+
'mach-o',
|
|
1313
|
+
'coff',
|
|
1314
|
+
'pe',
|
|
1315
|
+
'msi',
|
|
1316
|
+
'sys',
|
|
1317
|
+
'drv',
|
|
1318
|
+
'vxd',
|
|
1319
|
+
'ocx',
|
|
1320
|
+
'cpl',
|
|
1321
|
+
'scr',
|
|
1322
|
+
'com',
|
|
1323
|
+
'bat',
|
|
1324
|
+
'cmd',
|
|
1325
|
+
'ps1',
|
|
1326
|
+
'psm1',
|
|
1327
|
+
'psd1',
|
|
1328
|
+
'jar',
|
|
1329
|
+
'war',
|
|
1330
|
+
'ear',
|
|
1331
|
+
'nar',
|
|
1332
|
+
'sar',
|
|
1333
|
+
'rar',
|
|
1334
|
+
'zip',
|
|
1335
|
+
'7z',
|
|
1336
|
+
'tar',
|
|
1337
|
+
'gz',
|
|
1338
|
+
'bz2',
|
|
1339
|
+
'xz',
|
|
1340
|
+
'lz',
|
|
1341
|
+
'lzma',
|
|
1342
|
+
'z',
|
|
1343
|
+
'Z',
|
|
1344
|
+
'cab',
|
|
1345
|
+
'iso',
|
|
1346
|
+
'img',
|
|
1347
|
+
'dmg',
|
|
1348
|
+
'pkg',
|
|
1349
|
+
'deb',
|
|
1350
|
+
'rpm',
|
|
1351
|
+
'apk',
|
|
1352
|
+
'msix',
|
|
1353
|
+
'appx',
|
|
1354
|
+
'snap',
|
|
1355
|
+
'flatpak',
|
|
1356
|
+
'appimage',
|
|
1357
|
+
'swf',
|
|
1358
|
+
'flv',
|
|
1359
|
+
'pdf',
|
|
1360
|
+
'doc',
|
|
1361
|
+
'docx',
|
|
1362
|
+
'xls',
|
|
1363
|
+
'xlsx',
|
|
1364
|
+
'ppt',
|
|
1365
|
+
'pptx',
|
|
1366
|
+
'odt',
|
|
1367
|
+
'ods',
|
|
1368
|
+
'odp',
|
|
1369
|
+
'odg',
|
|
1370
|
+
'odf',
|
|
1371
|
+
'rtf',
|
|
1372
|
+
'wps',
|
|
1373
|
+
'wks',
|
|
1374
|
+
'wdb',
|
|
1375
|
+
'mdb',
|
|
1376
|
+
'accdb',
|
|
1377
|
+
'pub',
|
|
1378
|
+
'vsd',
|
|
1379
|
+
'vsdx',
|
|
1380
|
+
'mpp',
|
|
1381
|
+
'mpx',
|
|
1382
|
+
'one',
|
|
1383
|
+
'onetoc2',
|
|
1384
|
+
'msg',
|
|
1385
|
+
'eml',
|
|
1386
|
+
'pst',
|
|
1387
|
+
'ost',
|
|
1388
|
+
'ics',
|
|
1389
|
+
'vcf',
|
|
1390
|
+
'dwg',
|
|
1391
|
+
'dxf',
|
|
1392
|
+
'dgn',
|
|
1393
|
+
'stl',
|
|
1394
|
+
'3ds',
|
|
1395
|
+
'max',
|
|
1396
|
+
'ma',
|
|
1397
|
+
'mb',
|
|
1398
|
+
'blend'
|
|
1399
|
+
];
|
|
1400
|
+
let currentPreviewContentData = '';
|
|
1401
|
+
let isHtmlRenderMode = false;
|
|
1402
|
+
|
|
1403
|
+
function toggleHtmlPreview() {
|
|
1404
|
+
isHtmlRenderMode = !isHtmlRenderMode;
|
|
1405
|
+
const contentDiv = document.getElementById('htmlPreviewContent');
|
|
1406
|
+
const toggleBtn = document.getElementById('toggleHtmlBtn');
|
|
1407
|
+
|
|
1408
|
+
if (!contentDiv) return;
|
|
1409
|
+
|
|
1410
|
+
if (isHtmlRenderMode) {
|
|
1411
|
+
contentDiv.innerHTML = currentPreviewContentData;
|
|
1412
|
+
contentDiv.className = 'html-rendered';
|
|
1413
|
+
toggleBtn.textContent = '📝 查看源码';
|
|
1414
|
+
} else {
|
|
1415
|
+
contentDiv.innerHTML = `<pre class="text-preview">${escapeHtml(currentPreviewContentData)}</pre>`;
|
|
1416
|
+
contentDiv.className = 'html-source';
|
|
1417
|
+
toggleBtn.textContent = '🌐 渲染预览';
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function renderBinaryPreview(blob, filename) {
|
|
1422
|
+
const url = URL.createObjectURL(blob);
|
|
1423
|
+
return `
|
|
1424
|
+
<div class="preview-container">
|
|
1425
|
+
<div class="binary-info">
|
|
1426
|
+
<strong>文件信息</strong><br>
|
|
1427
|
+
文件大小: ${formatSize(blob.size)}<br>
|
|
1428
|
+
文件类型: ${blob.type || 'Unknown'}
|
|
1429
|
+
</div>
|
|
1430
|
+
<div class="empty-state">
|
|
1431
|
+
<p>该文件类型暂不支持预览</p>
|
|
1432
|
+
<p><a href="${url}" download="${filename}">点击下载</a></p>
|
|
1433
|
+
</div>
|
|
1434
|
+
</div>
|
|
1435
|
+
`;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
937
1438
|
async function previewFile(path) {
|
|
938
1439
|
const modal = document.getElementById('previewModal');
|
|
939
1440
|
const title = document.getElementById('previewTitle');
|
|
@@ -943,41 +1444,128 @@
|
|
|
943
1444
|
body.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
|
|
944
1445
|
modal.classList.add('active');
|
|
945
1446
|
|
|
1447
|
+
currentZoom = 1;
|
|
1448
|
+
currentRotation = 0;
|
|
1449
|
+
currentPreviewContentData = '';
|
|
1450
|
+
isHtmlRenderMode = false;
|
|
1451
|
+
|
|
1452
|
+
imageFiles = getImageFilesList();
|
|
1453
|
+
currentImageIndex = findCurrentImageIndex(path);
|
|
1454
|
+
|
|
946
1455
|
try {
|
|
947
1456
|
const ext = path.split('.').pop().toLowerCase();
|
|
948
|
-
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
|
|
949
1457
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1458
|
+
const fileInfo = await getFileInfo(path);
|
|
1459
|
+
const isLargeFile = fileInfo && fileInfo.size > LARGE_FILE_THRESHOLD;
|
|
1460
|
+
|
|
1461
|
+
if (IMAGE_EXTS.includes(ext)) {
|
|
1462
|
+
const hasPrev = imageFiles.length > 0 && currentImageIndex > 0;
|
|
1463
|
+
const hasNext = imageFiles.length > 0 && currentImageIndex < imageFiles.length - 1;
|
|
1464
|
+
|
|
1465
|
+
body.innerHTML = `
|
|
1466
|
+
<div class="image-preview-container">
|
|
1467
|
+
<div class="nav-container">
|
|
1468
|
+
<button class="nav-btn" onclick="navigateImage(-1)" ${!hasPrev ? 'disabled' : ''}>
|
|
1469
|
+
◀ 上一张
|
|
1470
|
+
</button>
|
|
1471
|
+
<span style="font-size: 14px; color: #666;">
|
|
1472
|
+
${imageFiles.length > 0 ? `${currentImageIndex + 1}/${imageFiles.length}` : ''}
|
|
1473
|
+
${fileInfo ? ` | ${formatSize(fileInfo.size)}` : ''}
|
|
1474
|
+
</span>
|
|
1475
|
+
<button class="nav-btn" onclick="navigateImage(1)" ${!hasNext ? 'disabled' : ''}>
|
|
1476
|
+
下一张 ▶
|
|
1477
|
+
</button>
|
|
1478
|
+
</div>
|
|
1479
|
+
<div class="image-preview-controls">
|
|
1480
|
+
<button class="zoom-btn" onclick="zoomImage(-0.1)">➖</button>
|
|
1481
|
+
<div class="zoom-level" id="zoomLevel">100%</div>
|
|
1482
|
+
<button class="zoom-btn" onclick="zoomImage(0.1)">➕</button>
|
|
1483
|
+
<button class="rotate-btn" onclick="rotateImage(-90)">↺</button>
|
|
1484
|
+
<button class="rotate-btn" onclick="rotateImage(90)">↻</button>
|
|
1485
|
+
<button class="zoom-btn" onclick="resetImage()">🔄 重置</button>
|
|
1486
|
+
</div>
|
|
1487
|
+
<img
|
|
1488
|
+
src="/__api/file?path=${encodeURIComponent(path)}"
|
|
1489
|
+
class="image-preview"
|
|
1490
|
+
id="previewImage"
|
|
1491
|
+
alt="Preview"
|
|
1492
|
+
style="transform: scale(1) rotate(0deg)"
|
|
1493
|
+
>
|
|
1494
|
+
</div>
|
|
1495
|
+
`;
|
|
1496
|
+
} else if (BINARY_EXTS.includes(ext)) {
|
|
1497
|
+
const downloadUrl = `/__api/file?path=${encodeURIComponent(path)}`;
|
|
1498
|
+
const fileSize = fileInfo ? formatSize(fileInfo.size) : '未知大小';
|
|
1499
|
+
|
|
1500
|
+
body.innerHTML = `
|
|
1501
|
+
<div class="preview-container">
|
|
1502
|
+
<div class="binary-info">
|
|
1503
|
+
<strong>二进制/特殊文件</strong><br>
|
|
1504
|
+
文件大小: ${fileSize}<br>
|
|
1505
|
+
文件类型: ${ext.toUpperCase()}
|
|
1506
|
+
</div>
|
|
1507
|
+
<div class="empty-state">
|
|
1508
|
+
<p>此文件类型无法预览内容</p>
|
|
1509
|
+
<p><a href="${downloadUrl}" download="${path.split('/').pop()}">点击下载</a></p>
|
|
1510
|
+
</div>
|
|
1511
|
+
</div>
|
|
1512
|
+
`;
|
|
954
1513
|
} else {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
<div class="preview-container">
|
|
964
|
-
<div class="preview-header">
|
|
965
|
-
<button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
|
|
966
|
-
</div>
|
|
967
|
-
<pre class="text-preview">${escapeHtml(data.content)}</pre>
|
|
1514
|
+
if (isLargeFile) {
|
|
1515
|
+
const downloadUrl = `/__api/file?path=${encodeURIComponent(path)}`;
|
|
1516
|
+
body.innerHTML = `
|
|
1517
|
+
<div class="preview-container">
|
|
1518
|
+
<div class="binary-info">
|
|
1519
|
+
<strong>大文件</strong><br>
|
|
1520
|
+
文件大小: ${formatSize(fileInfo.size)}<br>
|
|
1521
|
+
<span style="color: #ff9800;">⚠️ 文件较大,为避免性能问题,请下载后查看</span>
|
|
968
1522
|
</div>
|
|
969
|
-
|
|
1523
|
+
<div class="empty-state">
|
|
1524
|
+
<p>该文件超过 ${formatSize(LARGE_FILE_THRESHOLD)},不建议在线预览</p>
|
|
1525
|
+
<p><a href="${downloadUrl}" download="${path.split('/').pop()}">点击下载</a></p>
|
|
1526
|
+
</div>
|
|
1527
|
+
</div>
|
|
1528
|
+
`;
|
|
1529
|
+
} else {
|
|
1530
|
+
const response = await fetch(`/__api/file?path=${encodeURIComponent(path)}`);
|
|
1531
|
+
const contentType = response.headers.get('content-type');
|
|
1532
|
+
|
|
1533
|
+
if (contentType && contentType.includes('application/json')) {
|
|
1534
|
+
const data = await response.json();
|
|
1535
|
+
if (data.type === 'text') {
|
|
1536
|
+
currentPreviewContent = data.content;
|
|
1537
|
+
currentPreviewContentData = data.content;
|
|
1538
|
+
|
|
1539
|
+
if (HTML_EXTS.includes(ext)) {
|
|
1540
|
+
body.innerHTML = `
|
|
1541
|
+
<div class="preview-container">
|
|
1542
|
+
<div class="preview-header">
|
|
1543
|
+
<button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
|
|
1544
|
+
<button class="copy-btn" id="toggleHtmlBtn" onclick="toggleHtmlPreview()" style="margin-left: 8px;">🌐 渲染预览</button>
|
|
1545
|
+
</div>
|
|
1546
|
+
<div id="htmlPreviewContent" class="html-source">
|
|
1547
|
+
<pre class="text-preview">${escapeHtml(data.content)}</pre>
|
|
1548
|
+
</div>
|
|
1549
|
+
</div>
|
|
1550
|
+
`;
|
|
1551
|
+
} else {
|
|
1552
|
+
body.innerHTML = `
|
|
1553
|
+
<div class="preview-container">
|
|
1554
|
+
<div class="preview-header">
|
|
1555
|
+
<button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
|
|
1556
|
+
</div>
|
|
1557
|
+
<pre class="text-preview">${escapeHtml(data.content)}</pre>
|
|
1558
|
+
</div>
|
|
1559
|
+
`;
|
|
1560
|
+
}
|
|
1561
|
+
} else {
|
|
1562
|
+
const blob = await response.blob();
|
|
1563
|
+
body.innerHTML = renderBinaryPreview(blob, path.split('/').pop());
|
|
1564
|
+
}
|
|
970
1565
|
} else {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
)}" download>点击下载</a></p></div>`;
|
|
1566
|
+
const blob = await response.blob();
|
|
1567
|
+
body.innerHTML = renderBinaryPreview(blob, path.split('/').pop());
|
|
974
1568
|
}
|
|
975
|
-
} else {
|
|
976
|
-
const blob = await response.blob();
|
|
977
|
-
const url = URL.createObjectURL(blob);
|
|
978
|
-
body.innerHTML = `<div class="empty-state"><p>该文件类型暂不支持预览</p><p><a href="${url}" download="${path
|
|
979
|
-
.split('/')
|
|
980
|
-
.pop()}">点击下载</a></p></div>`;
|
|
981
1569
|
}
|
|
982
1570
|
}
|
|
983
1571
|
} catch (error) {
|
|
@@ -985,6 +1573,37 @@
|
|
|
985
1573
|
}
|
|
986
1574
|
}
|
|
987
1575
|
|
|
1576
|
+
function zoomImage(delta) {
|
|
1577
|
+
const img = document.getElementById('previewImage');
|
|
1578
|
+
if (!img) return;
|
|
1579
|
+
|
|
1580
|
+
currentZoom = Math.max(0.1, Math.min(5, currentZoom + delta));
|
|
1581
|
+
updateImageTransform();
|
|
1582
|
+
document.getElementById('zoomLevel').textContent = Math.round(currentZoom * 100) + '%';
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function rotateImage(degrees) {
|
|
1586
|
+
const img = document.getElementById('previewImage');
|
|
1587
|
+
if (!img) return;
|
|
1588
|
+
|
|
1589
|
+
currentRotation = (currentRotation + degrees) % 360;
|
|
1590
|
+
updateImageTransform();
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function resetImage() {
|
|
1594
|
+
currentZoom = 1;
|
|
1595
|
+
currentRotation = 0;
|
|
1596
|
+
updateImageTransform();
|
|
1597
|
+
document.getElementById('zoomLevel').textContent = '100%';
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function updateImageTransform() {
|
|
1601
|
+
const img = document.getElementById('previewImage');
|
|
1602
|
+
if (!img) return;
|
|
1603
|
+
|
|
1604
|
+
img.style.transform = `scale(${currentZoom}) rotate(${currentRotation}deg)`;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
988
1607
|
function closeModal() {
|
|
989
1608
|
document.getElementById('previewModal').classList.remove('active');
|
|
990
1609
|
}
|
|
@@ -1038,18 +1657,70 @@
|
|
|
1038
1657
|
png: '#ff6b6b',
|
|
1039
1658
|
gif: '#ff6b6b',
|
|
1040
1659
|
svg: '#ff6b6b',
|
|
1660
|
+
webp: '#ff6b6b',
|
|
1661
|
+
bmp: '#ff6b6b',
|
|
1662
|
+
ico: '#ff6b6b',
|
|
1041
1663
|
zip: '#4ecdc4',
|
|
1042
1664
|
'7z': '#4ecdc4',
|
|
1043
1665
|
rar: '#4ecdc4',
|
|
1044
1666
|
tar: '#4ecdc4',
|
|
1045
1667
|
gz: '#4ecdc4',
|
|
1668
|
+
tgz: '#4ecdc4',
|
|
1669
|
+
xz: '#4ecdc4',
|
|
1046
1670
|
txt: '#95a5a6',
|
|
1047
1671
|
log: '#95a5a6',
|
|
1048
1672
|
py: '#3776ab',
|
|
1049
1673
|
java: '#007396',
|
|
1050
1674
|
go: '#00add8',
|
|
1051
1675
|
rs: '#dea584',
|
|
1052
|
-
ts: '#3178c6'
|
|
1676
|
+
ts: '#3178c6',
|
|
1677
|
+
vue: '#42b883',
|
|
1678
|
+
react: '#61dafb',
|
|
1679
|
+
php: '#777bb4',
|
|
1680
|
+
rb: '#cc342d',
|
|
1681
|
+
c: '#555555',
|
|
1682
|
+
cpp: '#00599c',
|
|
1683
|
+
h: '#555555',
|
|
1684
|
+
hpp: '#00599c',
|
|
1685
|
+
cs: '#239120',
|
|
1686
|
+
swift: '#fa7343',
|
|
1687
|
+
kotlin: '#a97bff',
|
|
1688
|
+
dart: '#0175c2',
|
|
1689
|
+
sql: '#003b57',
|
|
1690
|
+
sh: '#4eaa25',
|
|
1691
|
+
bash: '#4eaa25',
|
|
1692
|
+
ps1: '#012456',
|
|
1693
|
+
bat: '#4caf50',
|
|
1694
|
+
cmd: '#4caf50',
|
|
1695
|
+
exe: '#ff4444',
|
|
1696
|
+
dll: '#ff4444',
|
|
1697
|
+
so: '#ff4444',
|
|
1698
|
+
dylib: '#ff4444',
|
|
1699
|
+
bin: '#ff4444',
|
|
1700
|
+
dat: '#ff4444',
|
|
1701
|
+
class: '#ea2d2e',
|
|
1702
|
+
jar: '#ea2d2e',
|
|
1703
|
+
war: '#ea2d2e',
|
|
1704
|
+
pyc: '#3776ab',
|
|
1705
|
+
pyd: '#3776ab',
|
|
1706
|
+
obj: '#555555',
|
|
1707
|
+
o: '#555555',
|
|
1708
|
+
pdf: '#f40f02',
|
|
1709
|
+
doc: '#2b579a',
|
|
1710
|
+
docx: '#2b579a',
|
|
1711
|
+
xls: '#217346',
|
|
1712
|
+
xlsx: '#217346',
|
|
1713
|
+
ppt: '#d24726',
|
|
1714
|
+
pptx: '#d24726',
|
|
1715
|
+
mp3: '#ff5722',
|
|
1716
|
+
wav: '#ff5722',
|
|
1717
|
+
flac: '#ff5722',
|
|
1718
|
+
aac: '#ff5722',
|
|
1719
|
+
mp4: '#2196f3',
|
|
1720
|
+
avi: '#2196f3',
|
|
1721
|
+
mov: '#2196f3',
|
|
1722
|
+
mkv: '#2196f3',
|
|
1723
|
+
webm: '#2196f3'
|
|
1053
1724
|
};
|
|
1054
1725
|
|
|
1055
1726
|
const color = iconColors[ext] || '#95a5a6';
|
|
@@ -1085,6 +1756,19 @@
|
|
|
1085
1756
|
}
|
|
1086
1757
|
});
|
|
1087
1758
|
|
|
1759
|
+
document.getElementById('previewModal').addEventListener(
|
|
1760
|
+
'wheel',
|
|
1761
|
+
e => {
|
|
1762
|
+
const img = document.getElementById('previewImage');
|
|
1763
|
+
if (!img) return;
|
|
1764
|
+
|
|
1765
|
+
e.preventDefault();
|
|
1766
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
1767
|
+
zoomImage(delta);
|
|
1768
|
+
},
|
|
1769
|
+
{ passive: false }
|
|
1770
|
+
);
|
|
1771
|
+
|
|
1088
1772
|
document.addEventListener('keydown', e => {
|
|
1089
1773
|
if (e.key === 'Escape') {
|
|
1090
1774
|
closeModal();
|
|
@@ -1093,6 +1777,24 @@
|
|
|
1093
1777
|
e.preventDefault();
|
|
1094
1778
|
goBack();
|
|
1095
1779
|
}
|
|
1780
|
+
const img = document.getElementById('previewImage');
|
|
1781
|
+
if (img) {
|
|
1782
|
+
if (e.key === '+' || e.key === '=') {
|
|
1783
|
+
zoomImage(0.1);
|
|
1784
|
+
} else if (e.key === '-') {
|
|
1785
|
+
zoomImage(-0.1);
|
|
1786
|
+
} else if (e.key === '0') {
|
|
1787
|
+
resetImage();
|
|
1788
|
+
} else if (e.key === '[' || e.key === '{') {
|
|
1789
|
+
rotateImage(-90);
|
|
1790
|
+
} else if (e.key === ']' || e.key === '}') {
|
|
1791
|
+
rotateImage(90);
|
|
1792
|
+
} else if (e.key === 'ArrowLeft') {
|
|
1793
|
+
navigateImage(-1);
|
|
1794
|
+
} else if (e.key === 'ArrowRight') {
|
|
1795
|
+
navigateImage(1);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1096
1798
|
});
|
|
1097
1799
|
</script>
|
|
1098
1800
|
</body>
|
|
@@ -48,11 +48,19 @@ function init() {
|
|
|
48
48
|
let dirPath = req.query.path || '/';
|
|
49
49
|
dirPath = decodeURIComponent(dirPath);
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
let fullPath;
|
|
52
|
+
if (dirPath.startsWith('/')) {
|
|
53
|
+
fullPath = dirPath;
|
|
54
|
+
} else {
|
|
55
|
+
fullPath = path.resolve(explorerRoot, dirPath);
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
// 防止路径遍历攻击
|
|
54
|
-
if (
|
|
55
|
-
|
|
58
|
+
// 防止路径遍历攻击 - 确保路径不会越界
|
|
59
|
+
if (explorerRoot !== '/') {
|
|
60
|
+
const resolvedRoot = path.resolve(explorerRoot);
|
|
61
|
+
if (!fullPath.startsWith(resolvedRoot)) {
|
|
62
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
if (!existsSync(fullPath)) {
|
|
@@ -70,16 +78,28 @@ function init() {
|
|
|
70
78
|
|
|
71
79
|
files.forEach(file => {
|
|
72
80
|
const filePath = path.join(fullPath, file.name);
|
|
73
|
-
|
|
81
|
+
let fileStats;
|
|
82
|
+
let hasError = false;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
fileStats = statSync(filePath);
|
|
86
|
+
} catch (statError) {
|
|
87
|
+
hasError = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
const relativePath = path.join(dirPath, file.name);
|
|
75
91
|
|
|
92
|
+
// 使用 statSync 的结果,因为 readdirSync 在根目录对某些特殊目录识别不准确
|
|
93
|
+
const isDirectory = hasError ? file.isDirectory() : fileStats.isDirectory();
|
|
94
|
+
|
|
76
95
|
result.push({
|
|
77
96
|
name: file.name,
|
|
78
97
|
path: relativePath.replace(/\\/g, '/'),
|
|
79
|
-
isDirectory:
|
|
80
|
-
size: fileStats.size,
|
|
81
|
-
mtime: fileStats.mtime,
|
|
82
|
-
isHidden: file.name.startsWith('.')
|
|
98
|
+
isDirectory: isDirectory,
|
|
99
|
+
size: hasError ? 0 : fileStats.size,
|
|
100
|
+
mtime: hasError ? new Date() : fileStats.mtime,
|
|
101
|
+
isHidden: file.name.startsWith('.'),
|
|
102
|
+
error: hasError ? 'Cannot access file' : null
|
|
83
103
|
});
|
|
84
104
|
});
|
|
85
105
|
|
|
@@ -106,11 +126,19 @@ function init() {
|
|
|
106
126
|
let filePath = req.query.path || '/';
|
|
107
127
|
filePath = decodeURIComponent(filePath);
|
|
108
128
|
|
|
109
|
-
|
|
129
|
+
let fullPath;
|
|
130
|
+
if (filePath.startsWith('/')) {
|
|
131
|
+
fullPath = filePath;
|
|
132
|
+
} else {
|
|
133
|
+
fullPath = path.resolve(explorerRoot, filePath);
|
|
134
|
+
}
|
|
110
135
|
|
|
111
|
-
// 防止路径遍历攻击
|
|
112
|
-
if (
|
|
113
|
-
|
|
136
|
+
// 防止路径遍历攻击 - 确保路径不会越界
|
|
137
|
+
if (explorerRoot !== '/') {
|
|
138
|
+
const resolvedRoot = path.resolve(explorerRoot);
|
|
139
|
+
if (!fullPath.startsWith(resolvedRoot)) {
|
|
140
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
141
|
+
}
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
if (!existsSync(fullPath)) {
|
|
@@ -152,11 +180,19 @@ function init() {
|
|
|
152
180
|
let filePath = req.body.path || '/';
|
|
153
181
|
filePath = decodeURIComponent(filePath);
|
|
154
182
|
|
|
155
|
-
|
|
183
|
+
let fullPath;
|
|
184
|
+
if (filePath.startsWith('/')) {
|
|
185
|
+
fullPath = filePath;
|
|
186
|
+
} else {
|
|
187
|
+
fullPath = path.resolve(explorerRoot, filePath);
|
|
188
|
+
}
|
|
156
189
|
|
|
157
|
-
// 防止路径遍历攻击
|
|
158
|
-
if (
|
|
159
|
-
|
|
190
|
+
// 防止路径遍历攻击 - 确保路径不会越界
|
|
191
|
+
if (explorerRoot !== '/') {
|
|
192
|
+
const resolvedRoot = path.resolve(explorerRoot);
|
|
193
|
+
if (!fullPath.startsWith(resolvedRoot)) {
|
|
194
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
195
|
+
}
|
|
160
196
|
}
|
|
161
197
|
|
|
162
198
|
if (!existsSync(fullPath)) {
|