lexlepty commited on
Commit
4360963
·
verified ·
1 Parent(s): d839bcc

Upload 2 files

Browse files
Files changed (2) hide show
  1. templates/index.html +1535 -0
  2. templates/login.html +138 -0
templates/index.html ADDED
@@ -0,0 +1,1535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
356
+
357
+ .logo {
358
+ display: none;
359
+ }
360
+
361
+ .nav-item {
362
+ flex: 1;
363
+ margin: 0 5px;
364
+ padding: 8px;
365
+ flex-direction: column;
366
+ font-size: 12px;
367
+ }
368
+
369
+ .nav-item i {
370
+ margin: 0 0 5px 0;
371
+ }
372
+
373
+ .main-content {
374
+ margin-left: 0;
375
+ margin-bottom: 60px;
376
+ padding-top: 90px;
377
+ }
378
+
379
+ .header {
380
+ left: 0;
381
+ }
382
+
383
+ .search-container {
384
+ margin: 0;
385
+ }
386
+
387
+ .upload-btn {
388
+ right: 20px;
389
+ bottom: 80px;
390
+ }
391
+
392
+ .file-grid {
393
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
394
+ }
395
+ }
396
+
397
+ /* 拖拽上传区域样式 */
398
+ .drag-overlay {
399
+ position: fixed;
400
+ top: 0;
401
+ left: 0;
402
+ right: 0;
403
+ bottom: 0;
404
+ background: rgba(255, 149, 128, 0.1);
405
+ border: 3px dashed var(--primary-glow);
406
+ z-index: 2000;
407
+ display: none;
408
+ align-items: center;
409
+ justify-content: center;
410
+ font-size: 24px;
411
+ color: var(--primary-glow);
412
+ }
413
+
414
+ /* 面包屑导航 */
415
+ .breadcrumb {
416
+ margin-bottom: 20px;
417
+ display: flex;
418
+ align-items: center;
419
+ gap: 8px;
420
+ font-size: 14px;
421
+ }
422
+
423
+ .breadcrumb-item {
424
+ cursor: pointer;
425
+ color: var(--text);
426
+ transition: color 0.3s ease;
427
+ }
428
+
429
+ .breadcrumb-item:hover {
430
+ color: var(--primary-glow);
431
+ }
432
+
433
+ .breadcrumb-separator {
434
+ color: var(--border-color);
435
+ }
436
+ /* 加载指示器样式 */
437
+ .loading-indicator {
438
+ display: flex;
439
+ flex-direction: column;
440
+ align-items: center;
441
+ padding: 2rem;
442
+ background: var(--card-bg);
443
+ border-radius: 15px;
444
+ }
445
+
446
+ .spinner {
447
+ width: 40px;
448
+ height: 40px;
449
+ border: 4px solid var(--border-color);
450
+ border-top-color: var(--primary-glow);
451
+ border-radius: 50%;
452
+ animation: spin 1s linear infinite;
453
+ margin-bottom: 1rem;
454
+ }
455
+
456
+ @keyframes spin {
457
+ 100% { transform: rotate(360deg); }
458
+ }
459
+
460
+ .loading-text {
461
+ color: var(--text);
462
+ font-size: 1rem;
463
+ margin-top: 1rem;
464
+ }
465
+
466
+ /* 上传进度样式 */
467
+ .upload-progress {
468
+ width: 400px;
469
+ max-width: 90vw;
470
+ }
471
+
472
+ .progress-item {
473
+ background: var(--card-bg);
474
+ border-radius: 8px;
475
+ padding: 1rem;
476
+ margin-bottom: 0.5rem;
477
+ box-shadow: 0 2px 8px var(--shadow-color);
478
+ }
479
+
480
+ .file-info {
481
+ display: flex;
482
+ justify-content: space-between;
483
+ align-items: center;
484
+ margin-bottom: 0.5rem;
485
+ }
486
+
487
+ .filename {
488
+ font-weight: 500;
489
+ max-width: 250px;
490
+ overflow: hidden;
491
+ text-overflow: ellipsis;
492
+ white-space: nowrap;
493
+ }
494
+
495
+ .cancel-upload {
496
+ background: #ff4444;
497
+ color: white;
498
+ border: none;
499
+ border-radius: 4px;
500
+ padding: 0.25rem 0.75rem;
501
+ cursor: pointer;
502
+ font-size: 0.875rem;
503
+ transition: all 0.3s ease;
504
+ }
505
+
506
+ .cancel-upload:hover {
507
+ background: #ff6666;
508
+ transform: translateY(-1px);
509
+ }
510
+
511
+ .upload-stats {
512
+ display: flex;
513
+ justify-content: space-between;
514
+ font-size: 0.875rem;
515
+ color: #666;
516
+ margin-top: 0.5rem;
517
+ }
518
+
519
+ .progress-bar {
520
+ width: 100%;
521
+ height: 6px;
522
+ background: var(--border-color);
523
+ border-radius: 3px;
524
+ overflow: hidden;
525
+ }
526
+
527
+ .progress-fill {
528
+ height: 100%;
529
+ background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
530
+ width: 0%;
531
+ transition: width 0.3s ease;
532
+ }
533
+ /* 确认对话框样式 */
534
+ .confirm-modal {
535
+ position: fixed;
536
+ top: 0;
537
+ left: 0;
538
+ right: 0;
539
+ bottom: 0;
540
+ background: rgba(0, 0, 0, 0.5);
541
+ display: flex;
542
+ align-items: center;
543
+ justify-content: center;
544
+ z-index: 3000;
545
+ }
546
+
547
+ .confirm-content {
548
+ background: var(--card-bg);
549
+ border-radius: 12px;
550
+ padding: 24px;
551
+ max-width: 400px;
552
+ width: 90%;
553
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
554
+ }
555
+
556
+ .confirm-content h3 {
557
+ margin-bottom: 16px;
558
+ color: var(--text);
559
+ }
560
+
561
+ .confirm-content p {
562
+ margin-bottom: 24px;
563
+ color: #666;
564
+ line-height: 1.5;
565
+ }
566
+
567
+ .confirm-buttons {
568
+ display: flex;
569
+ justify-content: flex-end;
570
+ gap: 12px;
571
+ }
572
+
573
+ .confirm-buttons button {
574
+ padding: 8px 20px;
575
+ border-radius: 6px;
576
+ border: none;
577
+ cursor: pointer;
578
+ transition: all 0.3s ease;
579
+ }
580
+
581
+ .confirm-cancel {
582
+ background: #f0f0f0;
583
+ color: #666;
584
+ }
585
+
586
+ .confirm-ok {
587
+ background: #ff4444;
588
+ color: white;
589
+ }
590
+
591
+ .confirm-buttons button:hover {
592
+ transform: translateY(-1px);
593
+ }
594
+
595
+ /* 提示消息样式 */
596
+ .toast-message {
597
+ position: fixed;
598
+ bottom: 24px;
599
+ left: 50%;
600
+ transform: translateX(-50%) translateY(100px);
601
+ background: rgba(0, 0, 0, 0.8);
602
+ color: white;
603
+ padding: 12px 24px;
604
+ border-radius: 6px;
605
+ font-size: 14px;
606
+ opacity: 0;
607
+ transition: all 0.3s ease;
608
+ }
609
+
610
+ .toast-message.show {
611
+ transform: translateX(-50%) translateY(0);
612
+ opacity: 1;
613
+ }
614
+ .preview-modal {
615
+ position: fixed;
616
+ top: 0;
617
+ left: 0;
618
+ right: 0;
619
+ bottom: 0;
620
+ background: rgba(0, 0, 0, 0.85);
621
+ display: none;
622
+ z-index: 2000;
623
+ }
624
+ .preview-content {
625
+ max-width: 90%;
626
+ max-height: 90%;
627
+ position: relative;
628
+ background: #fff;
629
+ border-radius: 12px;
630
+ overflow: hidden;
631
+ display: flex;
632
+ flex-direction: column;
633
+ }
634
+
635
+ .preview-container {
636
+ position: relative;
637
+ width: 100%;
638
+ height: 100%;
639
+ display: flex;
640
+ align-items: center;
641
+ justify-content: center;
642
+ }
643
+
644
+ .preview-header {
645
+ padding: 16px;
646
+ background: #f8f9fa;
647
+ border-bottom: 1px solid #e9ecef;
648
+ display: flex;
649
+ justify-content: space-between;
650
+ align-items: center;
651
+ }
652
+
653
+ .preview-body {
654
+ flex: 1;
655
+ overflow: auto;
656
+ padding: 24px;
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: center;
660
+ }
661
+ .preview-image-container {
662
+ overflow: hidden;
663
+ display: flex;
664
+ align-items: center;
665
+ justify-content: center;
666
+ }
667
+
668
+ .preview-image {
669
+ max-width: 100%;
670
+ max-height: 100%;
671
+ object-fit: contain;
672
+ transition: transform 0.3s ease;
673
+ }
674
+
675
+ .text-preview,
676
+ .markdown-preview {
677
+ background: white;
678
+ padding: 20px;
679
+ overflow: auto;
680
+ font-size: 14px;
681
+ line-height: 1.6;
682
+ }
683
+
684
+ .markdown-preview {
685
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
686
+ }
687
+
688
+ .preview-action-btn {
689
+ padding: 8px;
690
+ margin-left: 8px;
691
+ border: none;
692
+ background: none;
693
+ color: #666;
694
+ cursor: pointer;
695
+ transition: all 0.3s ease;
696
+ }
697
+
698
+ .preview-action-btn:hover {
699
+ color: #000;
700
+ background: #e9ecef;
701
+ border-radius: 4px;
702
+ }
703
+ .preview-close {
704
+ position: absolute;
705
+ top: 20px;
706
+ right: 20px;
707
+ width: 40px;
708
+ height: 40px;
709
+ border-radius: 50%;
710
+ background: rgba(255, 255, 255, 0.2);
711
+ border: none;
712
+ color: white;
713
+ cursor: pointer;
714
+ display: flex;
715
+ align-items: center;
716
+ justify-content: center;
717
+ transition: all 0.3s ease;
718
+ }
719
+ </style>
720
+ </head>
721
+ <body>
722
+ <div class="container">
723
+ <!-- 侧边栏 -->
724
+ <nav class="sidebar">
725
+ <div class="logo">
726
+ <i class="fas fa-cloud"></i> Cloud Vault
727
+ </div>
728
+ <div class="nav-item active" data-type="all">
729
+ <i class="fas fa-folder"></i>
730
+ <span class="nav-item-text">全部文件</span>
731
+ </div>
732
+ <div class="nav-item" data-type="image">
733
+ <i class="fas fa-image"></i>
734
+ <span class="nav-item-text">图片</span>
735
+ </div>
736
+ <div class="nav-item" data-type="video">
737
+ <i class="fas fa-video"></i>
738
+ <span class="nav-item-text">视频</span>
739
+ </div>
740
+ <div class="nav-item" data-type="document">
741
+ <i class="fas fa-file-alt"></i>
742
+ <span class="nav-item-text">文档</span>
743
+ </div>
744
+ <div class="nav-item" data-type="audio">
745
+ <i class="fas fa-music"></i>
746
+ <span class="nav-item-text">音频</span>
747
+ </div>
748
+ <div class="nav-item" data-type="archive">
749
+ <i class="fas fa-file-archive"></i>
750
+ <span class="nav-item-text">压缩包</span>
751
+ </div>
752
+ </nav>
753
+
754
+ <!-- 顶部搜索栏 -->
755
+ <header class="header">
756
+ <div class="search-container">
757
+ <input type="text" class="search-box" placeholder="搜索文件...">
758
+ </div>
759
+ </header>
760
+
761
+ <!-- 主内容区 -->
762
+ <main class="main-content">
763
+ <!-- 视图切换按钮 -->
764
+ <div class="view-toggle">
765
+ <button class="view-btn active" data-view="grid">
766
+ <i class="fas fa-th"></i>
767
+ </button>
768
+ <button class="view-btn" data-view="list">
769
+ <i class="fas fa-list"></i>
770
+ </button>
771
+ </div>
772
+
773
+ <!-- 面包屑导航 -->
774
+ <div class="breadcrumb">
775
+ <span class="breadcrumb-item" data-path="/">根目录</span>
776
+ </div>
777
+
778
+ <!-- 文件容器 -->
779
+ <div class="file-container">
780
+ <!-- 文件内容将通过 JavaScript 动态生成 -->
781
+ </div>
782
+ </main>
783
+
784
+ <!-- 上传按钮 -->
785
+ <div class="upload-btn" id="uploadBtn">
786
+ <i class="fas fa-plus"></i>
787
+ <input type="file" id="fileInput" style="display: none;" multiple>
788
+ </div>
789
+
790
+ <!-- 上传进度条 -->
791
+ <div class="upload-progress">
792
+ <div class="progress-bar">
793
+ <div class="progress-fill"></div>
794
+ </div>
795
+ </div>
796
+ </div>
797
+
798
+ <!-- 拖拽上传遮罩 -->
799
+ <div class="drag-overlay">
800
+ <div>释放鼠标上传文件</div>
801
+ </div>
802
+
803
+ <!-- 文件操作菜单 -->
804
+ <div class="file-menu" style="display: none;">
805
+ <div class="file-menu-item" data-action="preview">
806
+ <i class="fas fa-eye"></i> 预览
807
+ </div>
808
+ <div class="file-menu-item" data-action="download">
809
+ <i class="fas fa-download"></i> 下载
810
+ </div>
811
+ <div class="file-menu-item" data-action="delete">
812
+ <i class="fas fa-trash"></i> 删除
813
+ </div>
814
+ </div>
815
+ <!-- 预览模态框 -->
816
+ <div class="preview-modal">
817
+ <div class="preview-container">
818
+ <div class="preview-content">
819
+ <!-- 预览内容将通过 JavaScript 动态生成 -->
820
+ </div>
821
+ <button class="preview-close">
822
+ <i class="fas fa-times"></i>
823
+ </button>
824
+ </div>
825
+ </div>
826
+ <script>
827
+ // 文件管理类
828
+ class FileManager {
829
+ constructor() {
830
+ this.currentPath = '/';
831
+ this.currentView = 'grid';
832
+ this.currentFileType = 'all';
833
+ this.files = [];
834
+ this.initEventListeners();
835
+ this.loadFiles();
836
+ }
837
+
838
+ // 初始化事件监听
839
+ initEventListeners() {
840
+ // 视图切换
841
+ document.querySelectorAll('.view-btn').forEach(btn => {
842
+ btn.addEventListener('click', () => this.switchView(btn.dataset.view));
843
+ });
844
+
845
+ // 文件类型筛选
846
+ document.querySelectorAll('.nav-item').forEach(item => {
847
+ item.addEventListener('click', () => this.filterByType(item.dataset.type));
848
+ });
849
+
850
+ // 搜索
851
+ const searchBox = document.querySelector('.search-box');
852
+ searchBox.addEventListener('input', this.debounce((e) => this.handleSearch(e.target.value), 300));
853
+
854
+ // 文件上传
855
+ const uploadBtn = document.getElementById('uploadBtn');
856
+ const fileInput = document.getElementById('fileInput');
857
+
858
+ uploadBtn.addEventListener('click', () => fileInput.click());
859
+ fileInput.addEventListener('change', (e) => this.handleFileUpload(e.target.files));
860
+
861
+ // 拖拽上传
862
+ this.initDragAndDrop();
863
+ }
864
+
865
+ // 加载文件列表
866
+ async loadFiles() {
867
+ try {
868
+ const path = this.currentPath === '/' ? '' : this.currentPath;
869
+ const response = await fetch(`/api/files/list/${path}`);
870
+ if (!response.ok) throw new Error('Failed to load files');
871
+
872
+ this.files = await response.json();
873
+ this.renderFiles();
874
+ this.updateBreadcrumb();
875
+ } catch (error) {
876
+ console.error('Error loading files:', error);
877
+ this.showError('加载文件失败');
878
+ }
879
+ }
880
+
881
+ // 渲染文件列表
882
+ renderFiles() {
883
+ const container = document.querySelector('.file-container');
884
+ container.innerHTML = '';
885
+
886
+ const viewClass = this.currentView === 'grid' ? 'file-grid' : 'file-list';
887
+ container.className = `file-container ${viewClass}`;
888
+
889
+ let filteredFiles = this.files;
890
+ if (this.currentFileType !== 'all') {
891
+ filteredFiles = this.files.filter(file => file.file_type === this.currentFileType);
892
+ }
893
+
894
+ filteredFiles.forEach(file => {
895
+ const fileElement = this.createFileElement(file);
896
+ container.appendChild(fileElement);
897
+ });
898
+ }
899
+
900
+ // 创建文件元素
901
+ createFileElement(file) {
902
+ const element = document.createElement('div');
903
+ element.className = `file-item ${this.currentView}`;
904
+
905
+ const icon = this.getFileIcon(file.type, file.file_type);
906
+ const size = this.formatFileSize(file.size);
907
+
908
+ if (this.currentView === 'grid') {
909
+ element.innerHTML = `
910
+ <i class="${icon} file-icon"></i>
911
+ <div class="file-name">${file.path.split('/').pop()}</div>
912
+ <div class="file-size">${size}</div>
913
+ `;
914
+ } else {
915
+ element.innerHTML = `
916
+ <i class="${icon} file-icon"></i>
917
+ <div class="file-info">
918
+ <div class="file-name">${file.path.split('/').pop()}</div>
919
+ <div class="file-meta">
920
+ <span>${size}</span>
921
+ <span>${file.file_type || '未知类型'}</span>
922
+ </div>
923
+ </div>
924
+ `;
925
+ }
926
+
927
+ // 添加事件监听
928
+ element.addEventListener('click', () => this.handleFileClick(file));
929
+ element.addEventListener('contextmenu', (e) => this.showFileMenu(e, file));
930
+
931
+ return element;
932
+ }
933
+
934
+ // 处理文件点击
935
+ handleFileClick(file) {
936
+ if (file.type === 'directory') {
937
+ this.currentPath = file.path;
938
+ this.loadFiles();
939
+ } else {
940
+ this.previewFile(file);
941
+ }
942
+ }
943
+
944
+ // 显示文件操作菜单
945
+ showFileMenu(e, file) {
946
+ e.preventDefault();
947
+
948
+ const menu = document.querySelector('.file-menu');
949
+ menu.style.display = 'block';
950
+ menu.style.left = `${e.pageX}px`;
951
+ menu.style.top = `${e.pageY}px`;
952
+
953
+ // 清除旧的事件监听
954
+ const menuItems = menu.querySelectorAll('.file-menu-item');
955
+ menuItems.forEach(item => {
956
+ const clone = item.cloneNode(true);
957
+ item.parentNode.replaceChild(clone, item);
958
+ });
959
+
960
+ // 添加新的事件监听
961
+ menu.querySelector('[data-action="preview"]').addEventListener('click', () => this.previewFile(file));
962
+ menu.querySelector('[data-action="download"]').addEventListener('click', () => this.downloadFile(file));
963
+ menu.querySelector('[data-action="delete"]').addEventListener('click', () => this.deleteFile(file));
964
+
965
+ // 点击其他地方关闭菜单
966
+ const closeMenu = () => {
967
+ menu.style.display = 'none';
968
+ document.removeEventListener('click', closeMenu);
969
+ };
970
+
971
+ setTimeout(() => {
972
+ document.addEventListener('click', closeMenu);
973
+ }, 0);
974
+ }
975
+
976
+ // 文件预览
977
+ async previewFile(file) {
978
+ try {
979
+ const modal = document.querySelector('.preview-modal');
980
+ const content = modal.querySelector('.preview-content');
981
+
982
+ // 计算合适的预览尺寸
983
+ const windowWidth = window.innerWidth;
984
+ const windowHeight = window.innerHeight;
985
+ const maxWidth = Math.min(windowWidth * 0.9, 1200); // 最大宽度不超过1200px
986
+ const maxHeight = windowHeight * 0.85;
987
+
988
+ modal.style.display = 'flex';
989
+ content.innerHTML = `
990
+ <div class="loading-indicator">
991
+ <div class="spinner"></div>
992
+ <div class="loading-text">正在加载预览...</div>
993
+ </div>
994
+ `;
995
+
996
+ const response = await fetch(`/api/files/preview/${file.path}`);
997
+ if (!response.ok) throw new Error('Failed to preview file');
998
+
999
+ const blob = await response.blob();
1000
+ const url = URL.createObjectURL(blob);
1001
+ const mimeType = response.headers.get('content-type') || '';
1002
+ const fileName = file.path.split('/').pop();
1003
+
1004
+ // 获取预览内容
1005
+ const previewContent = `
1006
+ <div class="preview-header">
1007
+ <div class="preview-info">
1008
+ <i class="${this.getFileIcon(file.type, file.file_type)}"></i>
1009
+ <span>${fileName}</span>
1010
+ </div>
1011
+ <div class="preview-actions">
1012
+ <button class="preview-action-btn zoom-in">
1013
+ <i class="fas fa-search-plus"></i>
1014
+ </button>
1015
+ <button class="preview-action-btn zoom-out">
1016
+ <i class="fas fa-search-minus"></i>
1017
+ </button>
1018
+ <button class="preview-action-btn download">
1019
+ <i class="fas fa-download"></i>
1020
+ </button>
1021
+ </div>
1022
+ </div>
1023
+ <div class="preview-body" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
1024
+ ${await this.getPreviewContent(file, url, mimeType, maxWidth, maxHeight)}
1025
+ </div>
1026
+ `;
1027
+
1028
+ content.innerHTML = previewContent;
1029
+
1030
+ // 绑定事件处理
1031
+ this.bindPreviewEvents(modal, content, file, url);
1032
+
1033
+ } catch (error) {
1034
+ console.error('Error previewing file:', error);
1035
+ this.showError('预览文件失败');
1036
+ }
1037
+ }
1038
+
1039
+ async getPreviewContent(file, url, mimeType, maxWidth, maxHeight) {
1040
+ const extension = file.path.split('.').pop().toLowerCase();
1041
+
1042
+ if (file.file_type === 'image' || mimeType.startsWith('image/')) {
1043
+ return `
1044
+ <div class="preview-image-container" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
1045
+ <img src="${url}" alt="${file.path}" class="preview-image">
1046
+ </div>
1047
+ `;
1048
+ }
1049
+
1050
+ if (file.file_type === 'video' || mimeType.startsWith('video/')) {
1051
+ return `
1052
+ <div class="video-container" style="max-width: ${maxWidth * 0.8}px;">
1053
+ <video class="plyr-media" controls crossorigin playsinline>
1054
+ <source src="${url}" type="${mimeType}">
1055
+ </video>
1056
+ </div>
1057
+ `;
1058
+ }
1059
+
1060
+ if (file.file_type === 'audio' || mimeType.startsWith('audio/')) {
1061
+ return `
1062
+ <div class="audio-container" style="width: ${maxWidth * 0.6}px;">
1063
+ <audio class="plyr-media" controls>
1064
+ <source src="${url}" type="${mimeType}">
1065
+ </audio>
1066
+ </div>
1067
+ `;
1068
+ }
1069
+
1070
+ if (mimeType.includes('pdf')) {
1071
+ return `
1072
+ <iframe src="${url}#view=FitH" type="application/pdf"
1073
+ style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
1074
+ </iframe>
1075
+ `;
1076
+ }
1077
+
1078
+ // 支持 Markdown 预览
1079
+ if (extension === 'md') {
1080
+ const text = await (await fetch(url)).text();
1081
+ const marked = window.marked; // 确保已引入 marked 库
1082
+ const htmlContent = marked ? marked(text) : text;
1083
+ return `
1084
+ <div class="markdown-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
1085
+ ${htmlContent}
1086
+ </div>
1087
+ `;
1088
+ }
1089
+
1090
+ // 支持 HTML 预览
1091
+ if (extension === 'html' || mimeType.includes('html')) {
1092
+ return `
1093
+ <iframe src="${url}" sandbox="allow-same-origin allow-scripts"
1094
+ style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
1095
+ </iframe>
1096
+ `;
1097
+ }
1098
+
1099
+ if (mimeType.includes('text/') || mimeType.includes('application/json')) {
1100
+ const text = await (await fetch(url)).text();
1101
+ return `
1102
+ <div class="text-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
1103
+ <pre><code>${this.escapeHtml(text)}</code></pre>
1104
+ </div>
1105
+ `;
1106
+ }
1107
+
1108
+ return `
1109
+ <div class="unsupported-preview">
1110
+ <i class="fas fa-exclamation-circle"></i>
1111
+ <p>此文件类型暂不支持预览</p>
1112
+ <button class="download-btn">
1113
+ <i class="fas fa-download"></i> 下载文件
1114
+ </button>
1115
+ </div>
1116
+ `;
1117
+ }
1118
+
1119
+ bindPreviewEvents(modal, content, file, url) {
1120
+ // 缩放功能
1121
+ let currentScale = 1;
1122
+ const zoomStep = 0.1;
1123
+ const maxScale = 3;
1124
+ const minScale = 0.5;
1125
+
1126
+ const zoomIn = content.querySelector('.zoom-in');
1127
+ const zoomOut = content.querySelector('.zoom-out');
1128
+ const previewImage = content.querySelector('.preview-image');
1129
+ const downloadBtn = content.querySelector('.preview-action-btn.download');
1130
+
1131
+ if (zoomIn && zoomOut && previewImage) {
1132
+ zoomIn.onclick = () => {
1133
+ if (currentScale < maxScale) {
1134
+ currentScale += zoomStep;
1135
+ previewImage.style.transform = `scale(${currentScale})`;
1136
+ }
1137
+ };
1138
+
1139
+ zoomOut.onclick = () => {
1140
+ if (currentScale > minScale) {
1141
+ currentScale -= zoomStep;
1142
+ previewImage.style.transform = `scale(${currentScale})`;
1143
+ }
1144
+ };
1145
+ }
1146
+
1147
+ // 下载功能
1148
+ if (downloadBtn) {
1149
+ downloadBtn.onclick = () => this.downloadFile(file);
1150
+ }
1151
+
1152
+ // 初始化视频播放器
1153
+ if (file.file_type === 'video' || file.file_type === 'audio') {
1154
+ const playerElement = content.querySelector('.plyr-media');
1155
+ if (playerElement && window.Plyr) {
1156
+ new Plyr(playerElement);
1157
+ }
1158
+ }
1159
+
1160
+ // 关闭预览
1161
+ const closeBtn = modal.querySelector('.preview-close');
1162
+ const closePreview = () => {
1163
+ URL.revokeObjectURL(url);
1164
+ modal.style.display = 'none';
1165
+ const players = document.querySelectorAll('.plyr');
1166
+ players.forEach(player => {
1167
+ if (player.plyr) {
1168
+ player.plyr.destroy();
1169
+ }
1170
+ });
1171
+ };
1172
+ closeBtn.onclick = closePreview;
1173
+ }
1174
+ // 文件下载
1175
+ async downloadFile(file) {
1176
+ try {
1177
+ const response = await fetch(`/api/files/download/${file.path}`);
1178
+ if (!response.ok) throw new Error('Download failed');
1179
+
1180
+ const blob = await response.blob();
1181
+ const url = URL.createObjectURL(blob);
1182
+ const a = document.createElement('a');
1183
+ a.href = url;
1184
+ a.download = file.path.split('/').pop();
1185
+ document.body.appendChild(a);
1186
+ a.click();
1187
+ document.body.removeChild(a);
1188
+ URL.revokeObjectURL(url);
1189
+ } catch (error) {
1190
+ console.error('Error downloading file:', error);
1191
+ this.showError('下载文件失败');
1192
+ }
1193
+ }
1194
+
1195
+ // 文件上传处理
1196
+ async handleFileUpload(files) {
1197
+ const uploadProgress = document.querySelector('.upload-progress');
1198
+ uploadProgress.style.display = 'block';
1199
+ uploadProgress.innerHTML = ''; // 清除之前的进度条
1200
+
1201
+ for (const file of files) {
1202
+ try {
1203
+ const formData = new FormData();
1204
+ formData.append('file', file);
1205
+ formData.append('path', this.currentPath);
1206
+
1207
+ const xhr = new XMLHttpRequest();
1208
+ const startTime = Date.now();
1209
+ let lastLoaded = 0;
1210
+ let lastTime = startTime;
1211
+
1212
+ // 创建进度条元素
1213
+ const progressItem = document.createElement('div');
1214
+ progressItem.className = 'progress-item';
1215
+ progressItem.innerHTML = `
1216
+ <div class="file-info">
1217
+ <span class="filename">${file.name}</span>
1218
+ <button class="cancel-upload">取消</button>
1219
+ </div>
1220
+ <div class="progress-bar">
1221
+ <div class="progress-fill"></div>
1222
+ </div>
1223
+ <div class="upload-stats">
1224
+ <span class="speed">0 KB/s</span>
1225
+ <span class="time-remaining">计算中...</span>
1226
+ </div>
1227
+ `;
1228
+ uploadProgress.appendChild(progressItem);
1229
+
1230
+ const progressFill = progressItem.querySelector('.progress-fill');
1231
+ const speedElement = progressItem.querySelector('.speed');
1232
+ const timeElement = progressItem.querySelector('.time-remaining');
1233
+ const cancelButton = progressItem.querySelector('.cancel-upload');
1234
+
1235
+ // 处理取消上传
1236
+ cancelButton.addEventListener('click', () => {
1237
+ xhr.abort();
1238
+ progressItem.remove();
1239
+ if (uploadProgress.children.length === 0) {
1240
+ uploadProgress.style.display = 'none';
1241
+ }
1242
+ });
1243
+
1244
+ xhr.upload.addEventListener('progress', (e) => {
1245
+ if (e.lengthComputable) {
1246
+ const percent = (e.loaded / e.total) * 100;
1247
+ progressFill.style.width = `${percent}%`;
1248
+
1249
+ // 计算上传速度
1250
+ const currentTime = Date.now();
1251
+ const timeElapsed = (currentTime - lastTime) / 1000; // 秒
1252
+ const loaded = e.loaded - lastLoaded;
1253
+ const speed = loaded / timeElapsed; // 字节每秒
1254
+
1255
+ // 计算剩余时间
1256
+ const remaining = (e.total - e.loaded) / speed;
1257
+ const minutes = Math.floor(remaining / 60);
1258
+ const seconds = Math.floor(remaining % 60);
1259
+
1260
+ // 更新UI
1261
+ speedElement.textContent = `${this.formatFileSize(speed)}/s`;
1262
+ timeElement.textContent = `预计剩余 ${minutes}分${seconds}秒`;
1263
+
1264
+ lastLoaded = e.loaded;
1265
+ lastTime = currentTime;
1266
+ }
1267
+ });
1268
+
1269
+ await new Promise((resolve, reject) => {
1270
+ xhr.onload = () => xhr.status === 200 ? resolve() : reject(new Error('Upload failed'));
1271
+ xhr.onerror = () => reject(new Error('Upload failed'));
1272
+ xhr.onabort = () => reject(new Error('Upload cancelled'));
1273
+
1274
+ xhr.open('POST', '/api/files/upload');
1275
+ xhr.send(formData);
1276
+ });
1277
+
1278
+ // 上传成功后移除进度条
1279
+ progressItem.remove();
1280
+ if (uploadProgress.children.length === 0) {
1281
+ uploadProgress.style.display = 'none';
1282
+ }
1283
+
1284
+ } catch (error) {
1285
+ if (error.message !== 'Upload cancelled') {
1286
+ this.showError(`上传文件 ${file.name} 失败`);
1287
+ }
1288
+ }
1289
+ }
1290
+
1291
+ // 刷新文件列表
1292
+ await this.loadFiles();
1293
+ }
1294
+ // 拖拽上传初始化
1295
+ initDragAndDrop() {
1296
+ const dragOverlay = document.querySelector('.drag-overlay');
1297
+ const container = document.querySelector('.container');
1298
+
1299
+ container.addEventListener('dragover', (e) => {
1300
+ e.preventDefault();
1301
+ dragOverlay.style.display = 'flex';
1302
+ });
1303
+
1304
+ container.addEventListener('dragleave', (e) => {
1305
+ if (e.relatedTarget === null) {
1306
+ dragOverlay.style.display = 'none';
1307
+ }
1308
+ });
1309
+
1310
+ container.addEventListener('drop', (e) => {
1311
+ e.preventDefault();
1312
+ dragOverlay.style.display = 'none';
1313
+
1314
+ if (e.dataTransfer.files.length > 0) {
1315
+ this.handleFileUpload(e.dataTransfer.files);
1316
+ }
1317
+ });
1318
+ }
1319
+
1320
+ // 面包屑导航更新
1321
+ updateBreadcrumb() {
1322
+ const breadcrumb = document.querySelector('.breadcrumb');
1323
+ const paths = this.currentPath.split('/').filter(Boolean);
1324
+
1325
+ breadcrumb.innerHTML = '<span class="breadcrumb-item" data-path="/">根目录</span>';
1326
+
1327
+ let currentPath = '';
1328
+ paths.forEach(path => {
1329
+ currentPath += `/${path}`;
1330
+ breadcrumb.innerHTML += `
1331
+ <span class="breadcrumb-separator">/</span>
1332
+ <span class="breadcrumb-item" data-path="${currentPath}">${decodeURIComponent(path)}</span>
1333
+ `;
1334
+ });
1335
+
1336
+ breadcrumb.querySelectorAll('.breadcrumb-item').forEach(item => {
1337
+ item.addEventListener('click', () => {
1338
+ this.currentPath = item.dataset.path;
1339
+ this.loadFiles();
1340
+ });
1341
+ });
1342
+ }
1343
+
1344
+ // 视图切换
1345
+ switchView(view) {
1346
+ const buttons = document.querySelectorAll('.view-btn');
1347
+ buttons.forEach(btn => {
1348
+ btn.classList.toggle('active', btn.dataset.view === view);
1349
+ });
1350
+
1351
+ this.currentView = view;
1352
+ this.renderFiles();
1353
+ }
1354
+
1355
+ // 文件类型筛选
1356
+ filterByType(type) {
1357
+ const items = document.querySelectorAll('.nav-item');
1358
+ items.forEach(item => {
1359
+ item.classList.toggle('active', item.dataset.type === type);
1360
+ });
1361
+
1362
+ this.currentFileType = type;
1363
+ this.renderFiles();
1364
+ }
1365
+
1366
+ // 搜索处理
1367
+ async handleSearch(keyword) {
1368
+ if (!keyword) {
1369
+ await this.loadFiles();
1370
+ return;
1371
+ }
1372
+
1373
+ try {
1374
+ const response = await fetch(`/api/files/search?keyword=${encodeURIComponent(keyword)}`);
1375
+ if (!response.ok) throw new Error('Search failed');
1376
+
1377
+ const searchResults = await response.json();
1378
+ // Transform the MySQL search results to match the file list format
1379
+ this.files = searchResults.map(file => ({
1380
+ type: 'file',
1381
+ path: file.path,
1382
+ size: parseInt(file.size), // Convert size string to number
1383
+ file_type: file.type,
1384
+ size_formatted: file.size,
1385
+ preview_url: `/api/files/preview/${file.path}`,
1386
+ download_url: `/api/files/download/${file.path}`,
1387
+ created_at: file.created_at
1388
+ }));
1389
+
1390
+ // Update the breadcrumb to show we're in search mode
1391
+ const breadcrumb = document.querySelector('.breadcrumb');
1392
+ breadcrumb.innerHTML = `
1393
+ <span class="breadcrumb-item" data-path="/">根目录</span>
1394
+ <span class="breadcrumb-separator">/</span>
1395
+ <span class="breadcrumb-item">搜索结果: "${keyword}"</span>
1396
+ `;
1397
+
1398
+ this.renderFiles();
1399
+
1400
+ // Show result count
1401
+ this.showMessage(`找到 ${this.files.length} 个匹配的文件`);
1402
+
1403
+ } catch (error) {
1404
+ console.error('Error searching files:', error);
1405
+ this.showError('搜索失败');
1406
+ }
1407
+ }
1408
+
1409
+ // 辅助方法
1410
+ getFileIcon(type, fileType) {
1411
+ const icons = {
1412
+ directory: 'fas fa-folder',
1413
+ image: 'fas fa-file-image',
1414
+ video: 'fas fa-file-video',
1415
+ document: 'fas fa-file-alt',
1416
+ audio: 'fas fa-file-audio',
1417
+ archive: 'fas fa-file-archive',
1418
+ code: 'fas fa-file-code',
1419
+ other: 'fas fa-file'
1420
+ };
1421
+
1422
+ if (type === 'directory') return icons.directory;
1423
+ return icons[fileType] || icons.other;
1424
+ }
1425
+
1426
+ formatFileSize(bytes) {
1427
+ if (!bytes) return '0 B';
1428
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1429
+ let size = bytes;
1430
+ let unitIndex = 0;
1431
+
1432
+ while (size >= 1024 && unitIndex < units.length - 1) {
1433
+ size /= 1024;
1434
+ unitIndex++;
1435
+ }
1436
+
1437
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
1438
+ }
1439
+
1440
+ debounce(func, wait) {
1441
+ let timeout;
1442
+ return function executedFunction(...args) {
1443
+ const later = () => {
1444
+ clearTimeout(timeout);
1445
+ func(...args);
1446
+ };
1447
+ clearTimeout(timeout);
1448
+ timeout = setTimeout(later, wait);
1449
+ };
1450
+ }
1451
+
1452
+ showError(message) {
1453
+ // 可以根据需要实现错误提示UI
1454
+ alert(message);
1455
+ }
1456
+ async deleteFile(file) {
1457
+ try {
1458
+ const confirmed = await this.showConfirmDialog(
1459
+ '确认删除',
1460
+ `确定要删除文件 "${file.path.split('/').pop()}" 吗?此操作不可恢复。`
1461
+ );
1462
+
1463
+ if (!confirmed) return;
1464
+
1465
+ const response = await fetch(`/api/files/delete/${encodeURIComponent(file.path)}`, {
1466
+ method: 'DELETE',
1467
+ headers: {
1468
+ 'Content-Type': 'application/json'
1469
+ }
1470
+ });
1471
+
1472
+ if (!response.ok) {
1473
+ const errorData = await response.json();
1474
+ throw new Error(errorData.details || 'Delete failed');
1475
+ }
1476
+
1477
+ await this.loadFiles();
1478
+ this.showMessage('文件已成功删除');
1479
+
1480
+ } catch (error) {
1481
+ console.error('Error deleting file:', error);
1482
+ this.showError(`删除文件失败: ${error.message}`);
1483
+ }
1484
+ }
1485
+
1486
+ // 添加确认对话框的实现
1487
+ showConfirmDialog(title, message) {
1488
+ return new Promise((resolve) => {
1489
+ const modal = document.createElement('div');
1490
+ modal.className = 'confirm-modal';
1491
+ modal.innerHTML = `
1492
+ <div class="confirm-content">
1493
+ <h3>${title}</h3>
1494
+ <p>${message}</p>
1495
+ <div class="confirm-buttons">
1496
+ <button class="confirm-cancel">取消</button>
1497
+ <button class="confirm-ok">确定</button>
1498
+ </div>
1499
+ </div>
1500
+ `;
1501
+
1502
+ document.body.appendChild(modal);
1503
+
1504
+ const handleConfirm = (confirmed) => {
1505
+ modal.remove();
1506
+ resolve(confirmed);
1507
+ };
1508
+
1509
+ modal.querySelector('.confirm-cancel').addEventListener('click', () => handleConfirm(false));
1510
+ modal.querySelector('.confirm-ok').addEventListener('click', () => handleConfirm(true));
1511
+ });
1512
+ }
1513
+
1514
+ // 添加提示消息的实现
1515
+ showMessage(message) {
1516
+ const toast = document.createElement('div');
1517
+ toast.className = 'toast-message';
1518
+ toast.textContent = message;
1519
+
1520
+ document.body.appendChild(toast);
1521
+
1522
+ setTimeout(() => {
1523
+ toast.classList.add('show');
1524
+ setTimeout(() => {
1525
+ toast.classList.remove('show');
1526
+ setTimeout(() => toast.remove(), 300);
1527
+ }, 2000);
1528
+ }, 100);
1529
+ }
1530
+ }
1531
+ // 初始化文件管理器
1532
+ new FileManager();
1533
+ </script>
1534
+ </body>
1535
+ </html>
templates/login.html ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <style>
8
+ :root {
9
+ --primary-glow: #ff9580;
10
+ --secondary-glow: #ffd700;
11
+ --background: #ffffff;
12
+ --text: #333333;
13
+ --card-bg: #ffffff;
14
+ --border-color: #e0e0e0;
15
+ }
16
+
17
+ * {
18
+ margin: 0;
19
+ padding: 0;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
25
+ background: var(--background);
26
+ color: var(--text);
27
+ min-height: 100vh;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ }
32
+
33
+ .login-container {
34
+ background: var(--card-bg);
35
+ padding: 40px;
36
+ border-radius: 15px;
37
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
38
+ width: 100%;
39
+ max-width: 400px;
40
+ transition: all 0.3s ease;
41
+ }
42
+
43
+ .login-container:hover {
44
+ transform: translateY(-5px);
45
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
46
+ }
47
+
48
+ .login-title {
49
+ text-align: center;
50
+ margin-bottom: 30px;
51
+ font-size: 24px;
52
+ color: var(--text);
53
+ }
54
+
55
+ .login-form {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 20px;
59
+ }
60
+
61
+ .password-input {
62
+ width: 100%;
63
+ padding: 12px 20px;
64
+ border: 2px solid var(--border-color);
65
+ border-radius: 25px;
66
+ font-size: 16px;
67
+ transition: all 0.3s ease;
68
+ }
69
+
70
+ .password-input:focus {
71
+ outline: none;
72
+ border-color: var(--primary-glow);
73
+ box-shadow: 0 0 10px rgba(255, 149, 128, 0.3);
74
+ }
75
+
76
+ .login-button {
77
+ width: 100%;
78
+ padding: 12px 20px;
79
+ border: none;
80
+ border-radius: 25px;
81
+ background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
82
+ color: white;
83
+ font-size: 16px;
84
+ cursor: pointer;
85
+ transition: all 0.3s ease;
86
+ }
87
+
88
+ .login-button:hover {
89
+ transform: translateY(-2px);
90
+ box-shadow: 0 5px 15px rgba(255, 149, 128, 0.4);
91
+ }
92
+
93
+ .error-message {
94
+ color: #ff4444;
95
+ text-align: center;
96
+ margin-top: 10px;
97
+ display: none;
98
+ }
99
+ </style>
100
+ </head>
101
+ <body>
102
+ <div class="login-container">
103
+ <h1 class="login-title"> Cloud Vault的登录界面,没错在这输一下密码,密码1234</h1>
104
+ <form class="login-form" id="loginForm">
105
+ <input type="password" class="password-input" placeholder="请输入访问密码" required>
106
+ <button type="submit" class="login-button">登录</button>
107
+ </form>
108
+ <div class="error-message" id="errorMessage">密码错误,请重试</div>
109
+ </div>
110
+
111
+ <script>
112
+ document.getElementById('loginForm').addEventListener('submit', async (e) => {
113
+ e.preventDefault();
114
+ const password = document.querySelector('.password-input').value;
115
+ const errorMessage = document.getElementById('errorMessage');
116
+
117
+ try {
118
+ const response = await fetch('/login', {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/x-www-form-urlencoded',
122
+ },
123
+ body: `password=${encodeURIComponent(password)}`
124
+ });
125
+
126
+ if (response.ok) {
127
+ window.location.href = '/';
128
+ } else {
129
+ errorMessage.style.display = 'block';
130
+ }
131
+ } catch (error) {
132
+ console.error('Login failed:', error);
133
+ errorMessage.style.display = 'block';
134
+ }
135
+ });
136
+ </script>
137
+ </body>
138
+ </html>