lexlepty commited on
Commit
776c698
·
verified ·
1 Parent(s): 871e6df

Upload index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +2118 -0
templates/index.html ADDED
@@ -0,0 +1,2118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <div class="file-container">
1025
+ <!-- 文件内容将通过 JavaScript 动态生成 -->
1026
+ </div>
1027
+ </main>
1028
+
1029
+ <!-- 上传按钮 -->
1030
+ <div class="upload-btn" id="uploadBtn">
1031
+ <i class="fas fa-plus"></i>
1032
+ <input type="file" id="fileInput" style="display: none;" multiple>
1033
+ </div>
1034
+
1035
+ <!-- 上传进度条 -->
1036
+ <div class="upload-progress">
1037
+ </div>
1038
+ </div>
1039
+
1040
+ <!-- 拖拽上传遮罩 -->
1041
+ <div class="drag-overlay">
1042
+ <div>释放鼠标上传文件</div>
1043
+ </div>
1044
+
1045
+ <!-- 文件操作菜单 -->
1046
+ <div class="file-menu" style="display: none;">
1047
+ <div class="file-menu-item" data-action="preview">
1048
+ <i class="fas fa-eye"></i> 预览
1049
+ </div>
1050
+ <div class="file-menu-item" data-action="download">
1051
+ <i class="fas fa-download"></i> 下载
1052
+ </div>
1053
+ <div class="file-menu-item" data-action="delete">
1054
+ <i class="fas fa-trash"></i> 删除
1055
+ </div>
1056
+ </div>
1057
+ <!-- 预览模态框 -->
1058
+ <div class="preview-modal">
1059
+ <div class="preview-container">
1060
+ <div class="preview-content">
1061
+ <!-- 预览内容将通过 JavaScript 动态生成 -->
1062
+ </div>
1063
+ <button class="preview-close">
1064
+ <i class="fas fa-times"></i>
1065
+ </button>
1066
+ </div>
1067
+ </div>
1068
+ <script>
1069
+ // 文件管理类
1070
+ class FileManager {
1071
+ constructor() {
1072
+ this.selectedFiles = new Set();
1073
+ this.isMultiSelectMode = false;
1074
+ this.currentPath = '/';
1075
+ this.currentView = 'grid';
1076
+ this.currentFileType = 'all';
1077
+ this.files = [];
1078
+ this.initEventListeners();
1079
+ this.loadFiles();
1080
+ }
1081
+
1082
+ // 初始化事件监听
1083
+ initEventListeners() {
1084
+ // 视图切换
1085
+ document.querySelectorAll('.view-btn').forEach(btn => {
1086
+ btn.addEventListener('click', () => this.switchView(btn.dataset.view));
1087
+ });
1088
+
1089
+ // 文件类型筛选
1090
+ document.querySelectorAll('.nav-item').forEach(item => {
1091
+ item.addEventListener('click', () => this.filterByType(item.dataset.type));
1092
+ });
1093
+
1094
+ // 搜索
1095
+ const searchBox = document.querySelector('.search-box');
1096
+ searchBox.addEventListener('input', this.debounce((e) => this.handleSearch(e.target.value), 300));
1097
+
1098
+ // 文件上传
1099
+ const uploadBtn = document.getElementById('uploadBtn');
1100
+ const fileInput = document.getElementById('fileInput');
1101
+
1102
+ uploadBtn.addEventListener('click', () => fileInput.click());
1103
+ fileInput.addEventListener('change', (e) => this.handleFileUpload(e.target.files));
1104
+
1105
+ // 拖拽上传
1106
+ this.initDragAndDrop();
1107
+ // 新建文件夹按钮监听
1108
+ const createFolderBtn = document.querySelector('.create-folder-btn');
1109
+ createFolderBtn.addEventListener('click', () => this.showCreateFolderDialog());
1110
+
1111
+ // 添加多选按钮
1112
+ const multiSelectBtn = document.createElement('button');
1113
+ multiSelectBtn.className = 'action-btn multi-select-btn';
1114
+ multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>多选</span>';
1115
+ multiSelectBtn.addEventListener('click', () => this.toggleMultiSelectMode());
1116
+ document.querySelector('.view-toggle').prepend(multiSelectBtn);
1117
+ }
1118
+
1119
+ // 加载文件列表
1120
+ async loadFiles() {
1121
+ try {
1122
+ const path = this.currentPath === '/' ? '' : this.currentPath;
1123
+ const response = await fetch(`/api/files/list/${path}`);
1124
+ if (!response.ok) throw new Error('Failed to load files');
1125
+
1126
+ this.files = await response.json();
1127
+ this.renderFiles();
1128
+ this.updateBreadcrumb();
1129
+ } catch (error) {
1130
+ console.error('Error loading files:', error);
1131
+ this.showError('加载文件失败');
1132
+ }
1133
+ }
1134
+
1135
+ // 渲染文件列表
1136
+ renderFiles() {
1137
+ const container = document.querySelector('.file-container');
1138
+ container.innerHTML = '';
1139
+
1140
+ const viewClass = this.currentView === 'grid' ? 'file-grid' : 'file-list';
1141
+ container.className = `file-container ${viewClass}`;
1142
+
1143
+ let filteredFiles = this.files;
1144
+ if (this.currentFileType !== 'all') {
1145
+ filteredFiles = this.files.filter(file => file.file_type === this.currentFileType);
1146
+ }
1147
+
1148
+ filteredFiles.forEach(file => {
1149
+ const fileElement = this.createFileElement(file);
1150
+ container.appendChild(fileElement);
1151
+ });
1152
+ }
1153
+
1154
+ // 创建文件元素
1155
+ createFileElement(file) {
1156
+ const element = document.createElement('div');
1157
+ element.className = `file-item ${this.currentView}`;
1158
+
1159
+ // 添加多选模式相关的类
1160
+ if (this.isMultiSelectMode) {
1161
+ element.classList.add('selectable');
1162
+ if (this.selectedFiles.has(file)) {
1163
+ element.classList.add('selected');
1164
+ }
1165
+ }
1166
+
1167
+ const icon = this.getFileIcon(file.type, file.file_type);
1168
+ const size = this.formatFileSize(file.size);
1169
+
1170
+ if (this.currentView === 'grid') {
1171
+ element.innerHTML = `
1172
+ <i class="${icon} file-icon"></i>
1173
+ <div class="file-name">${file.path.split('/').pop()}</div>
1174
+ <div class="file-size">${size}</div>
1175
+ `;
1176
+ } else {
1177
+ element.innerHTML = `
1178
+ <i class="${icon} file-icon"></i>
1179
+ <div class="file-info">
1180
+ <div class="file-name">${file.path.split('/').pop()}</div>
1181
+ <div class="file-meta">
1182
+ <span>${size}</span>
1183
+ <span>${file.file_type || '未知类型'}</span>
1184
+ </div>
1185
+ </div>
1186
+ `;
1187
+ }
1188
+
1189
+ // 事件处理逻辑
1190
+ if (this.isMultiSelectMode) {
1191
+ // 多选模式下的点击处理
1192
+ element.addEventListener('click', (e) => {
1193
+ e.preventDefault();
1194
+ e.stopPropagation();
1195
+
1196
+ if (file.type === 'directory') {
1197
+ // 文件夹仍然保持导航功能
1198
+ this.currentPath = file.path;
1199
+ this.loadFiles();
1200
+ } else {
1201
+ // 文件切换选中状态
1202
+ if (this.selectedFiles.has(file)) {
1203
+ this.selectedFiles.delete(file);
1204
+ element.classList.remove('selected');
1205
+ } else {
1206
+ this.selectedFiles.add(file);
1207
+ element.classList.add('selected');
1208
+ }
1209
+ }
1210
+ });
1211
+ } else {
1212
+ // 普通模式下的点击处理
1213
+ element.addEventListener('click', () => this.handleFileClick(file));
1214
+ }
1215
+
1216
+ // 右键菜单处理
1217
+ element.addEventListener('contextmenu', (e) => {
1218
+ e.preventDefault();
1219
+ this.showFileMenu(e, file);
1220
+ });
1221
+
1222
+ return element;
1223
+ }
1224
+ // 处理文件点击
1225
+ handleFileClick(file) {
1226
+ if (file.type === 'directory') {
1227
+ this.currentPath = file.path;
1228
+ this.loadFiles();
1229
+ } else {
1230
+ this.previewFile(file);
1231
+ }
1232
+ }
1233
+
1234
+ // 显示文件操作菜单
1235
+ showFileMenu(e, file) {
1236
+ e.preventDefault();
1237
+
1238
+ const menu = document.querySelector('.file-menu');
1239
+ menu.style.display = 'block';
1240
+ menu.style.left = `${e.pageX}px`;
1241
+ menu.style.top = `${e.pageY}px`;
1242
+
1243
+ // 清除旧的事件监听
1244
+ const menuItems = menu.querySelectorAll('.file-menu-item');
1245
+ menuItems.forEach(item => {
1246
+ const clone = item.cloneNode(true);
1247
+ item.parentNode.replaceChild(clone, item);
1248
+ });
1249
+
1250
+ // 添加新的事件监听
1251
+ menu.querySelector('[data-action="preview"]').addEventListener('click', () => this.previewFile(file));
1252
+ menu.querySelector('[data-action="download"]').addEventListener('click', () => this.downloadFile(file));
1253
+ menu.querySelector('[data-action="delete"]').addEventListener('click', () => this.deleteFile(file));
1254
+
1255
+ // 点击其他地方关闭菜单
1256
+ const closeMenu = () => {
1257
+ menu.style.display = 'none';
1258
+ document.removeEventListener('click', closeMenu);
1259
+ };
1260
+
1261
+ setTimeout(() => {
1262
+ document.addEventListener('click', closeMenu);
1263
+ }, 0);
1264
+ }
1265
+
1266
+ // 文件预览
1267
+ async previewFile(file) {
1268
+ try {
1269
+ const modal = document.querySelector('.preview-modal');
1270
+ const content = modal.querySelector('.preview-content');
1271
+
1272
+ // 计算合适的预览尺寸
1273
+ const windowWidth = window.innerWidth;
1274
+ const windowHeight = window.innerHeight;
1275
+ const maxWidth = Math.min(windowWidth * 0.9, 1200); // 最大宽度不超过1200px
1276
+ const maxHeight = windowHeight * 0.85;
1277
+
1278
+ modal.style.display = 'flex';
1279
+ content.innerHTML = `
1280
+ <div class="loading-indicator">
1281
+ <div class="spinner"></div>
1282
+ <div class="loading-text">正在加载预览...</div>
1283
+ </div>
1284
+ `;
1285
+
1286
+ const response = await fetch(`/api/files/preview/${file.path}`);
1287
+ if (!response.ok) throw new Error('Failed to preview file');
1288
+
1289
+ const blob = await response.blob();
1290
+ const url = URL.createObjectURL(blob);
1291
+ const mimeType = response.headers.get('content-type') || '';
1292
+ const fileName = file.path.split('/').pop();
1293
+
1294
+ // 获取预览内容
1295
+ const previewContent = `
1296
+ <div class="preview-header">
1297
+ <div class="preview-info">
1298
+ <i class="${this.getFileIcon(file.type, file.file_type)}"></i>
1299
+ <span>${fileName}</span>
1300
+ </div>
1301
+ <div class="preview-actions">
1302
+ <button class="preview-action-btn zoom-in">
1303
+ <i class="fas fa-search-plus"></i>
1304
+ </button>
1305
+ <button class="preview-action-btn zoom-out">
1306
+ <i class="fas fa-search-minus"></i>
1307
+ </button>
1308
+ <button class="preview-action-btn download">
1309
+ <i class="fas fa-download"></i>
1310
+ </button>
1311
+ </div>
1312
+ </div>
1313
+ <div class="preview-body" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
1314
+ ${await this.getPreviewContent(file, url, mimeType, maxWidth, maxHeight)}
1315
+ </div>
1316
+ `;
1317
+
1318
+ content.innerHTML = previewContent;
1319
+
1320
+ // 绑定事件处理
1321
+ this.bindPreviewEvents(modal, content, file, url);
1322
+
1323
+ } catch (error) {
1324
+ console.error('Error previewing file:', error);
1325
+ this.showError('预览文件失败');
1326
+ }
1327
+ }
1328
+
1329
+ async getPreviewContent(file, url, mimeType, maxWidth, maxHeight) {
1330
+ const extension = file.path.split('.').pop().toLowerCase();
1331
+
1332
+ if (file.file_type === 'image' || mimeType.startsWith('image/')) {
1333
+ return `
1334
+ <div class="preview-image-container" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
1335
+ <img src="${url}" alt="${file.path}" class="preview-image">
1336
+ </div>
1337
+ `;
1338
+ }
1339
+
1340
+ if (file.file_type === 'video' || mimeType.startsWith('video/')) {
1341
+ return `
1342
+ <div class="video-container" style="max-width: ${maxWidth * 0.8}px;">
1343
+ <video class="plyr-media" controls crossorigin playsinline>
1344
+ <source src="${url}" type="${mimeType}">
1345
+ </video>
1346
+ </div>
1347
+ `;
1348
+ }
1349
+
1350
+ if (file.file_type === 'audio' || mimeType.startsWith('audio/')) {
1351
+ return `
1352
+ <div class="audio-container" style="width: ${maxWidth * 0.6}px;">
1353
+ <audio class="plyr-media" controls>
1354
+ <source src="${url}" type="${mimeType}">
1355
+ </audio>
1356
+ </div>
1357
+ `;
1358
+ }
1359
+
1360
+ if (mimeType.includes('pdf')) {
1361
+ return `
1362
+ <iframe src="${url}#view=FitH" type="application/pdf"
1363
+ style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
1364
+ </iframe>
1365
+ `;
1366
+ }
1367
+
1368
+ // 支持 Markdown 预览
1369
+ if (extension === 'md') {
1370
+ const text = await (await fetch(url)).text();
1371
+ const marked = window.marked; // 确保已引入 marked 库
1372
+ const htmlContent = marked ? marked(text) : text;
1373
+ return `
1374
+ <div class="markdown-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
1375
+ ${htmlContent}
1376
+ </div>
1377
+ `;
1378
+ }
1379
+
1380
+ // 支持 HTML 预览
1381
+ if (extension === 'html' || mimeType.includes('html')) {
1382
+ return `
1383
+ <iframe src="${url}" sandbox="allow-same-origin allow-scripts"
1384
+ style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
1385
+ </iframe>
1386
+ `;
1387
+ }
1388
+
1389
+ if (mimeType.includes('text/') || mimeType.includes('application/json')) {
1390
+ const text = await (await fetch(url)).text();
1391
+ return `
1392
+ <div class="text-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
1393
+ <pre><code>${this.escapeHtml(text)}</code></pre>
1394
+ </div>
1395
+ `;
1396
+ }
1397
+
1398
+ return `
1399
+ <div class="unsupported-preview">
1400
+ <i class="fas fa-exclamation-circle"></i>
1401
+ <p>此文件类型暂不支持预览</p>
1402
+ <button class="download-btn">
1403
+ <i class="fas fa-download"></i> 下载文件
1404
+ </button>
1405
+ </div>
1406
+ `;
1407
+ }
1408
+
1409
+ bindPreviewEvents(modal, content, file, url) {
1410
+ // 缩放功能
1411
+ let currentScale = 1;
1412
+ const zoomStep = 0.1;
1413
+ const maxScale = 3;
1414
+ const minScale = 0.5;
1415
+
1416
+ const zoomIn = content.querySelector('.zoom-in');
1417
+ const zoomOut = content.querySelector('.zoom-out');
1418
+ const previewImage = content.querySelector('.preview-image');
1419
+ const downloadBtn = content.querySelector('.preview-action-btn.download');
1420
+
1421
+ if (zoomIn && zoomOut && previewImage) {
1422
+ zoomIn.onclick = () => {
1423
+ if (currentScale < maxScale) {
1424
+ currentScale += zoomStep;
1425
+ previewImage.style.transform = `scale(${currentScale})`;
1426
+ }
1427
+ };
1428
+
1429
+ zoomOut.onclick = () => {
1430
+ if (currentScale > minScale) {
1431
+ currentScale -= zoomStep;
1432
+ previewImage.style.transform = `scale(${currentScale})`;
1433
+ }
1434
+ };
1435
+ }
1436
+
1437
+ // 下载功能
1438
+ if (downloadBtn) {
1439
+ downloadBtn.onclick = () => this.downloadFile(file);
1440
+ }
1441
+
1442
+ // 初始化视频播放器
1443
+ if (file.file_type === 'video' || file.file_type === 'audio') {
1444
+ const playerElement = content.querySelector('.plyr-media');
1445
+ if (playerElement && window.Plyr) {
1446
+ new Plyr(playerElement);
1447
+ }
1448
+ }
1449
+
1450
+ // 关闭预览
1451
+ const closeBtn = modal.querySelector('.preview-close');
1452
+ const closePreview = () => {
1453
+ URL.revokeObjectURL(url);
1454
+ modal.style.display = 'none';
1455
+ const players = document.querySelectorAll('.plyr');
1456
+ players.forEach(player => {
1457
+ if (player.plyr) {
1458
+ player.plyr.destroy();
1459
+ }
1460
+ });
1461
+ };
1462
+ closeBtn.onclick = closePreview;
1463
+ }
1464
+ // 文件下载
1465
+ async downloadFile(file) {
1466
+ try {
1467
+ const uploadProgress = document.querySelector('.upload-progress');
1468
+ const progressItem = document.createElement('div');
1469
+ progressItem.className = 'progress-item';
1470
+ progressItem.innerHTML = ''; // 清除之前的进度条
1471
+ progressItem.innerHTML = `
1472
+ <div class="file-info">
1473
+ <span class="filename">${file.path.split('/').pop()}</span>
1474
+ <button class="cancel-download">
1475
+ <i class="fas fa-times"></i> 取消
1476
+ </button>
1477
+ </div>
1478
+ <div class="progress-bar">
1479
+ <div class="progress-fill"></div>
1480
+ </div>
1481
+ <div class="download-stats">
1482
+ <div class="stats-row">
1483
+ <span class="progress-text">0%</span>
1484
+ <span class="downloaded-size">0 MB / 0 MB</span>
1485
+ </div>
1486
+ <div class="stats-row">
1487
+ <span class="speed">等待开始...</span>
1488
+ </div>
1489
+ </div>
1490
+ `;
1491
+ uploadProgress.style.display = 'block';
1492
+ uploadProgress.appendChild(progressItem);
1493
+ const progressFill = progressItem.querySelector('.progress-fill');
1494
+ const progressText = progressItem.querySelector('.progress-text');
1495
+ const speedElement = progressItem.querySelector('.speed');
1496
+ const sizeElement = progressItem.querySelector('.downloaded-size');
1497
+ const cancelButton = progressItem.querySelector('.cancel-download');
1498
+ const controller = new AbortController();
1499
+ let isDownloadCancelled = false;
1500
+ cancelButton.onclick = () => {
1501
+ controller.abort();
1502
+ isDownloadCancelled = true;
1503
+ progressItem.remove();
1504
+ if (!uploadProgress.hasChildNodes()) {
1505
+ uploadProgress.style.display = 'none';
1506
+ }
1507
+ };
1508
+ const response = await fetch(`/api/files/download/${file.path}`, {
1509
+ signal: controller.signal
1510
+ });
1511
+ if (!response.ok) throw new Error('Download failed');
1512
+ const contentLength = response.headers.get('content-length');
1513
+ const total = parseInt(contentLength, 10);
1514
+ const reader = response.body.getReader();
1515
+
1516
+ let receivedLength = 0;
1517
+ let lastTime = Date.now();
1518
+ let lastReceived = 0;
1519
+ let currentSpeed = 0;
1520
+ let lastSpeedUpdate = Date.now();
1521
+ const chunks = [];
1522
+ while (true) {
1523
+ const {done, value} = await reader.read();
1524
+
1525
+ if (done || isDownloadCancelled) break;
1526
+
1527
+ chunks.push(value);
1528
+ receivedLength += value.length;
1529
+
1530
+ const percent = (receivedLength / total) * 100;
1531
+ progressFill.style.width = `${percent.toFixed(1)}%`;
1532
+ progressText.textContent = `${percent.toFixed(1)}%`;
1533
+
1534
+ const now = Date.now();
1535
+ if (now - lastSpeedUpdate >= 1000) {
1536
+ const timeElapsed = (now - lastTime) / 1000;
1537
+ const receivedSinceLastTime = receivedLength - lastReceived;
1538
+
1539
+ if (timeElapsed > 0) {
1540
+ currentSpeed = receivedSinceLastTime / timeElapsed;
1541
+ if (currentSpeed > 0) {
1542
+ speedElement.textContent = `${this.formatFileSize(currentSpeed)}/s`;
1543
+ }
1544
+ }
1545
+
1546
+ lastTime = now;
1547
+ lastReceived = receivedLength;
1548
+ lastSpeedUpdate = now;
1549
+ }
1550
+
1551
+ sizeElement.textContent = `${this.formatFileSize(receivedLength)} / ${this.formatFileSize(total)}`;
1552
+ }
1553
+ if (!isDownloadCancelled) {
1554
+ const blob = new Blob(chunks);
1555
+ const url = URL.createObjectURL(blob);
1556
+ const a = document.createElement('a');
1557
+ a.href = url;
1558
+ a.download = file.path.split('/').pop();
1559
+ document.body.appendChild(a);
1560
+ a.click();
1561
+ document.body.removeChild(a);
1562
+ URL.revokeObjectURL(url);
1563
+ progressItem.remove();
1564
+ if (!uploadProgress.hasChildNodes()) {
1565
+ uploadProgress.style.display = 'none';
1566
+ }
1567
+ }
1568
+ } catch (error) {
1569
+ const uploadProgress = document.querySelector('.upload-progress');
1570
+ if (error.name === 'AbortError') {
1571
+ this.showMessage('下载已取消');
1572
+ } else {
1573
+ console.error('Error downloading file:', error);
1574
+ this.showError('下载文件失败');
1575
+ }
1576
+ if (!uploadProgress.hasChildNodes()) {
1577
+ uploadProgress.style.display = 'none';
1578
+ }
1579
+ }
1580
+ }
1581
+ // 文件上传处理
1582
+ async handleFileUpload(files) {
1583
+ const uploadProgress = document.querySelector('.upload-progress');
1584
+ uploadProgress.style.display = 'block';
1585
+ uploadProgress.innerHTML = ''; // 清除之前的进度条
1586
+
1587
+ let hasSuccessfulUpload = false; // 跟踪是否有文件上传成功
1588
+
1589
+ for (const file of files) {
1590
+ try {
1591
+ const formData = new FormData();
1592
+ formData.append('file', file);
1593
+ formData.append('path', this.currentPath);
1594
+
1595
+ const xhr = new XMLHttpRequest();
1596
+ const startTime = Date.now();
1597
+ let lastLoaded = 0;
1598
+ let lastTime = startTime;
1599
+
1600
+ // 创建进度条元素
1601
+ const progressItem = document.createElement('div');
1602
+ progressItem.className = 'progress-item';
1603
+ progressItem.innerHTML = `
1604
+ <div class="file-info">
1605
+ <span class="filename">${file.name}</span>
1606
+ <button class="cancel-upload">取消</button>
1607
+ </div>
1608
+ <div class="progress-bar">
1609
+ <div class="progress-fill"></div>
1610
+ </div>
1611
+ <div class="upload-stats">
1612
+ <span class="speed">0 KB/s</span>
1613
+ <span class="time-remaining">计算中...</span>
1614
+ </div>
1615
+ `;
1616
+ uploadProgress.appendChild(progressItem);
1617
+
1618
+ const progressFill = progressItem.querySelector('.progress-fill');
1619
+ const speedElement = progressItem.querySelector('.speed');
1620
+ const timeElement = progressItem.querySelector('.time-remaining');
1621
+ const cancelButton = progressItem.querySelector('.cancel-upload');
1622
+
1623
+ // 处理取消上传
1624
+ cancelButton.addEventListener('click', () => {
1625
+ xhr.abort();
1626
+ progressItem.remove();
1627
+ if (uploadProgress.children.length === 0) {
1628
+ uploadProgress.style.display = 'none';
1629
+ }
1630
+ });
1631
+
1632
+ // 处理上传进度
1633
+ xhr.upload.addEventListener('progress', (e) => {
1634
+ if (e.lengthComputable) {
1635
+ const percent = (e.loaded / e.total) * 100;
1636
+ progressFill.style.width = `${percent}%`;
1637
+
1638
+ // 计算上传速度
1639
+ const currentTime = Date.now();
1640
+ const timeElapsed = (currentTime - lastTime) / 1000; // 秒
1641
+ const loaded = e.loaded - lastLoaded;
1642
+ const speed = loaded / timeElapsed; // 字节每秒
1643
+
1644
+ // 计算剩余时间
1645
+ const remaining = (e.total - e.loaded) / speed;
1646
+ const minutes = Math.floor(remaining / 60);
1647
+ const seconds = Math.floor(remaining % 60);
1648
+
1649
+ // 更新UI
1650
+ speedElement.textContent = `${this.formatFileSize(speed)}/s`;
1651
+ timeElement.textContent = `预计剩余 ${minutes}分${seconds}秒`;
1652
+
1653
+ lastLoaded = e.loaded;
1654
+ lastTime = currentTime;
1655
+ }
1656
+ });
1657
+
1658
+ // 执行上传请求
1659
+ await new Promise((resolve, reject) => {
1660
+ xhr.onload = async () => {
1661
+ try {
1662
+ const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
1663
+
1664
+ if (xhr.status === 200 && response.success) {
1665
+ this.showMessage(`文件 ${file.name} 上传成功`);
1666
+ hasSuccessfulUpload = true; // 标记上传成功
1667
+ resolve();
1668
+ } else {
1669
+ const errorMessage = response.error || '上传失败';
1670
+ reject(new Error(errorMessage));
1671
+ }
1672
+ } catch (e) {
1673
+ reject(new Error('服务器响应格式错误'));
1674
+ }
1675
+ };
1676
+
1677
+ xhr.onerror = () => reject(new Error('网络错误'));
1678
+ xhr.onabort = () => reject(new Error('Upload cancelled'));
1679
+
1680
+ xhr.open('POST', '/api/files/upload');
1681
+ xhr.send(formData);
1682
+ });
1683
+
1684
+ // 上传完成后移除进度条
1685
+ progressItem.remove();
1686
+ if (uploadProgress.children.length === 0) {
1687
+ uploadProgress.style.display = 'none';
1688
+ }
1689
+
1690
+ } catch (error) {
1691
+ if (error.message !== 'Upload cancelled') {
1692
+ this.showError(`上传文件 ${file.name} 失败`);
1693
+ }
1694
+ }
1695
+ }
1696
+
1697
+ // 所有上传完成后,如果有文件上传成功则刷新文件列表
1698
+ if (hasSuccessfulUpload) {
1699
+ await this.loadFiles();
1700
+ }
1701
+ }
1702
+ // 拖拽上传初始化
1703
+ initDragAndDrop() {
1704
+ const dragOverlay = document.querySelector('.drag-overlay');
1705
+ const container = document.querySelector('.container');
1706
+
1707
+ container.addEventListener('dragover', (e) => {
1708
+ e.preventDefault();
1709
+ dragOverlay.style.display = 'flex';
1710
+ });
1711
+
1712
+ container.addEventListener('dragleave', (e) => {
1713
+ if (e.relatedTarget === null) {
1714
+ dragOverlay.style.display = 'none';
1715
+ }
1716
+ });
1717
+
1718
+ container.addEventListener('drop', (e) => {
1719
+ e.preventDefault();
1720
+ dragOverlay.style.display = 'none';
1721
+
1722
+ if (e.dataTransfer.files.length > 0) {
1723
+ this.handleFileUpload(e.dataTransfer.files);
1724
+ }
1725
+ });
1726
+ }
1727
+
1728
+ // 面包屑导航更新
1729
+ updateBreadcrumb() {
1730
+ const breadcrumb = document.querySelector('.breadcrumb');
1731
+ const paths = this.currentPath.split('/').filter(Boolean);
1732
+
1733
+ breadcrumb.innerHTML = '<span class="breadcrumb-item" data-path="/">根目录</span>';
1734
+
1735
+ let currentPath = '';
1736
+ paths.forEach(path => {
1737
+ currentPath += `/${path}`;
1738
+ breadcrumb.innerHTML += `
1739
+ <span class="breadcrumb-separator">/</span>
1740
+ <span class="breadcrumb-item" data-path="${currentPath}">${decodeURIComponent(path)}</span>
1741
+ `;
1742
+ });
1743
+
1744
+ breadcrumb.querySelectorAll('.breadcrumb-item').forEach(item => {
1745
+ item.addEventListener('click', () => {
1746
+ this.currentPath = item.dataset.path;
1747
+ this.loadFiles();
1748
+ });
1749
+ });
1750
+ }
1751
+
1752
+ // 视图切换
1753
+ switchView(view) {
1754
+ const buttons = document.querySelectorAll('.view-btn');
1755
+ buttons.forEach(btn => {
1756
+ btn.classList.toggle('active', btn.dataset.view === view);
1757
+ });
1758
+
1759
+ this.currentView = view;
1760
+ this.renderFiles();
1761
+ }
1762
+
1763
+ // 文件类型筛选
1764
+ filterByType(type) {
1765
+ const items = document.querySelectorAll('.nav-item');
1766
+ items.forEach(item => {
1767
+ item.classList.toggle('active', item.dataset.type === type);
1768
+ });
1769
+
1770
+ this.currentFileType = type;
1771
+ this.renderFiles();
1772
+ }
1773
+
1774
+ // 搜索处理
1775
+ async handleSearch(keyword) {
1776
+ if (!keyword) {
1777
+ await this.loadFiles();
1778
+ return;
1779
+ }
1780
+
1781
+ try {
1782
+ const response = await fetch(`/api/files/search?keyword=${encodeURIComponent(keyword)}`);
1783
+ if (!response.ok) throw new Error('Search failed');
1784
+
1785
+ const searchResults = await response.json();
1786
+ // Transform the MySQL search results to match the file list format
1787
+ this.files = searchResults.map(file => ({
1788
+ type: 'file',
1789
+ path: file.path,
1790
+ size: parseInt(file.size), // Convert size string to number
1791
+ file_type: file.type,
1792
+ size_formatted: file.size,
1793
+ preview_url: `/api/files/preview/${file.path}`,
1794
+ download_url: `/api/files/download/${file.path}`,
1795
+ created_at: file.created_at
1796
+ }));
1797
+
1798
+ // Update the breadcrumb to show we're in search mode
1799
+ const breadcrumb = document.querySelector('.breadcrumb');
1800
+ breadcrumb.innerHTML = `
1801
+ <span class="breadcrumb-item" data-path="/">根目录</span>
1802
+ <span class="breadcrumb-separator">/</span>
1803
+ <span class="breadcrumb-item">搜索结果: "${keyword}"</span>
1804
+ `;
1805
+
1806
+ this.renderFiles();
1807
+
1808
+ // Show result count
1809
+ this.showMessage(`找到 ${this.files.length} 个匹配的文件`);
1810
+
1811
+ } catch (error) {
1812
+ console.error('Error searching files:', error);
1813
+ this.showError('搜索失败');
1814
+ }
1815
+ }
1816
+
1817
+ // 辅助方法
1818
+ getFileIcon(type, fileType) {
1819
+ const icons = {
1820
+ directory: 'fas fa-folder',
1821
+ image: 'fas fa-file-image',
1822
+ video: 'fas fa-file-video',
1823
+ document: 'fas fa-file-alt',
1824
+ audio: 'fas fa-file-audio',
1825
+ archive: 'fas fa-file-archive',
1826
+ code: 'fas fa-file-code',
1827
+ other: 'fas fa-file'
1828
+ };
1829
+
1830
+ if (type === 'directory') return icons.directory;
1831
+ return icons[fileType] || icons.other;
1832
+ }
1833
+
1834
+ formatFileSize(bytes) {
1835
+ if (!bytes) return '0 B';
1836
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1837
+ let size = bytes;
1838
+ let unitIndex = 0;
1839
+
1840
+ while (size >= 1024 && unitIndex < units.length - 1) {
1841
+ size /= 1024;
1842
+ unitIndex++;
1843
+ }
1844
+
1845
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
1846
+ }
1847
+
1848
+ debounce(func, wait) {
1849
+ let timeout;
1850
+ return function executedFunction(...args) {
1851
+ const later = () => {
1852
+ clearTimeout(timeout);
1853
+ func(...args);
1854
+ };
1855
+ clearTimeout(timeout);
1856
+ timeout = setTimeout(later, wait);
1857
+ };
1858
+ }
1859
+
1860
+ showError(message) {
1861
+ // 可以根据需要实现错误提示UI
1862
+ alert(message);
1863
+ }
1864
+ async deleteFile(file) {
1865
+ try {
1866
+ const confirmed = await this.showConfirmDialog(
1867
+ '确认删除',
1868
+ `确定要删除文件 "${file.path.split('/').pop()}" 吗?此操作不可恢复。`
1869
+ );
1870
+
1871
+ if (!confirmed) return;
1872
+
1873
+ const response = await fetch(`/api/files/delete/${encodeURIComponent(file.path)}`, {
1874
+ method: 'DELETE',
1875
+ headers: {
1876
+ 'Content-Type': 'application/json'
1877
+ }
1878
+ });
1879
+
1880
+ if (!response.ok) {
1881
+ const errorData = await response.json();
1882
+ throw new Error(errorData.error || '删除失败');
1883
+ }
1884
+
1885
+ // Only proceed with refresh and success message if deletion was successful
1886
+ await this.loadFiles();
1887
+ this.showMessage(`文件 "${file.path.split('/').pop()}" 已成功删除`);
1888
+
1889
+ } catch (error) {
1890
+ console.error('Error deleting file:', error);
1891
+ this.showError('删除文件失败');
1892
+ }
1893
+ }
1894
+
1895
+ // 添加确认对话框的实现
1896
+ showConfirmDialog(title, message) {
1897
+ return new Promise((resolve) => {
1898
+ const modal = document.createElement('div');
1899
+ modal.className = 'confirm-modal';
1900
+ modal.innerHTML = `
1901
+ <div class="confirm-content">
1902
+ <h3>${title}</h3>
1903
+ <p>${message}</p>
1904
+ <div class="confirm-buttons">
1905
+ <button class="confirm-cancel">取消</button>
1906
+ <button class="confirm-ok">确定</button>
1907
+ </div>
1908
+ </div>
1909
+ `;
1910
+
1911
+ document.body.appendChild(modal);
1912
+
1913
+ const handleConfirm = (confirmed) => {
1914
+ modal.remove();
1915
+ resolve(confirmed);
1916
+ };
1917
+
1918
+ modal.querySelector('.confirm-cancel').addEventListener('click', () => handleConfirm(false));
1919
+ modal.querySelector('.confirm-ok').addEventListener('click', () => handleConfirm(true));
1920
+ });
1921
+ }
1922
+
1923
+ // 添加提示消息的实现
1924
+ showMessage(message) {
1925
+ const toast = document.createElement('div');
1926
+ toast.className = 'toast-message';
1927
+ toast.textContent = message;
1928
+
1929
+ document.body.appendChild(toast);
1930
+
1931
+ setTimeout(() => {
1932
+ toast.classList.add('show');
1933
+ setTimeout(() => {
1934
+ toast.classList.remove('show');
1935
+ setTimeout(() => toast.remove(), 300);
1936
+ }, 2000);
1937
+ }, 100);
1938
+ }
1939
+
1940
+ // 添加多选模式切换按钮
1941
+ addMultiSelectButton() {
1942
+ const multiSelectBtn = document.createElement('button');
1943
+ multiSelectBtn.className = 'multi-select-btn';
1944
+ multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i> 多选';
1945
+ multiSelectBtn.onclick = () => this.toggleMultiSelectMode();
1946
+
1947
+ document.querySelector('.view-toggle').appendChild(multiSelectBtn);
1948
+ }
1949
+
1950
+ // 切换多选模式
1951
+ toggleMultiSelectMode() {
1952
+ this.isMultiSelectMode = !this.isMultiSelectMode;
1953
+ this.selectedFiles.clear();
1954
+
1955
+ // 更新按钮状态
1956
+ const multiSelectBtn = document.querySelector('.multi-select-btn');
1957
+ multiSelectBtn.classList.toggle('active');
1958
+
1959
+ // 更新按钮文本
1960
+ if (this.isMultiSelectMode) {
1961
+ // 显示批量操作按钮
1962
+ this.showBatchOperations();
1963
+ multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>退出多选</span>';
1964
+ } else {
1965
+ // 隐藏批量操作按钮
1966
+ this.hideBatchOperations();
1967
+ multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>多选</span>';
1968
+ }
1969
+
1970
+ this.renderFiles();
1971
+ }
1972
+ showBatchOperations() {
1973
+ const batchOpsContainer = document.createElement('div');
1974
+ batchOpsContainer.className = 'batch-operations';
1975
+ batchOpsContainer.innerHTML = `
1976
+ <button class="action-btn batch-download-btn">
1977
+ <i class="fas fa-download"></i><span>批量下载</span>
1978
+ </button>
1979
+ <button class="action-btn batch-delete-btn">
1980
+ <i class="fas fa-trash"></i><span>批量删除</span>
1981
+ </button>
1982
+ `;
1983
+
1984
+ document.querySelector('.view-toggle').appendChild(batchOpsContainer);
1985
+
1986
+ // 绑定事件
1987
+ batchOpsContainer.querySelector('.batch-download-btn').onclick = () => this.batchDownload();
1988
+ batchOpsContainer.querySelector('.batch-delete-btn').onclick = () => this.batchDelete();
1989
+ }
1990
+
1991
+ hideBatchOperations() {
1992
+ const batchOps = document.querySelector('.batch-operations');
1993
+ if (batchOps) {
1994
+ batchOps.remove();
1995
+ }
1996
+ }
1997
+ // 批量下载
1998
+ async batchDownload() {
1999
+ for (const file of this.selectedFiles) {
2000
+ await this.downloadFile(file);
2001
+ }
2002
+ }
2003
+
2004
+ // 批量删除
2005
+ async batchDelete() {
2006
+ const confirmed = await this.showConfirmDialog(
2007
+ '批量删��',
2008
+ `确定要删除选中的 ${this.selectedFiles.size} 个文件吗?此操作不可恢复。`
2009
+ );
2010
+
2011
+ if (confirmed) {
2012
+ for (const file of this.selectedFiles) {
2013
+ await this.deleteFile(file);
2014
+ }
2015
+ }
2016
+ }
2017
+ async createFolder(folderName) {
2018
+ try {
2019
+ const response = await fetch('/api/files/create_folder', {
2020
+ method: 'POST',
2021
+ headers: {
2022
+ 'Content-Type': 'application/json'
2023
+ },
2024
+ body: JSON.stringify({
2025
+ path: this.currentPath,
2026
+ name: folderName
2027
+ })
2028
+ });
2029
+
2030
+ if (!response.ok) {
2031
+ throw new Error('Failed to create folder');
2032
+ }
2033
+
2034
+ await this.loadFiles();
2035
+ this.showMessage('文件夹创建成功');
2036
+
2037
+ } catch (error) {
2038
+ console.error('Error creating folder:', error);
2039
+ this.showError('创建文件夹失败');
2040
+ }
2041
+ }
2042
+
2043
+ // 显示创建文件夹对话框
2044
+ async showCreateFolderDialog() {
2045
+ const modal = document.createElement('div');
2046
+ modal.className = 'confirm-modal';
2047
+ modal.innerHTML = `
2048
+ <div class="confirm-content">
2049
+ <h3>新建文件夹</h3>
2050
+ <div class="input-container">
2051
+ <input type="text"
2052
+ class="folder-name-input"
2053
+ placeholder="请输入文件夹名称"
2054
+ maxlength="255">
2055
+ </div>
2056
+ <div class="confirm-buttons">
2057
+ <button class="confirm-cancel">取消</button>
2058
+ <button class="confirm-ok">创建</button>
2059
+ </div>
2060
+ </div>
2061
+ `;
2062
+
2063
+ document.body.appendChild(modal);
2064
+ const input = modal.querySelector('.folder-name-input');
2065
+ input.focus();
2066
+
2067
+ try {
2068
+ const folderName = await new Promise((resolve) => {
2069
+ const handleCreateFolder = () => {
2070
+ const name = input.value.trim();
2071
+ if (name) {
2072
+ resolve(name);
2073
+ }
2074
+ modal.remove();
2075
+ };
2076
+
2077
+ const handleCancel = () => {
2078
+ resolve(null);
2079
+ modal.remove();
2080
+ };
2081
+
2082
+ modal.querySelector('.confirm-ok').onclick = handleCreateFolder;
2083
+ modal.querySelector('.confirm-cancel').onclick = handleCancel;
2084
+
2085
+ input.onkeyup = (e) => {
2086
+ if (e.key === 'Enter') handleCreateFolder();
2087
+ if (e.key === 'Escape') handleCancel();
2088
+ };
2089
+ });
2090
+
2091
+ if (folderName) {
2092
+ await this.createFolder(folderName);
2093
+ }
2094
+ } catch (error) {
2095
+ console.error('Error creating folder:', error);
2096
+ this.showError('创建文件夹失败');
2097
+ }
2098
+ }
2099
+
2100
+ }
2101
+ async function handleLogout() {
2102
+ try {
2103
+ const response = await fetch('/logout');
2104
+ if (response.ok) {
2105
+ window.location.href = '/login';
2106
+ } else {
2107
+ throw new Error('Logout failed');
2108
+ }
2109
+ } catch (error) {
2110
+ console.error('Error during logout:', error);
2111
+ alert('退出登录失败,请重试');
2112
+ }
2113
+ }
2114
+ // 初始化文件管理器
2115
+ new FileManager();
2116
+ </script>
2117
+ </body>
2118
+ </html>