lexlepty commited on
Commit
346395d
·
verified ·
1 Parent(s): 164b5eb

Delete templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +0 -2120
templates/index.html DELETED
@@ -1,2120 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>云存储</title>
7
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
- <link href="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.css" rel="stylesheet">
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.js"></script>
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script>
11
- <style>
12
- /* 基础样式变量 */
13
- :root {
14
- --primary-glow: #ff9580;
15
- --secondary-glow: #ffd700;
16
- --background: #ffffff;
17
- --text: #333333;
18
- --sidebar-bg: #f8f9fa;
19
- --card-bg: #ffffff;
20
- --border-color: #e0e0e0;
21
- --shadow-color: rgba(0, 0, 0, 0.1);
22
- --sidebar-width: 240px;
23
- --header-height: 70px;
24
- }
25
-
26
- * {
27
- margin: 0;
28
- padding: 0;
29
- box-sizing: border-box;
30
- }
31
-
32
- body {
33
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
- background: var(--background);
35
- color: var(--text);
36
- min-height: 100vh;
37
- }
38
-
39
- /* 布局样式 */
40
- .container {
41
- display: flex;
42
- min-height: 100vh;
43
- }
44
-
45
- /* 侧边栏样式 */
46
- .sidebar {
47
- width: var(--sidebar-width);
48
- background: var(--sidebar-bg);
49
- border-right: 1px solid var(--border-color);
50
- padding: 20px;
51
- position: fixed;
52
- height: 100vh;
53
- overflow-y: auto;
54
- transition: all 0.3s ease;
55
- }
56
-
57
- .logo {
58
- padding: 20px 15px;
59
- margin-bottom: 30px;
60
- font-size: 24px;
61
- font-weight: bold;
62
- color: var(--primary-glow);
63
- }
64
-
65
- .nav-item {
66
- display: flex;
67
- align-items: center;
68
- padding: 15px;
69
- margin: 8px 0;
70
- border-radius: 12px;
71
- cursor: pointer;
72
- transition: all 0.3s ease;
73
- background: var(--card-bg);
74
- border: 1px solid transparent;
75
- }
76
-
77
- .nav-item:hover {
78
- border-color: var(--primary-glow);
79
- box-shadow: 0 0 15px rgba(255, 149, 128, 0.2);
80
- transform: translateX(5px);
81
- }
82
-
83
- .nav-item.active {
84
- background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
85
- color: white;
86
- }
87
-
88
- .nav-item i {
89
- margin-right: 12px;
90
- font-size: 20px;
91
- }
92
-
93
- /* 主内容区样式 */
94
- .main-content {
95
- flex: 1;
96
- margin-left: var(--sidebar-width);
97
- padding: calc(var(--header-height) + 20px) 30px 30px;
98
- background: var(--background);
99
- }
100
-
101
- /* 头部搜索栏样式 */
102
- .header {
103
- position: fixed;
104
- top: 0;
105
- left: var(--sidebar-width);
106
- right: 0;
107
- height: var(--header-height);
108
- background: var(--card-bg);
109
- padding: 15px 30px;
110
- display: flex;
111
- align-items: center;
112
- box-shadow: 0 2px 10px var(--shadow-color);
113
- z-index: 100;
114
- }
115
-
116
- .search-container {
117
- flex: 1;
118
- max-width: 600px;
119
- margin: 0 20px;
120
- position: relative;
121
- }
122
-
123
- .search-box {
124
- width: 100%;
125
- padding: 12px 20px;
126
- border-radius: 25px;
127
- border: 2px solid var(--border-color);
128
- background: var(--background);
129
- font-size: 16px;
130
- transition: all 0.3s ease;
131
- }
132
-
133
- .search-box:focus {
134
- outline: none;
135
- border-color: var(--primary-glow);
136
- box-shadow: 0 0 10px rgba(255, 149, 128, 0.3);
137
- }
138
-
139
- /* 视图切换按钮样式 */
140
- .view-toggle {
141
- position: absolute;
142
- right: 30px;
143
- top: calc(var(--header-height) + 20px);
144
- display: flex;
145
- gap: 10px;
146
- z-index: 10;
147
- }
148
-
149
- .view-btn {
150
- padding: 8px 15px;
151
- border: 1px solid var(--border-color);
152
- border-radius: 8px;
153
- background: var(--card-bg);
154
- cursor: pointer;
155
- transition: all 0.3s ease;
156
- }
157
-
158
- .view-btn.active {
159
- background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
160
- color: white;
161
- border-color: transparent;
162
- }
163
-
164
- /* 文件显示样式 */
165
- .file-container {
166
- margin-top: 60px;
167
- }
168
-
169
- /* 网格视图样式 */
170
- .file-grid {
171
- display: grid;
172
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
173
- gap: 20px;
174
- padding: 20px 0;
175
- }
176
-
177
- .file-item.grid {
178
- background: var(--card-bg);
179
- border-radius: 15px;
180
- padding: 20px;
181
- text-align: center;
182
- cursor: pointer;
183
- transition: all 0.3s ease;
184
- border: 1px solid var(--border-color);
185
- position: relative;
186
- overflow: hidden;
187
- }
188
-
189
- .file-item.grid:hover {
190
- transform: translateY(-5px);
191
- box-shadow: 0 10px 20px var(--shadow-color);
192
- border-color: var(--primary-glow);
193
- }
194
-
195
- .file-item.grid::before {
196
- content: '';
197
- position: absolute;
198
- top: 0;
199
- left: 0;
200
- right: 0;
201
- height: 4px;
202
- background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
203
- opacity: 0;
204
- transition: opacity 0.3s ease;
205
- }
206
-
207
- .file-item.grid:hover::before {
208
- opacity: 1;
209
- }
210
-
211
- /* 列表视图样式 */
212
- .file-list {
213
- display: flex;
214
- flex-direction: column;
215
- gap: 10px;
216
- }
217
-
218
- .file-item.list {
219
- display: flex;
220
- align-items: center;
221
- padding: 15px;
222
- background: var(--card-bg);
223
- border-radius: 12px;
224
- border: 1px solid var(--border-color);
225
- transition: all 0.3s ease;
226
- }
227
-
228
- .file-item.list:hover {
229
- transform: translateX(5px);
230
- border-color: var(--primary-glow);
231
- box-shadow: 0 5px 15px var(--shadow-color);
232
- }
233
-
234
- .file-item.list .file-icon {
235
- font-size: 24px;
236
- margin-right: 15px;
237
- }
238
-
239
- .file-item.list .file-info {
240
- flex: 1;
241
- display: flex;
242
- justify-content: space-between;
243
- align-items: center;
244
- }
245
-
246
- .file-item.list .file-name {
247
- font-weight: 500;
248
- }
249
-
250
- .file-item.list .file-meta {
251
- display: flex;
252
- gap: 20px;
253
- color: #666;
254
- }
255
-
256
- /* 文件图标和信息样式 */
257
- .file-icon {
258
- font-size: 48px;
259
- margin-bottom: 15px;
260
- color: var(--primary-glow);
261
- }
262
-
263
- .file-name {
264
- font-size: 14px;
265
- margin-bottom: 8px;
266
- word-break: break-word;
267
- }
268
-
269
- .file-size {
270
- font-size: 12px;
271
- color: #666;
272
- }
273
-
274
- /* 文件操作菜单 */
275
- .file-menu {
276
- position: absolute;
277
- background: var(--card-bg);
278
- border-radius: 8px;
279
- box-shadow: 0 5px 20px var(--shadow-color);
280
- padding: 8px 0;
281
- z-index: 1000;
282
- }
283
-
284
- .file-menu-item {
285
- padding: 8px 20px;
286
- cursor: pointer;
287
- transition: background 0.3s ease;
288
- white-space: nowrap;
289
- }
290
-
291
- .file-menu-item:hover {
292
- background: var(--sidebar-bg);
293
- }
294
-
295
-
296
- /* 上传按钮和进度条 */
297
- .upload-btn {
298
- position: fixed;
299
- right: 30px;
300
- bottom: 30px;
301
- width: 60px;
302
- height: 60px;
303
- border-radius: 50%;
304
- background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
305
- color: white;
306
- display: flex;
307
- align-items: center;
308
- justify-content: center;
309
- cursor: pointer;
310
- box-shadow: 0 4px 15px rgba(255, 149, 128, 0.4);
311
- transition: all 0.3s ease;
312
- z-index: 1000;
313
- }
314
-
315
- .upload-btn:hover {
316
- transform: scale(1.1);
317
- }
318
-
319
- .upload-progress {
320
- position: fixed;
321
- bottom: 30px;
322
- right: 100px;
323
- background: var(--card-bg);
324
- padding: 15px;
325
- border-radius: 12px;
326
- box-shadow: 0 5px 20px var(--shadow-color);
327
- display: none;
328
- }
329
-
330
- .progress-bar {
331
- width: 200px;
332
- height: 6px;
333
- background: var(--border-color);
334
- border-radius: 3px;
335
- overflow: hidden;
336
- }
337
-
338
- .progress-fill {
339
- height: 100%;
340
- background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
341
- width: 0%;
342
- transition: width 0.3s ease;
343
- }
344
-
345
- /* 移动端适配 */
346
- @media (max-width: 768px) {
347
- .sidebar {
348
- width: 100%;
349
- height: 60px;
350
- padding: 0 10px;
351
- bottom: 0;
352
- display: flex;
353
- align-items: center;
354
- justify-content: space-around;
355
- z-index: 1000;
356
- background: var(--sidebar-bg);
357
- box-shadow: 0 -2px 10px var(--shadow-color);
358
- }
359
-
360
- .logo {
361
- display: none;
362
- }
363
-
364
- .nav-item {
365
- flex: 1;
366
- margin: 0 5px;
367
- padding: 8px 15px;
368
- flex-direction: row;
369
- align-items: center;
370
- font-size: 12px;
371
- height: 40px;
372
- border-radius: 8px;
373
- }
374
-
375
- .nav-item i {
376
- margin: 0 8px 0 0;
377
- font-size: 16px;
378
- }
379
-
380
- .nav-item-text {
381
- display: block;
382
- white-space: nowrap;
383
- overflow: hidden;
384
- text-overflow: ellipsis;
385
- }
386
-
387
- .main-content {
388
- margin-left: 0;
389
- margin-bottom: 70px;
390
- padding-top: 90px;
391
- padding-bottom: 70px;
392
- min-height: calc(100vh - 70px);
393
- }
394
-
395
- .header {
396
- left: 0;
397
- z-index: 999;
398
- }
399
-
400
- .search-container {
401
- margin: 0;
402
- }
403
-
404
- .upload-btn {
405
- right: 20px;
406
- bottom: 80px;
407
- z-index: 1001;
408
- }
409
-
410
- .file-grid {
411
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
412
- }
413
-
414
- .view-toggle {
415
- right: 20px;
416
- gap: 8px;
417
- }
418
-
419
- .upload-progress {
420
- bottom: 70px;
421
- right: 20px;
422
- max-width: calc(100vw - 40px);
423
- z-index: 1001;
424
- }
425
- }
426
- .action-buttons {
427
- position: absolute;
428
- right: 30px;
429
- top: calc(var(--header-height) + 20px);
430
- display: flex;
431
- gap: 16px;
432
- align-items: center;
433
- z-index: 10;
434
- }
435
-
436
- .action-btn {
437
- padding: 8px 15px;
438
- border: 1px solid var(--border-color);
439
- border-radius: 8px;
440
- background: var(--card-bg);
441
- cursor: pointer;
442
- transition: all 0.3s ease;
443
- display: flex;
444
- align-items: center;
445
- gap: 8px;
446
- color: var(--text);
447
- }
448
-
449
- .action-btn:hover {
450
- border-color: var(--primary-glow);
451
- box-shadow: 0 2px 8px var(--shadow-color);
452
- }
453
-
454
- .action-btn i {
455
- font-size: 16px;
456
- color: var(--primary-glow);
457
- }
458
-
459
- @media (max-width: 768px) {
460
- .action-buttons {
461
- right: 20px;
462
- gap: 8px;
463
- }
464
-
465
- .action-btn {
466
- padding: 6px 12px;
467
- font-size: 12px;
468
- }
469
-
470
- .action-btn i {
471
- font-size: 14px;
472
- }
473
- }
474
- /* 拖拽上传区域样式 */
475
- .drag-overlay {
476
- position: fixed;
477
- top: 0;
478
- left: 0;
479
- right: 0;
480
- bottom: 0;
481
- background: rgba(255, 149, 128, 0.1);
482
- border: 3px dashed var(--primary-glow);
483
- z-index: 2000;
484
- display: none;
485
- align-items: center;
486
- justify-content: center;
487
- font-size: 24px;
488
- color: var(--primary-glow);
489
- }
490
-
491
- /* 面包屑导航 */
492
- .breadcrumb {
493
- margin: 20px 0;
494
- padding: 12px 16px;
495
- display: inline-flex;
496
- align-items: center;
497
- flex-wrap: wrap;
498
- gap: 8px;
499
- font-size: 14px;
500
- background: var(--card-bg);
501
- border-radius: 8px;
502
- box-shadow: 0 2px 8px var(--shadow-color);
503
- width: auto;
504
- min-width: min-content;
505
- }
506
-
507
- .breadcrumb-item {
508
- cursor: pointer;
509
- color: var(--text);
510
- transition: all 0.3s ease;
511
- padding: 4px 8px;
512
- border-radius: 4px;
513
- display: inline-flex; /* Changed from flex to inline-flex */
514
- align-items: center;
515
- white-space: nowrap;
516
- overflow: hidden;
517
- text-overflow: ellipsis;
518
- }
519
-
520
- .breadcrumb-item:hover {
521
- color: var(--primary-glow);
522
- background: rgba(255, 149, 128, 0.1);
523
- }
524
-
525
- .breadcrumb-separator {
526
- color: var(--border-color);
527
- margin: 0 4px;
528
- user-select: none;
529
- }
530
-
531
- @media (max-width: 768px) {
532
- .breadcrumb {
533
- padding: 8px 12px;
534
- margin: 12px 0;
535
- font-size: 12px;
536
- overflow-x: auto;
537
- -webkit-overflow-scrolling: touch;
538
- scrollbar-width: none;
539
- -ms-overflow-style: none;
540
- }
541
-
542
- .breadcrumb::-webkit-scrollbar {
543
- display: none;
544
- }
545
-
546
- .breadcrumb-item {
547
- padding: 4px 6px;
548
- max-width: 150px;
549
- }
550
- }
551
- /* 加载指示器样式 */
552
- .loading-indicator {
553
- display: flex;
554
- flex-direction: column;
555
- align-items: center;
556
- padding: 2rem;
557
- background: var(--card-bg);
558
- border-radius: 15px;
559
- }
560
-
561
- .spinner {
562
- width: 40px;
563
- height: 40px;
564
- border: 4px solid var(--border-color);
565
- border-top-color: var(--primary-glow);
566
- border-radius: 50%;
567
- animation: spin 1s linear infinite;
568
- margin-bottom: 1rem;
569
- }
570
-
571
- @keyframes spin {
572
- 100% { transform: rotate(360deg); }
573
- }
574
-
575
- .loading-text {
576
- color: var(--text);
577
- font-size: 1rem;
578
- margin-top: 1rem;
579
- }
580
-
581
- /* 上传进度样式 */
582
- .upload-progress {
583
- width: 400px;
584
- max-width: 90vw;
585
- }
586
-
587
- .progress-item {
588
- background: var(--card-bg);
589
- border-radius: 8px;
590
- padding: 1rem;
591
- margin-bottom: 0.5rem;
592
- box-shadow: 0 2px 8px var(--shadow-color);
593
- }
594
-
595
- .file-info {
596
- display: flex;
597
- justify-content: space-between;
598
- align-items: center;
599
- margin-bottom: 0.5rem;
600
- }
601
-
602
- .filename {
603
- font-weight: 500;
604
- max-width: 250px;
605
- overflow: hidden;
606
- text-overflow: ellipsis;
607
- white-space: nowrap;
608
- }
609
-
610
- .cancel-upload {
611
- background: #ff4444;
612
- color: white;
613
- border: none;
614
- border-radius: 4px;
615
- padding: 0.25rem 0.75rem;
616
- cursor: pointer;
617
- font-size: 0.875rem;
618
- transition: all 0.3s ease;
619
- }
620
-
621
- .cancel-upload:hover {
622
- background: #ff6666;
623
- transform: translateY(-1px);
624
- }
625
-
626
- .upload-stats {
627
- display: flex;
628
- justify-content: space-between;
629
- font-size: 0.875rem;
630
- color: #666;
631
- margin-top: 0.5rem;
632
- }
633
-
634
- .progress-bar {
635
- width: 100%;
636
- height: 6px;
637
- background: var(--border-color);
638
- border-radius: 3px;
639
- overflow: hidden;
640
- }
641
-
642
- .progress-fill {
643
- height: 100%;
644
- background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
645
- width: 0%;
646
- transition: width 0.3s ease;
647
- }
648
- /* 确认对话框样式 */
649
- .confirm-modal {
650
- position: fixed;
651
- top: 0;
652
- left: 0;
653
- right: 0;
654
- bottom: 0;
655
- background: rgba(0, 0, 0, 0.5);
656
- display: flex;
657
- align-items: center;
658
- justify-content: center;
659
- z-index: 3000;
660
- }
661
-
662
- .confirm-content {
663
- background: var(--card-bg);
664
- border-radius: 12px;
665
- padding: 24px;
666
- max-width: 400px;
667
- width: 90%;
668
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
669
- }
670
-
671
- .confirm-content h3 {
672
- margin-bottom: 16px;
673
- color: var(--text);
674
- }
675
-
676
- .confirm-content p {
677
- margin-bottom: 24px;
678
- color: #666;
679
- line-height: 1.5;
680
- }
681
-
682
- .confirm-buttons {
683
- display: flex;
684
- justify-content: flex-end;
685
- gap: 12px;
686
- }
687
-
688
- .confirm-buttons button {
689
- padding: 8px 20px;
690
- border-radius: 6px;
691
- border: none;
692
- cursor: pointer;
693
- transition: all 0.3s ease;
694
- }
695
-
696
- .confirm-cancel {
697
- background: #f0f0f0;
698
- color: #666;
699
- }
700
-
701
- .confirm-ok {
702
- background: #ff4444;
703
- color: white;
704
- }
705
-
706
- .confirm-buttons button:hover {
707
- transform: translateY(-1px);
708
- }
709
-
710
- /* 提示消息样式 */
711
- .toast-message {
712
- position: fixed;
713
- bottom: 24px;
714
- left: 50%;
715
- transform: translateX(-50%) translateY(100px);
716
- background: rgba(0, 0, 0, 0.8);
717
- color: white;
718
- padding: 12px 24px;
719
- border-radius: 6px;
720
- font-size: 14px;
721
- opacity: 0;
722
- transition: all 0.3s ease;
723
- }
724
-
725
- .toast-message.show {
726
- transform: translateX(-50%) translateY(0);
727
- opacity: 1;
728
- }
729
- .preview-modal {
730
- position: fixed;
731
- top: 0;
732
- left: 0;
733
- right: 0;
734
- bottom: 0;
735
- background: rgba(0, 0, 0, 0.85);
736
- display: none;
737
- z-index: 2000;
738
- }
739
- .preview-content {
740
- max-width: 90%;
741
- max-height: 90%;
742
- position: relative;
743
- background: #fff;
744
- border-radius: 12px;
745
- overflow: hidden;
746
- display: flex;
747
- flex-direction: column;
748
- }
749
-
750
- .preview-container {
751
- position: relative;
752
- width: 100%;
753
- height: 100%;
754
- display: flex;
755
- align-items: center;
756
- justify-content: center;
757
- }
758
-
759
- .preview-header {
760
- padding: 16px;
761
- background: #f8f9fa;
762
- border-bottom: 1px solid #e9ecef;
763
- display: flex;
764
- justify-content: space-between;
765
- align-items: center;
766
- }
767
-
768
- .preview-body {
769
- flex: 1;
770
- overflow: auto;
771
- padding: 24px;
772
- display: flex;
773
- align-items: center;
774
- justify-content: center;
775
- }
776
- .preview-image-container {
777
- overflow: hidden;
778
- display: flex;
779
- align-items: center;
780
- justify-content: center;
781
- }
782
-
783
- .preview-image {
784
- max-width: 100%;
785
- max-height: 100%;
786
- object-fit: contain;
787
- transition: transform 0.3s ease;
788
- }
789
-
790
- .text-preview,
791
- .markdown-preview {
792
- background: white;
793
- padding: 20px;
794
- overflow: auto;
795
- font-size: 14px;
796
- line-height: 1.6;
797
- }
798
-
799
- .markdown-preview {
800
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
801
- }
802
-
803
- .preview-action-btn {
804
- padding: 8px;
805
- margin-left: 8px;
806
- border: none;
807
- background: none;
808
- color: #666;
809
- cursor: pointer;
810
- transition: all 0.3s ease;
811
- }
812
-
813
- .preview-action-btn:hover {
814
- color: #000;
815
- background: #e9ecef;
816
- border-radius: 4px;
817
- }
818
- .preview-close {
819
- position: absolute;
820
- top: 20px;
821
- right: 20px;
822
- width: 40px;
823
- height: 40px;
824
- border-radius: 50%;
825
- background: rgba(255, 255, 255, 0.2);
826
- border: none;
827
- color: white;
828
- cursor: pointer;
829
- display: flex;
830
- align-items: center;
831
- justify-content: center;
832
- transition: all 0.3s ease;
833
- }
834
- .cancel-download {
835
- padding: 4px 8px;
836
- border: none;
837
- background: #ff4444;
838
- color: white;
839
- border-radius: 4px;
840
- cursor: pointer;
841
- font-size: 12px;
842
- display: flex;
843
- align-items: center;
844
- gap: 4px;
845
- transition: all 0.3s ease;
846
- }
847
-
848
- .cancel-download:hover {
849
- background: #ff6666;
850
- transform: translateY(-1px);
851
- }
852
-
853
- .stats-row {
854
- display: flex;
855
- justify-content: space-between;
856
- margin-top: 4px;
857
- }
858
-
859
- .download-stats {
860
- font-size: 12px;
861
- color: #666;
862
- margin-top: 8px;
863
- }
864
- .file-item.selectable {
865
- position: relative;
866
- cursor: pointer;
867
- }
868
-
869
- .file-item.selectable::before {
870
- content: '';
871
- position: absolute;
872
- top: 10px;
873
- left: 10px;
874
- width: 20px;
875
- height: 20px;
876
- border: 2px solid var(--border-color);
877
- border-radius: 4px;
878
- background: white;
879
- z-index: 1;
880
- }
881
-
882
- .file-item.selected::before {
883
- background: var(--primary-glow);
884
- border-color: var(--primary-glow);
885
- }
886
-
887
- .file-item.selected::after {
888
- content: '\f00c';
889
- font-family: 'Font Awesome 6 Free';
890
- font-weight: 900;
891
- position: absolute;
892
- top: 10px;
893
- left: 10px;
894
- width: 20px;
895
- height: 20px;
896
- color: white;
897
- display: flex;
898
- align-items: center;
899
- justify-content: center;
900
- z-index: 2;
901
- }
902
-
903
- .multi-select-btn.active {
904
- background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
905
- color: white;
906
- border-color: transparent;
907
- }
908
- .batch-operations {
909
- display: flex;
910
- gap: 8px;
911
- margin-left: 16px;
912
- }
913
-
914
- .folder-name-input {
915
- width: 100%;
916
- padding: 8px 12px;
917
- border: 1px solid var(--border-color);
918
- border-radius: 4px;
919
- margin: 16px 0;
920
- font-size: 14px;
921
- }
922
-
923
- .folder-name-input:focus {
924
- outline: none;
925
- border-color: var(--primary-glow);
926
- box-shadow: 0 0 0 2px rgba(255, 149, 128, 0.2);
927
- }
928
-
929
- .multi-select-btn.active {
930
- background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
931
- color: white;
932
- border-color: transparent;
933
- }
934
- .logout-btn {
935
- padding: 8px 15px;
936
- border: 1px solid var(--border-color);
937
- border-radius: 8px;
938
- background: var(--card-bg);
939
- color: var(--text);
940
- cursor: pointer;
941
- transition: all 0.3s ease;
942
- display: flex;
943
- align-items: center;
944
- gap: 8px;
945
- margin-right: 10px;
946
- margin-left: 10px;
947
- }
948
- .logout-btn:hover {
949
- border-color: var(--primary-glow);
950
- color: var(--primary-glow);
951
- transform: translateY(-2px);
952
- }
953
-
954
- .logout-btn i {
955
- font-size: 16px;
956
- }
957
- </style>
958
- </head>
959
- <body>
960
- <div class="container">
961
- <!-- 侧边栏 -->
962
- <nav class="sidebar">
963
- <div class="logo">
964
- <i class="fas fa-cloud"></i> Cloud Vault
965
- </div>
966
- <div class="nav-item active" data-type="all">
967
- <i class="fas fa-folder"></i>
968
- <span class="nav-item-text">全部文件</span>
969
- </div>
970
- <div class="nav-item" data-type="image">
971
- <i class="fas fa-image"></i>
972
- <span class="nav-item-text">图片</span>
973
- </div>
974
- <div class="nav-item" data-type="video">
975
- <i class="fas fa-video"></i>
976
- <span class="nav-item-text">视频</span>
977
- </div>
978
- <div class="nav-item" data-type="document">
979
- <i class="fas fa-file-alt"></i>
980
- <span class="nav-item-text">文档</span>
981
- </div>
982
- <div class="nav-item" data-type="audio">
983
- <i class="fas fa-music"></i>
984
- <span class="nav-item-text">音频</span>
985
- </div>
986
- <div class="nav-item" data-type="archive">
987
- <i class="fas fa-file-archive"></i>
988
- <span class="nav-item-text">压缩包</span>
989
- </div>
990
- </nav>
991
-
992
- <!-- 顶部搜索栏 -->
993
- <header class="header">
994
- <div class="search-container">
995
- <input type="text" class="search-box" placeholder="搜索文件...">
996
- </div>
997
- <!-- 添加退出登录按钮 -->
998
- <button class="logout-btn" onclick="handleLogout()">
999
- <i class="fas fa-sign-out-alt"></i>
1000
- 退出登录
1001
- </button>
1002
- </header>
1003
-
1004
- <!-- 主内容区 -->
1005
- <main class="main-content">
1006
- <!-- 面包屑导航 -->
1007
- <div class="breadcrumb">
1008
- <span class="breadcrumb-item" data-path="/">根目录</span>
1009
- </div>
1010
- <button class="action-btn create-folder-btn">
1011
- <i class="fas fa-folder-plus"></i>
1012
- <span>新建文件夹</span>
1013
- </button>
1014
- <!-- 视图切换按钮 -->
1015
- <div class="view-toggle">
1016
- <button class="view-btn active" data-view="grid">
1017
- <i class="fas fa-th"></i>
1018
- </button>
1019
- <button class="view-btn" data-view="list">
1020
- <i class="fas fa-list"></i>
1021
- </button>
1022
- </div>
1023
-
1024
-
1025
-
1026
- <!-- 文件容器 -->
1027
- <div class="file-container">
1028
- <!-- 文件内容将通过 JavaScript 动态生成 -->
1029
- </div>
1030
- </main>
1031
-
1032
- <!-- 上传按钮 -->
1033
- <div class="upload-btn" id="uploadBtn">
1034
- <i class="fas fa-plus"></i>
1035
- <input type="file" id="fileInput" style="display: none;" multiple>
1036
- </div>
1037
-
1038
- <!-- 上传进度条 -->
1039
- <div class="upload-progress">
1040
- </div>
1041
- </div>
1042
-
1043
- <!-- 拖拽上传遮罩 -->
1044
- <div class="drag-overlay">
1045
- <div>释放鼠标上传文件</div>
1046
- </div>
1047
-
1048
- <!-- 文件操作菜单 -->
1049
- <div class="file-menu" style="display: none;">
1050
- <div class="file-menu-item" data-action="preview">
1051
- <i class="fas fa-eye"></i> 预览
1052
- </div>
1053
- <div class="file-menu-item" data-action="download">
1054
- <i class="fas fa-download"></i> 下载
1055
- </div>
1056
- <div class="file-menu-item" data-action="delete">
1057
- <i class="fas fa-trash"></i> 删除
1058
- </div>
1059
- </div>
1060
- <!-- 预览模态框 -->
1061
- <div class="preview-modal">
1062
- <div class="preview-container">
1063
- <div class="preview-content">
1064
- <!-- 预览内容将通过 JavaScript 动态生成 -->
1065
- </div>
1066
- <button class="preview-close">
1067
- <i class="fas fa-times"></i>
1068
- </button>
1069
- </div>
1070
- </div>
1071
- <script>
1072
- // 文件管理类
1073
- class FileManager {
1074
- constructor() {
1075
- this.selectedFiles = new Set();
1076
- this.isMultiSelectMode = false;
1077
- this.currentPath = '/';
1078
- this.currentView = 'grid';
1079
- this.currentFileType = 'all';
1080
- this.files = [];
1081
- this.initEventListeners();
1082
- this.loadFiles();
1083
- }
1084
-
1085
- // 初始化事件监听
1086
- initEventListeners() {
1087
- // 视图切换
1088
- document.querySelectorAll('.view-btn').forEach(btn => {
1089
- btn.addEventListener('click', () => this.switchView(btn.dataset.view));
1090
- });
1091
-
1092
- // 文件类型筛选
1093
- document.querySelectorAll('.nav-item').forEach(item => {
1094
- item.addEventListener('click', () => this.filterByType(item.dataset.type));
1095
- });
1096
-
1097
- // 搜索
1098
- const searchBox = document.querySelector('.search-box');
1099
- searchBox.addEventListener('input', this.debounce((e) => this.handleSearch(e.target.value), 300));
1100
-
1101
- // 文件上传
1102
- const uploadBtn = document.getElementById('uploadBtn');
1103
- const fileInput = document.getElementById('fileInput');
1104
-
1105
- uploadBtn.addEventListener('click', () => fileInput.click());
1106
- fileInput.addEventListener('change', (e) => this.handleFileUpload(e.target.files));
1107
-
1108
- // 拖拽上传
1109
- this.initDragAndDrop();
1110
- // 新建文件夹按钮监听
1111
- const createFolderBtn = document.querySelector('.create-folder-btn');
1112
- createFolderBtn.addEventListener('click', () => this.showCreateFolderDialog());
1113
-
1114
- // 添加多选按钮
1115
- const multiSelectBtn = document.createElement('button');
1116
- multiSelectBtn.className = 'action-btn multi-select-btn';
1117
- multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>多选</span>';
1118
- multiSelectBtn.addEventListener('click', () => this.toggleMultiSelectMode());
1119
- document.querySelector('.view-toggle').prepend(multiSelectBtn);
1120
- }
1121
-
1122
- // 加载文件列表
1123
- async loadFiles() {
1124
- try {
1125
- const path = this.currentPath === '/' ? '' : this.currentPath;
1126
- const response = await fetch(`/api/files/list/${path}`);
1127
- if (!response.ok) throw new Error('Failed to load files');
1128
-
1129
- this.files = await response.json();
1130
- this.renderFiles();
1131
- this.updateBreadcrumb();
1132
- } catch (error) {
1133
- console.error('Error loading files:', error);
1134
- this.showError('加载文件失败');
1135
- }
1136
- }
1137
-
1138
- // 渲染文件列表
1139
- renderFiles() {
1140
- const container = document.querySelector('.file-container');
1141
- container.innerHTML = '';
1142
-
1143
- const viewClass = this.currentView === 'grid' ? 'file-grid' : 'file-list';
1144
- container.className = `file-container ${viewClass}`;
1145
-
1146
- let filteredFiles = this.files;
1147
- if (this.currentFileType !== 'all') {
1148
- filteredFiles = this.files.filter(file => file.file_type === this.currentFileType);
1149
- }
1150
-
1151
- filteredFiles.forEach(file => {
1152
- const fileElement = this.createFileElement(file);
1153
- container.appendChild(fileElement);
1154
- });
1155
- }
1156
-
1157
- // 创建文件元素
1158
- createFileElement(file) {
1159
- const element = document.createElement('div');
1160
- element.className = `file-item ${this.currentView}`;
1161
-
1162
- // 添加多选模式相关的类
1163
- if (this.isMultiSelectMode) {
1164
- element.classList.add('selectable');
1165
- if (this.selectedFiles.has(file)) {
1166
- element.classList.add('selected');
1167
- }
1168
- }
1169
-
1170
- const icon = this.getFileIcon(file.type, file.file_type);
1171
- const size = this.formatFileSize(file.size);
1172
-
1173
- if (this.currentView === 'grid') {
1174
- element.innerHTML = `
1175
- <i class="${icon} file-icon"></i>
1176
- <div class="file-name">${file.path.split('/').pop()}</div>
1177
- <div class="file-size">${size}</div>
1178
- `;
1179
- } else {
1180
- element.innerHTML = `
1181
- <i class="${icon} file-icon"></i>
1182
- <div class="file-info">
1183
- <div class="file-name">${file.path.split('/').pop()}</div>
1184
- <div class="file-meta">
1185
- <span>${size}</span>
1186
- <span>${file.file_type || '未知类型'}</span>
1187
- </div>
1188
- </div>
1189
- `;
1190
- }
1191
-
1192
- // 事件处理逻辑
1193
- if (this.isMultiSelectMode) {
1194
- // 多选模式下的点击处理
1195
- element.addEventListener('click', (e) => {
1196
- e.preventDefault();
1197
- e.stopPropagation();
1198
-
1199
- if (file.type === 'directory') {
1200
- // 文件夹仍然保持导航功能
1201
- this.currentPath = file.path;
1202
- this.loadFiles();
1203
- } else {
1204
- // 文件切换选中状态
1205
- if (this.selectedFiles.has(file)) {
1206
- this.selectedFiles.delete(file);
1207
- element.classList.remove('selected');
1208
- } else {
1209
- this.selectedFiles.add(file);
1210
- element.classList.add('selected');
1211
- }
1212
- }
1213
- });
1214
- } else {
1215
- // 普通模式下的点击处理
1216
- element.addEventListener('click', () => this.handleFileClick(file));
1217
- }
1218
-
1219
- // 右键菜单处理
1220
- element.addEventListener('contextmenu', (e) => {
1221
- e.preventDefault();
1222
- this.showFileMenu(e, file);
1223
- });
1224
-
1225
- return element;
1226
- }
1227
- // 处理文件点击
1228
- handleFileClick(file) {
1229
- if (file.type === 'directory') {
1230
- this.currentPath = file.path;
1231
- this.loadFiles();
1232
- } else {
1233
- this.previewFile(file);
1234
- }
1235
- }
1236
-
1237
- // 显示文件操作菜单
1238
- showFileMenu(e, file) {
1239
- e.preventDefault();
1240
-
1241
- const menu = document.querySelector('.file-menu');
1242
- menu.style.display = 'block';
1243
- menu.style.left = `${e.pageX}px`;
1244
- menu.style.top = `${e.pageY}px`;
1245
-
1246
- // 清除旧的事件监听
1247
- const menuItems = menu.querySelectorAll('.file-menu-item');
1248
- menuItems.forEach(item => {
1249
- const clone = item.cloneNode(true);
1250
- item.parentNode.replaceChild(clone, item);
1251
- });
1252
-
1253
- // 添加新的事件监听
1254
- menu.querySelector('[data-action="preview"]').addEventListener('click', () => this.previewFile(file));
1255
- menu.querySelector('[data-action="download"]').addEventListener('click', () => this.downloadFile(file));
1256
- menu.querySelector('[data-action="delete"]').addEventListener('click', () => this.deleteFile(file));
1257
-
1258
- // 点击其他地方关闭菜单
1259
- const closeMenu = () => {
1260
- menu.style.display = 'none';
1261
- document.removeEventListener('click', closeMenu);
1262
- };
1263
-
1264
- setTimeout(() => {
1265
- document.addEventListener('click', closeMenu);
1266
- }, 0);
1267
- }
1268
-
1269
- // 文件预览
1270
- async previewFile(file) {
1271
- try {
1272
- const modal = document.querySelector('.preview-modal');
1273
- const content = modal.querySelector('.preview-content');
1274
-
1275
- // 计算合适的预览尺寸
1276
- const windowWidth = window.innerWidth;
1277
- const windowHeight = window.innerHeight;
1278
- const maxWidth = Math.min(windowWidth * 0.9, 1200); // 最大宽度不超过1200px
1279
- const maxHeight = windowHeight * 0.85;
1280
-
1281
- modal.style.display = 'flex';
1282
- content.innerHTML = `
1283
- <div class="loading-indicator">
1284
- <div class="spinner"></div>
1285
- <div class="loading-text">正在加载预览...</div>
1286
- </div>
1287
- `;
1288
-
1289
- const response = await fetch(`/api/files/preview/${file.path}`);
1290
- if (!response.ok) throw new Error('Failed to preview file');
1291
-
1292
- const blob = await response.blob();
1293
- const url = URL.createObjectURL(blob);
1294
- const mimeType = response.headers.get('content-type') || '';
1295
- const fileName = file.path.split('/').pop();
1296
-
1297
- // 获取预览内容
1298
- const previewContent = `
1299
- <div class="preview-header">
1300
- <div class="preview-info">
1301
- <i class="${this.getFileIcon(file.type, file.file_type)}"></i>
1302
- <span>${fileName}</span>
1303
- </div>
1304
- <div class="preview-actions">
1305
- <button class="preview-action-btn zoom-in">
1306
- <i class="fas fa-search-plus"></i>
1307
- </button>
1308
- <button class="preview-action-btn zoom-out">
1309
- <i class="fas fa-search-minus"></i>
1310
- </button>
1311
- <button class="preview-action-btn download">
1312
- <i class="fas fa-download"></i>
1313
- </button>
1314
- </div>
1315
- </div>
1316
- <div class="preview-body" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
1317
- ${await this.getPreviewContent(file, url, mimeType, maxWidth, maxHeight)}
1318
- </div>
1319
- `;
1320
-
1321
- content.innerHTML = previewContent;
1322
-
1323
- // 绑定事件处理
1324
- this.bindPreviewEvents(modal, content, file, url);
1325
-
1326
- } catch (error) {
1327
- console.error('Error previewing file:', error);
1328
- this.showError('预览文件失败');
1329
- }
1330
- }
1331
-
1332
- async getPreviewContent(file, url, mimeType, maxWidth, maxHeight) {
1333
- const extension = file.path.split('.').pop().toLowerCase();
1334
-
1335
- if (file.file_type === 'image' || mimeType.startsWith('image/')) {
1336
- return `
1337
- <div class="preview-image-container" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
1338
- <img src="${url}" alt="${file.path}" class="preview-image">
1339
- </div>
1340
- `;
1341
- }
1342
-
1343
- if (file.file_type === 'video' || mimeType.startsWith('video/')) {
1344
- return `
1345
- <div class="video-container" style="max-width: ${maxWidth * 0.8}px;">
1346
- <video class="plyr-media" controls crossorigin playsinline>
1347
- <source src="${url}" type="${mimeType}">
1348
- </video>
1349
- </div>
1350
- `;
1351
- }
1352
-
1353
- if (file.file_type === 'audio' || mimeType.startsWith('audio/')) {
1354
- return `
1355
- <div class="audio-container" style="width: ${maxWidth * 0.6}px;">
1356
- <audio class="plyr-media" controls>
1357
- <source src="${url}" type="${mimeType}">
1358
- </audio>
1359
- </div>
1360
- `;
1361
- }
1362
-
1363
- if (mimeType.includes('pdf')) {
1364
- return `
1365
- <iframe src="${url}#view=FitH" type="application/pdf"
1366
- style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
1367
- </iframe>
1368
- `;
1369
- }
1370
-
1371
- // 支持 Markdown 预览
1372
- if (extension === 'md') {
1373
- const text = await (await fetch(url)).text();
1374
- const marked = window.marked; // 确保已引入 marked 库
1375
- const htmlContent = marked ? marked(text) : text;
1376
- return `
1377
- <div class="markdown-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
1378
- ${htmlContent}
1379
- </div>
1380
- `;
1381
- }
1382
-
1383
- // 支持 HTML 预览
1384
- if (extension === 'html' || mimeType.includes('html')) {
1385
- return `
1386
- <iframe src="${url}" sandbox="allow-same-origin allow-scripts"
1387
- style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
1388
- </iframe>
1389
- `;
1390
- }
1391
-
1392
- if (mimeType.includes('text/') || mimeType.includes('application/json')) {
1393
- const text = await (await fetch(url)).text();
1394
- return `
1395
- <div class="text-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
1396
- <pre><code>${this.escapeHtml(text)}</code></pre>
1397
- </div>
1398
- `;
1399
- }
1400
-
1401
- return `
1402
- <div class="unsupported-preview">
1403
- <i class="fas fa-exclamation-circle"></i>
1404
- <p>此文件类型暂不支持预览</p>
1405
- <button class="download-btn">
1406
- <i class="fas fa-download"></i> 下载文件
1407
- </button>
1408
- </div>
1409
- `;
1410
- }
1411
-
1412
- bindPreviewEvents(modal, content, file, url) {
1413
- // 缩放功能
1414
- let currentScale = 1;
1415
- const zoomStep = 0.1;
1416
- const maxScale = 3;
1417
- const minScale = 0.5;
1418
-
1419
- const zoomIn = content.querySelector('.zoom-in');
1420
- const zoomOut = content.querySelector('.zoom-out');
1421
- const previewImage = content.querySelector('.preview-image');
1422
- const downloadBtn = content.querySelector('.preview-action-btn.download');
1423
-
1424
- if (zoomIn && zoomOut && previewImage) {
1425
- zoomIn.onclick = () => {
1426
- if (currentScale < maxScale) {
1427
- currentScale += zoomStep;
1428
- previewImage.style.transform = `scale(${currentScale})`;
1429
- }
1430
- };
1431
-
1432
- zoomOut.onclick = () => {
1433
- if (currentScale > minScale) {
1434
- currentScale -= zoomStep;
1435
- previewImage.style.transform = `scale(${currentScale})`;
1436
- }
1437
- };
1438
- }
1439
-
1440
- // 下载功能
1441
- if (downloadBtn) {
1442
- downloadBtn.onclick = () => this.downloadFile(file);
1443
- }
1444
-
1445
- // 初始化视频播放器
1446
- if (file.file_type === 'video' || file.file_type === 'audio') {
1447
- const playerElement = content.querySelector('.plyr-media');
1448
- if (playerElement && window.Plyr) {
1449
- new Plyr(playerElement);
1450
- }
1451
- }
1452
-
1453
- // 关闭预览
1454
- const closeBtn = modal.querySelector('.preview-close');
1455
- const closePreview = () => {
1456
- URL.revokeObjectURL(url);
1457
- modal.style.display = 'none';
1458
- const players = document.querySelectorAll('.plyr');
1459
- players.forEach(player => {
1460
- if (player.plyr) {
1461
- player.plyr.destroy();
1462
- }
1463
- });
1464
- };
1465
- closeBtn.onclick = closePreview;
1466
- }
1467
- // 文件下载
1468
- async downloadFile(file) {
1469
- try {
1470
- const uploadProgress = document.querySelector('.upload-progress');
1471
- const progressItem = document.createElement('div');
1472
- progressItem.className = 'progress-item';
1473
- progressItem.innerHTML = `
1474
- <div class="file-info">
1475
- <span class="filename">${file.path.split('/').pop()}</span>
1476
- <button class="cancel-download">
1477
- <i class="fas fa-times"></i> 取消
1478
- </button>
1479
- </div>
1480
- <div class="progress-bar">
1481
- <div class="progress-fill"></div>
1482
- </div>
1483
- <div class="download-stats">
1484
- <div class="stats-row">
1485
- <span class="progress-text">0%</span>
1486
- <span class="downloaded-size">0 MB / 0 MB</span>
1487
- </div>
1488
- <div class="stats-row">
1489
- <span class="speed">等待开始...</span>
1490
- </div>
1491
- </div>
1492
- `;
1493
- uploadProgress.style.display = 'block';
1494
- uploadProgress.appendChild(progressItem);
1495
- const progressFill = progressItem.querySelector('.progress-fill');
1496
- const progressText = progressItem.querySelector('.progress-text');
1497
- const speedElement = progressItem.querySelector('.speed');
1498
- const sizeElement = progressItem.querySelector('.downloaded-size');
1499
- const cancelButton = progressItem.querySelector('.cancel-download');
1500
- const controller = new AbortController();
1501
- let isDownloadCancelled = false;
1502
- cancelButton.onclick = () => {
1503
- controller.abort();
1504
- isDownloadCancelled = true;
1505
- progressItem.remove();
1506
- if (!uploadProgress.hasChildNodes()) {
1507
- uploadProgress.style.display = 'none';
1508
- }
1509
- };
1510
- const response = await fetch(`/api/files/download/${file.path}`, {
1511
- signal: controller.signal
1512
- });
1513
- if (!response.ok) throw new Error('Download failed');
1514
- const contentLength = response.headers.get('content-length');
1515
- const total = parseInt(contentLength, 10);
1516
- const reader = response.body.getReader();
1517
-
1518
- let receivedLength = 0;
1519
- let lastTime = Date.now();
1520
- let lastReceived = 0;
1521
- let currentSpeed = 0;
1522
- let lastSpeedUpdate = Date.now();
1523
- const chunks = [];
1524
- while (true) {
1525
- const {done, value} = await reader.read();
1526
-
1527
- if (done || isDownloadCancelled) break;
1528
-
1529
- chunks.push(value);
1530
- receivedLength += value.length;
1531
-
1532
- const percent = (receivedLength / total) * 100;
1533
- progressFill.style.width = `${percent.toFixed(1)}%`;
1534
- progressText.textContent = `${percent.toFixed(1)}%`;
1535
-
1536
- const now = Date.now();
1537
- if (now - lastSpeedUpdate >= 1000) {
1538
- const timeElapsed = (now - lastTime) / 1000;
1539
- const receivedSinceLastTime = receivedLength - lastReceived;
1540
-
1541
- if (timeElapsed > 0) {
1542
- currentSpeed = receivedSinceLastTime / timeElapsed;
1543
- if (currentSpeed > 0) {
1544
- speedElement.textContent = `${this.formatFileSize(currentSpeed)}/s`;
1545
- }
1546
- }
1547
-
1548
- lastTime = now;
1549
- lastReceived = receivedLength;
1550
- lastSpeedUpdate = now;
1551
- }
1552
-
1553
- sizeElement.textContent = `${this.formatFileSize(receivedLength)} / ${this.formatFileSize(total)}`;
1554
- }
1555
- if (!isDownloadCancelled) {
1556
- const blob = new Blob(chunks);
1557
- const url = URL.createObjectURL(blob);
1558
- const a = document.createElement('a');
1559
- a.href = url;
1560
- a.download = file.path.split('/').pop();
1561
- document.body.appendChild(a);
1562
- a.click();
1563
- document.body.removeChild(a);
1564
- URL.revokeObjectURL(url);
1565
- progressItem.remove();
1566
- if (!uploadProgress.hasChildNodes()) {
1567
- uploadProgress.style.display = 'none';
1568
- }
1569
- }
1570
- } catch (error) {
1571
- const uploadProgress = document.querySelector('.upload-progress');
1572
- if (error.name === 'AbortError') {
1573
- this.showMessage('下载已取消');
1574
- } else {
1575
- console.error('Error downloading file:', error);
1576
- this.showError('下载文件失败');
1577
- }
1578
- if (!uploadProgress.hasChildNodes()) {
1579
- uploadProgress.style.display = 'none';
1580
- }
1581
- }
1582
- }
1583
- // 文件上传处理
1584
- async handleFileUpload(files) {
1585
- const uploadProgress = document.querySelector('.upload-progress');
1586
- uploadProgress.style.display = 'block';
1587
- uploadProgress.innerHTML = ''; // 清除之前的进度条
1588
-
1589
- let hasSuccessfulUpload = false; // 跟踪是否有文件上传成功
1590
-
1591
- for (const file of files) {
1592
- try {
1593
- const formData = new FormData();
1594
- formData.append('file', file);
1595
- formData.append('path', this.currentPath);
1596
-
1597
- const xhr = new XMLHttpRequest();
1598
- const startTime = Date.now();
1599
- let lastLoaded = 0;
1600
- let lastTime = startTime;
1601
-
1602
- // 创建进度条元素
1603
- const progressItem = document.createElement('div');
1604
- progressItem.className = 'progress-item';
1605
- progressItem.innerHTML = `
1606
- <div class="file-info">
1607
- <span class="filename">${file.name}</span>
1608
- <button class="cancel-upload">取消</button>
1609
- </div>
1610
- <div class="progress-bar">
1611
- <div class="progress-fill"></div>
1612
- </div>
1613
- <div class="upload-stats">
1614
- <span class="speed">0 KB/s</span>
1615
- <span class="time-remaining">计算中...</span>
1616
- </div>
1617
- `;
1618
- uploadProgress.appendChild(progressItem);
1619
-
1620
- const progressFill = progressItem.querySelector('.progress-fill');
1621
- const speedElement = progressItem.querySelector('.speed');
1622
- const timeElement = progressItem.querySelector('.time-remaining');
1623
- const cancelButton = progressItem.querySelector('.cancel-upload');
1624
-
1625
- // 处理取消上传
1626
- cancelButton.addEventListener('click', () => {
1627
- xhr.abort();
1628
- progressItem.remove();
1629
- if (uploadProgress.children.length === 0) {
1630
- uploadProgress.style.display = 'none';
1631
- }
1632
- });
1633
-
1634
- // 处理上传进度
1635
- xhr.upload.addEventListener('progress', (e) => {
1636
- if (e.lengthComputable) {
1637
- const percent = (e.loaded / e.total) * 100;
1638
- progressFill.style.width = `${percent}%`;
1639
-
1640
- // 计算上传速度
1641
- const currentTime = Date.now();
1642
- const timeElapsed = (currentTime - lastTime) / 1000; // 秒
1643
- const loaded = e.loaded - lastLoaded;
1644
- const speed = loaded / timeElapsed; // 字节每秒
1645
-
1646
- // 计算剩余时间
1647
- const remaining = (e.total - e.loaded) / speed;
1648
- const minutes = Math.floor(remaining / 60);
1649
- const seconds = Math.floor(remaining % 60);
1650
-
1651
- // 更新UI
1652
- speedElement.textContent = `${this.formatFileSize(speed)}/s`;
1653
- timeElement.textContent = `预计剩余 ${minutes}分${seconds}秒`;
1654
-
1655
- lastLoaded = e.loaded;
1656
- lastTime = currentTime;
1657
- }
1658
- });
1659
-
1660
- // 执行上传请求
1661
- await new Promise((resolve, reject) => {
1662
- xhr.onload = async () => {
1663
- try {
1664
- const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
1665
-
1666
- if (xhr.status === 200 && response.success) {
1667
- this.showMessage(`文件 ${file.name} 上传成功`);
1668
- hasSuccessfulUpload = true; // 标记上传成功
1669
- resolve();
1670
- } else {
1671
- const errorMessage = response.error || '上传失败';
1672
- reject(new Error(errorMessage));
1673
- }
1674
- } catch (e) {
1675
- reject(new Error('服务器响应格式错误'));
1676
- }
1677
- };
1678
-
1679
- xhr.onerror = () => reject(new Error('网络错误'));
1680
- xhr.onabort = () => reject(new Error('Upload cancelled'));
1681
-
1682
- xhr.open('POST', '/api/files/upload');
1683
- xhr.send(formData);
1684
- });
1685
-
1686
- // 上传完成后移除进度条
1687
- progressItem.remove();
1688
- if (uploadProgress.children.length === 0) {
1689
- uploadProgress.style.display = 'none';
1690
- }
1691
-
1692
- } catch (error) {
1693
- if (error.message !== 'Upload cancelled') {
1694
- this.showError(`上传文件 ${file.name} 失败`);
1695
- }
1696
- }
1697
- }
1698
-
1699
- // 所有上传完成后,如果有文件上传成功则刷新文件列表
1700
- if (hasSuccessfulUpload) {
1701
- await this.loadFiles();
1702
- }
1703
- }
1704
- // 拖拽上传初始化
1705
- initDragAndDrop() {
1706
- const dragOverlay = document.querySelector('.drag-overlay');
1707
- const container = document.querySelector('.container');
1708
-
1709
- container.addEventListener('dragover', (e) => {
1710
- e.preventDefault();
1711
- dragOverlay.style.display = 'flex';
1712
- });
1713
-
1714
- container.addEventListener('dragleave', (e) => {
1715
- if (e.relatedTarget === null) {
1716
- dragOverlay.style.display = 'none';
1717
- }
1718
- });
1719
-
1720
- container.addEventListener('drop', (e) => {
1721
- e.preventDefault();
1722
- dragOverlay.style.display = 'none';
1723
-
1724
- if (e.dataTransfer.files.length > 0) {
1725
- this.handleFileUpload(e.dataTransfer.files);
1726
- }
1727
- });
1728
- }
1729
-
1730
- // 面包屑导航更新
1731
- updateBreadcrumb() {
1732
- const breadcrumb = document.querySelector('.breadcrumb');
1733
- const paths = this.currentPath.split('/').filter(Boolean);
1734
-
1735
- breadcrumb.innerHTML = '<span class="breadcrumb-item" data-path="/">根目录</span>';
1736
-
1737
- let currentPath = '';
1738
- paths.forEach(path => {
1739
- currentPath += `/${path}`;
1740
- breadcrumb.innerHTML += `
1741
- <span class="breadcrumb-separator">/</span>
1742
- <span class="breadcrumb-item" data-path="${currentPath}">${decodeURIComponent(path)}</span>
1743
- `;
1744
- });
1745
-
1746
- breadcrumb.querySelectorAll('.breadcrumb-item').forEach(item => {
1747
- item.addEventListener('click', () => {
1748
- this.currentPath = item.dataset.path;
1749
- this.loadFiles();
1750
- });
1751
- });
1752
- }
1753
-
1754
- // 视图切换
1755
- switchView(view) {
1756
- const buttons = document.querySelectorAll('.view-btn');
1757
- buttons.forEach(btn => {
1758
- btn.classList.toggle('active', btn.dataset.view === view);
1759
- });
1760
-
1761
- this.currentView = view;
1762
- this.renderFiles();
1763
- }
1764
-
1765
- // 文件类型筛选
1766
- filterByType(type) {
1767
- const items = document.querySelectorAll('.nav-item');
1768
- items.forEach(item => {
1769
- item.classList.toggle('active', item.dataset.type === type);
1770
- });
1771
-
1772
- this.currentFileType = type;
1773
- this.renderFiles();
1774
- }
1775
-
1776
- // 搜索处理
1777
- async handleSearch(keyword) {
1778
- if (!keyword) {
1779
- await this.loadFiles();
1780
- return;
1781
- }
1782
-
1783
- try {
1784
- const response = await fetch(`/api/files/search?keyword=${encodeURIComponent(keyword)}`);
1785
- if (!response.ok) throw new Error('Search failed');
1786
-
1787
- const searchResults = await response.json();
1788
- // Transform the MySQL search results to match the file list format
1789
- this.files = searchResults.map(file => ({
1790
- type: 'file',
1791
- path: file.path,
1792
- size: parseInt(file.size), // Convert size string to number
1793
- file_type: file.type,
1794
- size_formatted: file.size,
1795
- preview_url: `/api/files/preview/${file.path}`,
1796
- download_url: `/api/files/download/${file.path}`,
1797
- created_at: file.created_at
1798
- }));
1799
-
1800
- // Update the breadcrumb to show we're in search mode
1801
- const breadcrumb = document.querySelector('.breadcrumb');
1802
- breadcrumb.innerHTML = `
1803
- <span class="breadcrumb-item" data-path="/">根目录</span>
1804
- <span class="breadcrumb-separator">/</span>
1805
- <span class="breadcrumb-item">搜索结果: "${keyword}"</span>
1806
- `;
1807
-
1808
- this.renderFiles();
1809
-
1810
- // Show result count
1811
- this.showMessage(`找到 ${this.files.length} 个匹配的文件`);
1812
-
1813
- } catch (error) {
1814
- console.error('Error searching files:', error);
1815
- this.showError('搜索失败');
1816
- }
1817
- }
1818
-
1819
- // 辅助方法
1820
- getFileIcon(type, fileType) {
1821
- const icons = {
1822
- directory: 'fas fa-folder',
1823
- image: 'fas fa-file-image',
1824
- video: 'fas fa-file-video',
1825
- document: 'fas fa-file-alt',
1826
- audio: 'fas fa-file-audio',
1827
- archive: 'fas fa-file-archive',
1828
- code: 'fas fa-file-code',
1829
- other: 'fas fa-file'
1830
- };
1831
-
1832
- if (type === 'directory') return icons.directory;
1833
- return icons[fileType] || icons.other;
1834
- }
1835
-
1836
- formatFileSize(bytes) {
1837
- if (!bytes) return '0 B';
1838
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1839
- let size = bytes;
1840
- let unitIndex = 0;
1841
-
1842
- while (size >= 1024 && unitIndex < units.length - 1) {
1843
- size /= 1024;
1844
- unitIndex++;
1845
- }
1846
-
1847
- return `${size.toFixed(2)} ${units[unitIndex]}`;
1848
- }
1849
-
1850
- debounce(func, wait) {
1851
- let timeout;
1852
- return function executedFunction(...args) {
1853
- const later = () => {
1854
- clearTimeout(timeout);
1855
- func(...args);
1856
- };
1857
- clearTimeout(timeout);
1858
- timeout = setTimeout(later, wait);
1859
- };
1860
- }
1861
-
1862
- showError(message) {
1863
- // 可以根据需要实现错误提示UI
1864
- alert(message);
1865
- }
1866
- async deleteFile(file) {
1867
- try {
1868
- const confirmed = await this.showConfirmDialog(
1869
- '确认删除',
1870
- `确定要删除文件 "${file.path.split('/').pop()}" 吗?此操作不可恢复。`
1871
- );
1872
-
1873
- if (!confirmed) return;
1874
-
1875
- const response = await fetch(`/api/files/delete/${encodeURIComponent(file.path)}`, {
1876
- method: 'DELETE',
1877
- headers: {
1878
- 'Content-Type': 'application/json'
1879
- }
1880
- });
1881
-
1882
- if (!response.ok) {
1883
- const errorData = await response.json();
1884
- throw new Error(errorData.error || '删除失败');
1885
- }
1886
-
1887
- // Only proceed with refresh and success message if deletion was successful
1888
- await this.loadFiles();
1889
- this.showMessage(`文件 "${file.path.split('/').pop()}" 已成功删除`);
1890
-
1891
- } catch (error) {
1892
- console.error('Error deleting file:', error);
1893
- this.showError('删除文件失败');
1894
- }
1895
- }
1896
-
1897
- // 添加确认对话框的实现
1898
- showConfirmDialog(title, message) {
1899
- return new Promise((resolve) => {
1900
- const modal = document.createElement('div');
1901
- modal.className = 'confirm-modal';
1902
- modal.innerHTML = `
1903
- <div class="confirm-content">
1904
- <h3>${title}</h3>
1905
- <p>${message}</p>
1906
- <div class="confirm-buttons">
1907
- <button class="confirm-cancel">取消</button>
1908
- <button class="confirm-ok">确定</button>
1909
- </div>
1910
- </div>
1911
- `;
1912
-
1913
- document.body.appendChild(modal);
1914
-
1915
- const handleConfirm = (confirmed) => {
1916
- modal.remove();
1917
- resolve(confirmed);
1918
- };
1919
-
1920
- modal.querySelector('.confirm-cancel').addEventListener('click', () => handleConfirm(false));
1921
- modal.querySelector('.confirm-ok').addEventListener('click', () => handleConfirm(true));
1922
- });
1923
- }
1924
-
1925
- // 添加提示消息的实现
1926
- showMessage(message) {
1927
- const toast = document.createElement('div');
1928
- toast.className = 'toast-message';
1929
- toast.textContent = message;
1930
-
1931
- document.body.appendChild(toast);
1932
-
1933
- setTimeout(() => {
1934
- toast.classList.add('show');
1935
- setTimeout(() => {
1936
- toast.classList.remove('show');
1937
- setTimeout(() => toast.remove(), 300);
1938
- }, 2000);
1939
- }, 100);
1940
- }
1941
-
1942
- // 添加多选模式切换按钮
1943
- addMultiSelectButton() {
1944
- const multiSelectBtn = document.createElement('button');
1945
- multiSelectBtn.className = 'multi-select-btn';
1946
- multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i> 多选';
1947
- multiSelectBtn.onclick = () => this.toggleMultiSelectMode();
1948
-
1949
- document.querySelector('.view-toggle').appendChild(multiSelectBtn);
1950
- }
1951
-
1952
- // 切换多选模式
1953
- toggleMultiSelectMode() {
1954
- this.isMultiSelectMode = !this.isMultiSelectMode;
1955
- this.selectedFiles.clear();
1956
-
1957
- // 更新按钮状态
1958
- const multiSelectBtn = document.querySelector('.multi-select-btn');
1959
- multiSelectBtn.classList.toggle('active');
1960
-
1961
- // 更新按钮文本
1962
- if (this.isMultiSelectMode) {
1963
- // 显示批量操作按钮
1964
- this.showBatchOperations();
1965
- multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>退出多选</span>';
1966
- } else {
1967
- // 隐藏批量操作按钮
1968
- this.hideBatchOperations();
1969
- multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>多选</span>';
1970
- }
1971
-
1972
- this.renderFiles();
1973
- }
1974
- showBatchOperations() {
1975
- const batchOpsContainer = document.createElement('div');
1976
- batchOpsContainer.className = 'batch-operations';
1977
- batchOpsContainer.innerHTML = `
1978
- <button class="action-btn batch-download-btn">
1979
- <i class="fas fa-download"></i><span>批量下载</span>
1980
- </button>
1981
- <button class="action-btn batch-delete-btn">
1982
- <i class="fas fa-trash"></i><span>批量删除</span>
1983
- </button>
1984
- `;
1985
-
1986
- document.querySelector('.view-toggle').appendChild(batchOpsContainer);
1987
-
1988
- // 绑定事件
1989
- batchOpsContainer.querySelector('.batch-download-btn').onclick = () => this.batchDownload();
1990
- batchOpsContainer.querySelector('.batch-delete-btn').onclick = () => this.batchDelete();
1991
- }
1992
-
1993
- hideBatchOperations() {
1994
- const batchOps = document.querySelector('.batch-operations');
1995
- if (batchOps) {
1996
- batchOps.remove();
1997
- }
1998
- }
1999
- // 批量下载
2000
- async batchDownload() {
2001
- for (const file of this.selectedFiles) {
2002
- await this.downloadFile(file);
2003
- }
2004
- }
2005
-
2006
- // 批量删除
2007
- async batchDelete() {
2008
- const confirmed = await this.showConfirmDialog(
2009
- '批量删除',
2010
- `确定要删除选中的 ${this.selectedFiles.size} 个文件吗?此操作不可恢复。`
2011
- );
2012
-
2013
- if (confirmed) {
2014
- for (const file of this.selectedFiles) {
2015
- await this.deleteFile(file);
2016
- }
2017
- }
2018
- }
2019
- async createFolder(folderName) {
2020
- try {
2021
- const response = await fetch('/api/files/create_folder', {
2022
- method: 'POST',
2023
- headers: {
2024
- 'Content-Type': 'application/json'
2025
- },
2026
- body: JSON.stringify({
2027
- path: this.currentPath,
2028
- name: folderName
2029
- })
2030
- });
2031
-
2032
- if (!response.ok) {
2033
- throw new Error('Failed to create folder');
2034
- }
2035
-
2036
- await this.loadFiles();
2037
- this.showMessage('文件夹创建成功');
2038
-
2039
- } catch (error) {
2040
- console.error('Error creating folder:', error);
2041
- this.showError('创建文件夹失败');
2042
- }
2043
- }
2044
-
2045
- // 显示创建文件夹对话框
2046
- async showCreateFolderDialog() {
2047
- const modal = document.createElement('div');
2048
- modal.className = 'confirm-modal';
2049
- modal.innerHTML = `
2050
- <div class="confirm-content">
2051
- <h3>新建文件夹</h3>
2052
- <div class="input-container">
2053
- <input type="text"
2054
- class="folder-name-input"
2055
- placeholder="请输入文件夹名称"
2056
- maxlength="255">
2057
- </div>
2058
- <div class="confirm-buttons">
2059
- <button class="confirm-cancel">取消</button>
2060
- <button class="confirm-ok">创建</button>
2061
- </div>
2062
- </div>
2063
- `;
2064
-
2065
- document.body.appendChild(modal);
2066
- const input = modal.querySelector('.folder-name-input');
2067
- input.focus();
2068
-
2069
- try {
2070
- const folderName = await new Promise((resolve) => {
2071
- const handleCreateFolder = () => {
2072
- const name = input.value.trim();
2073
- if (name) {
2074
- resolve(name);
2075
- }
2076
- modal.remove();
2077
- };
2078
-
2079
- const handleCancel = () => {
2080
- resolve(null);
2081
- modal.remove();
2082
- };
2083
-
2084
- modal.querySelector('.confirm-ok').onclick = handleCreateFolder;
2085
- modal.querySelector('.confirm-cancel').onclick = handleCancel;
2086
-
2087
- input.onkeyup = (e) => {
2088
- if (e.key === 'Enter') handleCreateFolder();
2089
- if (e.key === 'Escape') handleCancel();
2090
- };
2091
- });
2092
-
2093
- if (folderName) {
2094
- await this.createFolder(folderName);
2095
- }
2096
- } catch (error) {
2097
- console.error('Error creating folder:', error);
2098
- this.showError('创建文件夹失败');
2099
- }
2100
- }
2101
-
2102
- }
2103
- async function handleLogout() {
2104
- try {
2105
- const response = await fetch('/logout');
2106
- if (response.ok) {
2107
- window.location.href = '/login';
2108
- } else {
2109
- throw new Error('Logout failed');
2110
- }
2111
- } catch (error) {
2112
- console.error('Error during logout:', error);
2113
- alert('退出登录失败,请重试');
2114
- }
2115
- }
2116
- // 初始化文件管理器
2117
- new FileManager();
2118
- </script>
2119
- </body>
2120
- </html>