mock-service-cli 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1099 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>文件浏览器 - File Explorer</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .header {
27
+ background: white;
28
+ padding: 24px;
29
+ border-radius: 12px;
30
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
31
+ margin-bottom: 20px;
32
+ }
33
+
34
+ .header h1 {
35
+ color: #333;
36
+ font-size: 28px;
37
+ margin-bottom: 8px;
38
+ }
39
+
40
+ .breadcrumb {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 8px;
44
+ flex-wrap: wrap;
45
+ margin-top: 16px;
46
+ }
47
+
48
+ .breadcrumb-item {
49
+ padding: 6px 12px;
50
+ background: #f0f0f0;
51
+ border-radius: 6px;
52
+ font-size: 14px;
53
+ color: #666;
54
+ cursor: pointer;
55
+ transition: background 0.2s;
56
+ }
57
+
58
+ .breadcrumb-item:hover {
59
+ background: #e0e0e0;
60
+ }
61
+
62
+ .breadcrumb-item.current {
63
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64
+ color: white;
65
+ cursor: default;
66
+ }
67
+
68
+ .breadcrumb-separator {
69
+ color: #999;
70
+ }
71
+
72
+ .file-container {
73
+ background: white;
74
+ border-radius: 12px;
75
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
76
+ overflow: hidden;
77
+ }
78
+
79
+ .toolbar {
80
+ padding: 16px 20px;
81
+ border-bottom: 1px solid #eee;
82
+ display: flex;
83
+ gap: 12px;
84
+ align-items: center;
85
+ flex-wrap: wrap;
86
+ }
87
+
88
+ .search-box {
89
+ flex: 1;
90
+ min-width: 200px;
91
+ }
92
+
93
+ .search-box input {
94
+ width: 100%;
95
+ padding: 8px 16px;
96
+ border: 1px solid #ddd;
97
+ border-radius: 6px;
98
+ font-size: 14px;
99
+ outline: none;
100
+ transition: border-color 0.2s;
101
+ }
102
+
103
+ .search-box input:focus {
104
+ border-color: #667eea;
105
+ }
106
+
107
+ .sort-controls {
108
+ display: flex;
109
+ gap: 8px;
110
+ align-items: center;
111
+ }
112
+
113
+ .sort-select {
114
+ padding: 8px 12px;
115
+ border: 1px solid #ddd;
116
+ border-radius: 6px;
117
+ font-size: 14px;
118
+ cursor: pointer;
119
+ outline: none;
120
+ transition: border-color 0.2s;
121
+ }
122
+
123
+ .sort-select:focus {
124
+ border-color: #667eea;
125
+ }
126
+
127
+ .sort-order-btn {
128
+ padding: 8px 12px;
129
+ background: #f0f0f0;
130
+ border: none;
131
+ border-radius: 6px;
132
+ font-size: 14px;
133
+ cursor: pointer;
134
+ transition: background 0.2s;
135
+ }
136
+
137
+ .sort-order-btn:hover {
138
+ background: #e0e0e0;
139
+ }
140
+
141
+ .back-btn {
142
+ padding: 8px 16px;
143
+ background: #f0f0f0;
144
+ border: none;
145
+ border-radius: 6px;
146
+ font-size: 14px;
147
+ cursor: pointer;
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 6px;
151
+ transition: background 0.2s;
152
+ }
153
+
154
+ .back-btn:hover:not(:disabled) {
155
+ background: #e0e0e0;
156
+ }
157
+
158
+ .back-btn:disabled {
159
+ opacity: 0.5;
160
+ cursor: not-allowed;
161
+ }
162
+
163
+ .view-toggle {
164
+ display: flex;
165
+ background: #f0f0f0;
166
+ border-radius: 6px;
167
+ padding: 4px;
168
+ }
169
+
170
+ .view-btn {
171
+ padding: 6px 12px;
172
+ background: transparent;
173
+ border: none;
174
+ border-radius: 4px;
175
+ cursor: pointer;
176
+ font-size: 14px;
177
+ transition: all 0.2s;
178
+ }
179
+
180
+ .view-btn.active {
181
+ background: white;
182
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
183
+ }
184
+
185
+ .refresh-btn {
186
+ padding: 8px 12px;
187
+ background: #f0f0f0;
188
+ border: none;
189
+ border-radius: 6px;
190
+ font-size: 16px;
191
+ cursor: pointer;
192
+ transition: background 0.2s;
193
+ }
194
+
195
+ .refresh-btn:hover {
196
+ background: #e0e0e0;
197
+ }
198
+
199
+ .toggle-hidden-btn {
200
+ padding: 8px 12px;
201
+ background: #f0f0f0;
202
+ border: none;
203
+ border-radius: 6px;
204
+ font-size: 14px;
205
+ cursor: pointer;
206
+ transition: background 0.2s;
207
+ }
208
+
209
+ .toggle-hidden-btn:hover {
210
+ background: #e0e0e0;
211
+ }
212
+
213
+ .toggle-hidden-btn.active {
214
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
215
+ color: white;
216
+ }
217
+
218
+ .copy-btn {
219
+ padding: 4px 8px;
220
+ background: #f0f0f0;
221
+ border: none;
222
+ border-radius: 4px;
223
+ font-size: 12px;
224
+ cursor: pointer;
225
+ transition: all 0.2s;
226
+ }
227
+
228
+ .copy-btn:hover {
229
+ background: #ffc107;
230
+ color: #333;
231
+ }
232
+
233
+ .file-grid {
234
+ padding: 20px;
235
+ display: grid;
236
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
237
+ gap: 16px;
238
+ }
239
+
240
+ .file-grid.list-view {
241
+ grid-template-columns: 1fr;
242
+ gap: 0;
243
+ }
244
+
245
+ .file-item {
246
+ display: flex;
247
+ flex-direction: column;
248
+ align-items: center;
249
+ padding: 16px;
250
+ border-radius: 8px;
251
+ cursor: pointer;
252
+ transition: all 0.2s;
253
+ }
254
+
255
+ .file-item:hover {
256
+ background: #f8f9fa;
257
+ }
258
+
259
+ .list-view .file-item {
260
+ flex-direction: row;
261
+ padding: 12px 16px;
262
+ border-bottom: 1px solid #f0f0f0;
263
+ border-radius: 0;
264
+ }
265
+
266
+ .file-icon {
267
+ width: 64px;
268
+ height: 64px;
269
+ margin-bottom: 12px;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ }
274
+
275
+ .list-view .file-icon {
276
+ width: 32px;
277
+ height: 32px;
278
+ margin-bottom: 0;
279
+ margin-right: 12px;
280
+ }
281
+
282
+ .file-icon svg {
283
+ width: 100%;
284
+ height: 100%;
285
+ }
286
+
287
+ .file-info {
288
+ text-align: center;
289
+ word-break: break-all;
290
+ }
291
+
292
+ .list-view .file-info {
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 16px;
296
+ text-align: left;
297
+ flex: 1;
298
+ }
299
+
300
+ .file-name {
301
+ font-size: 14px;
302
+ color: #333;
303
+ font-weight: 500;
304
+ margin-bottom: 4px;
305
+ }
306
+
307
+ .list-view .file-name {
308
+ margin-bottom: 0;
309
+ flex: 1;
310
+ }
311
+
312
+ .file-meta {
313
+ font-size: 12px;
314
+ color: #999;
315
+ }
316
+
317
+ .list-view .file-meta {
318
+ display: flex;
319
+ gap: 16px;
320
+ }
321
+
322
+ .hidden-file {
323
+ opacity: 0.6;
324
+ }
325
+
326
+ .action-buttons {
327
+ display: none;
328
+ gap: 6px;
329
+ }
330
+
331
+ .list-view .action-buttons {
332
+ display: flex;
333
+ }
334
+
335
+ .action-btn {
336
+ padding: 4px 8px;
337
+ background: #f0f0f0;
338
+ border: none;
339
+ border-radius: 4px;
340
+ font-size: 12px;
341
+ cursor: pointer;
342
+ transition: all 0.2s;
343
+ }
344
+
345
+ .action-btn:hover {
346
+ background: #e0e0e0;
347
+ }
348
+
349
+ .action-btn.open:hover {
350
+ background: #667eea;
351
+ color: white;
352
+ }
353
+
354
+ .action-btn.download:hover {
355
+ background: #28a745;
356
+ color: white;
357
+ }
358
+
359
+ .modal {
360
+ display: none;
361
+ position: fixed;
362
+ top: 0;
363
+ left: 0;
364
+ right: 0;
365
+ bottom: 0;
366
+ background: rgba(0, 0, 0, 0.8);
367
+ z-index: 1000;
368
+ padding: 20px;
369
+ overflow: auto;
370
+ }
371
+
372
+ .modal.active {
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ }
377
+
378
+ .modal-content {
379
+ background: white;
380
+ border-radius: 12px;
381
+ max-width: 900px;
382
+ width: 100%;
383
+ max-height: 90vh;
384
+ overflow: hidden;
385
+ display: flex;
386
+ flex-direction: column;
387
+ }
388
+
389
+ .modal-header {
390
+ display: flex;
391
+ justify-content: space-between;
392
+ align-items: center;
393
+ padding: 16px 20px;
394
+ border-bottom: 1px solid #eee;
395
+ }
396
+
397
+ .modal-title {
398
+ font-size: 18px;
399
+ font-weight: 600;
400
+ color: #333;
401
+ word-break: break-all;
402
+ }
403
+
404
+ .close-btn {
405
+ width: 32px;
406
+ height: 32px;
407
+ border: none;
408
+ background: #f0f0f0;
409
+ border-radius: 6px;
410
+ cursor: pointer;
411
+ font-size: 18px;
412
+ color: #666;
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ transition: background 0.2s;
417
+ }
418
+
419
+ .close-btn:hover {
420
+ background: #e0e0e0;
421
+ }
422
+
423
+ .modal-body {
424
+ padding: 20px;
425
+ overflow: auto;
426
+ flex: 1;
427
+ }
428
+
429
+ .image-preview {
430
+ max-width: 100%;
431
+ max-height: 70vh;
432
+ display: block;
433
+ margin: 0 auto;
434
+ }
435
+
436
+ .text-preview {
437
+ background: #1e1e1e;
438
+ color: #d4d4d4;
439
+ padding: 20px;
440
+ border-radius: 8px;
441
+ max-height: 70vh;
442
+ overflow: auto;
443
+ font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
444
+ font-size: 13px;
445
+ line-height: 1.6;
446
+ white-space: pre-wrap;
447
+ word-wrap: break-word;
448
+ margin: 0;
449
+ }
450
+
451
+ .preview-container {
452
+ display: flex;
453
+ flex-direction: column;
454
+ height: 100%;
455
+ }
456
+
457
+ .preview-header {
458
+ display: flex;
459
+ justify-content: flex-end;
460
+ margin-bottom: 12px;
461
+ }
462
+
463
+ .preview-header .copy-btn {
464
+ padding: 8px 16px;
465
+ background: linear-gradient(135deg, #8b5cf6, #6366f1);
466
+ color: white;
467
+ border: none;
468
+ border-radius: 6px;
469
+ font-size: 14px;
470
+ cursor: pointer;
471
+ transition: all 0.2s;
472
+ display: flex;
473
+ align-items: center;
474
+ gap: 6px;
475
+ box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3);
476
+ }
477
+
478
+ .preview-header .copy-btn:hover {
479
+ transform: translateY(-1px);
480
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
481
+ background: linear-gradient(135deg, #7c3aed, #4f46e5);
482
+ }
483
+
484
+ .preview-header .copy-btn:active {
485
+ transform: translateY(0);
486
+ }
487
+
488
+ .empty-state {
489
+ text-align: center;
490
+ padding: 60px 20px;
491
+ color: #999;
492
+ }
493
+
494
+ .empty-state svg {
495
+ width: 100px;
496
+ height: 100px;
497
+ margin-bottom: 20px;
498
+ opacity: 0.5;
499
+ }
500
+
501
+ .loading {
502
+ text-align: center;
503
+ padding: 60px 20px;
504
+ color: #666;
505
+ }
506
+
507
+ .spinner {
508
+ display: inline-block;
509
+ width: 40px;
510
+ height: 40px;
511
+ border: 3px solid #f0f0f0;
512
+ border-radius: 50%;
513
+ border-top-color: #667eea;
514
+ animation: spin 0.8s linear infinite;
515
+ margin-bottom: 16px;
516
+ }
517
+
518
+ @keyframes spin {
519
+ to {
520
+ transform: rotate(360deg);
521
+ }
522
+ }
523
+ </style>
524
+ </head>
525
+ <body>
526
+ <div class="container">
527
+ <div class="header">
528
+ <h1>📁 文件浏览器</h1>
529
+ <div class="breadcrumb" id="breadcrumb"></div>
530
+ </div>
531
+
532
+ <div class="file-container">
533
+ <div class="toolbar">
534
+ <button class="back-btn" id="backBtn" onclick="goBack()">
535
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
536
+ <polyline points="15 18 9 12 15 6"></polyline>
537
+ </svg>
538
+ 返回上级
539
+ </button>
540
+ <button class="refresh-btn" onclick="loadDirectory(currentPath)" title="刷新">🔄</button>
541
+ <button
542
+ class="toggle-hidden-btn"
543
+ id="toggleHiddenBtn"
544
+ onclick="toggleHiddenFiles()"
545
+ title="显示/隐藏隐藏文件"
546
+ >
547
+ 👁️
548
+ </button>
549
+ <div class="search-box">
550
+ <input type="text" id="searchInput" placeholder="搜索文件名..." oninput="handleSearch()" />
551
+ </div>
552
+ <div class="sort-controls">
553
+ <select class="sort-select" id="sortSelect" onchange="handleSort()">
554
+ <option value="name">名称</option>
555
+ <option value="time">时间</option>
556
+ <option value="size">大小</option>
557
+ </select>
558
+ <button class="sort-order-btn" id="sortOrderBtn" onclick="toggleSortOrder()">
559
+ <span id="sortOrderIcon">↑</span>
560
+ </button>
561
+ </div>
562
+ <div class="view-toggle">
563
+ <button class="view-btn active" id="gridViewBtn" onclick="setView('grid')">网格</button>
564
+ <button class="view-btn" id="listViewBtn" onclick="setView('list')">列表</button>
565
+ </div>
566
+ </div>
567
+ <div class="file-grid" id="fileGrid"></div>
568
+ </div>
569
+ </div>
570
+
571
+ <div class="modal" id="previewModal">
572
+ <div class="modal-content">
573
+ <div class="modal-header">
574
+ <div class="modal-title" id="previewTitle"></div>
575
+ <button class="close-btn" onclick="closeModal()">×</button>
576
+ </div>
577
+ <div class="modal-body" id="previewBody"></div>
578
+ </div>
579
+ </div>
580
+
581
+ <script>
582
+ let currentPath = '/';
583
+ let currentView = 'grid';
584
+ let currentFiles = [];
585
+ let allFiles = [];
586
+ let searchQuery = '';
587
+ let sortBy = 'name';
588
+ let sortAscending = true;
589
+ let showHiddenFiles = true;
590
+ let currentPreviewContent = '';
591
+
592
+ document.addEventListener('DOMContentLoaded', () => {
593
+ loadDirectory('/');
594
+ });
595
+
596
+ async function loadDirectory(path) {
597
+ // console.log('Loading directory:', path);
598
+ const fileGrid = document.getElementById('fileGrid');
599
+
600
+ // 重置所有状态
601
+ currentPath = '/';
602
+ allFiles = [];
603
+ currentFiles = [];
604
+
605
+ // 显示加载状态
606
+ fileGrid.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
607
+
608
+ try {
609
+ const response = await fetch(`/__api/list?path=${encodeURIComponent(path)}`);
610
+ // console.log('Response status:', response.status);
611
+
612
+ const data = await response.json();
613
+ // console.log('API response data:', data);
614
+
615
+ if (data.error) {
616
+ throw new Error(data.error);
617
+ }
618
+
619
+ currentPath = data.currentPath;
620
+ allFiles = data.files || [];
621
+
622
+ // console.log('allFiles set to:', allFiles);
623
+
624
+ renderBreadcrumb(data.currentPath, data.parentPath);
625
+
626
+ // 确保在调用 processAndRenderFiles 之前 allFiles 已经设置好
627
+ setTimeout(() => {
628
+ processAndRenderFiles();
629
+ }, 10);
630
+
631
+ updateBackButton(data.parentPath);
632
+
633
+ // 清空搜索框
634
+ if (document.getElementById('searchInput')) {
635
+ document.getElementById('searchInput').value = '';
636
+ }
637
+ searchQuery = '';
638
+ } catch (error) {
639
+ console.error('Error loading directory:', error);
640
+ fileGrid.innerHTML = `<div class="empty-state"><p>错误: ${error.message}</p></div>`;
641
+ }
642
+ }
643
+
644
+ function processAndRenderFiles() {
645
+ // console.log('processAndRenderFiles called, allFiles:', allFiles);
646
+
647
+ if (!allFiles) {
648
+ console.error('allFiles is null or undefined');
649
+ renderFiles([]);
650
+ return;
651
+ }
652
+
653
+ let files = [...allFiles];
654
+
655
+ // 隐藏文件过滤
656
+ if (!showHiddenFiles) {
657
+ files = files.filter(file => !file.isHidden);
658
+ }
659
+
660
+ // 搜索过滤
661
+ if (searchQuery) {
662
+ const query = searchQuery.toLowerCase();
663
+ files = files.filter(file => file.name.toLowerCase().includes(query));
664
+ }
665
+
666
+ // 排序
667
+ files.sort((a, b) => {
668
+ let result = 0;
669
+
670
+ // 目录始终排在前面
671
+ if (a.isDirectory !== b.isDirectory) {
672
+ return a.isDirectory ? -1 : 1;
673
+ }
674
+
675
+ switch (sortBy) {
676
+ case 'name':
677
+ result = a.name.localeCompare(b.name);
678
+ break;
679
+ case 'time':
680
+ result = new Date(a.mtime) - new Date(b.mtime);
681
+ break;
682
+ case 'size':
683
+ result = a.size - b.size;
684
+ break;
685
+ }
686
+
687
+ return sortAscending ? result : -result;
688
+ });
689
+
690
+ currentFiles = files;
691
+ // console.log('Rendering', files.length, 'files');
692
+ renderFiles(files);
693
+ }
694
+
695
+ function toggleHiddenFiles() {
696
+ showHiddenFiles = !showHiddenFiles;
697
+ const btn = document.getElementById('toggleHiddenBtn');
698
+ if (showHiddenFiles) {
699
+ btn.classList.add('active');
700
+ btn.innerHTML = '👁️';
701
+ btn.title = '隐藏隐藏文件';
702
+ } else {
703
+ btn.classList.remove('active');
704
+ btn.innerHTML = '👁️‍🗨️';
705
+ btn.title = '显示隐藏文件';
706
+ }
707
+ processAndRenderFiles();
708
+ }
709
+
710
+ async function copyToClipboard(text) {
711
+ try {
712
+ await navigator.clipboard.writeText(text);
713
+ alert('路径已复制到剪贴板: ' + text);
714
+ } catch (error) {
715
+ console.error('复制失败:', error);
716
+ alert('复制失败: ' + error.message);
717
+ }
718
+ }
719
+
720
+ function handleSearch() {
721
+ searchQuery = document.getElementById('searchInput').value;
722
+ processAndRenderFiles();
723
+ }
724
+
725
+ function handleSort() {
726
+ sortBy = document.getElementById('sortSelect').value;
727
+ processAndRenderFiles();
728
+ }
729
+
730
+ function toggleSortOrder() {
731
+ sortAscending = !sortAscending;
732
+ document.getElementById('sortOrderIcon').textContent = sortAscending ? '↑' : '↓';
733
+ processAndRenderFiles();
734
+ }
735
+
736
+ async function openInExplorer(path) {
737
+ try {
738
+ const response = await fetch('/__api/open-in-explorer', {
739
+ method: 'POST',
740
+ headers: {
741
+ 'Content-Type': 'application/json'
742
+ },
743
+ body: JSON.stringify({ path: path })
744
+ });
745
+
746
+ const data = await response.json();
747
+
748
+ if (!data.success) {
749
+ alert('打开失败: ' + (data.error || '未知错误'));
750
+ }
751
+ } catch (error) {
752
+ console.error('打开失败:', error);
753
+ alert('打开失败: ' + error.message);
754
+ }
755
+ }
756
+
757
+ function downloadFile(filePath, fileName) {
758
+ const url = `/__api/file?path=${encodeURIComponent(filePath)}`;
759
+ const a = document.createElement('a');
760
+ a.href = url;
761
+ a.download = fileName;
762
+ document.body.appendChild(a);
763
+ a.click();
764
+ document.body.removeChild(a);
765
+ }
766
+
767
+ function renderBreadcrumb(currentPath, parentPath) {
768
+ const breadcrumb = document.getElementById('breadcrumb');
769
+ const parts = currentPath.split('/').filter(Boolean);
770
+
771
+ let html = '<div class="breadcrumb-item" onclick="loadDirectory(\'/\')">根目录</div>';
772
+
773
+ let current = '';
774
+ parts.forEach((part, index) => {
775
+ current += '/' + part;
776
+ const isLast = index === parts.length - 1;
777
+ html += '<span class="breadcrumb-separator">/</span>';
778
+ if (isLast) {
779
+ html += `<div class="breadcrumb-item current">${escapeHtml(part)}</div>`;
780
+ } else {
781
+ html += `<div class="breadcrumb-item" onclick="loadDirectory('${current}')">${escapeHtml(part)}</div>`;
782
+ }
783
+ });
784
+
785
+ breadcrumb.innerHTML = html;
786
+ }
787
+
788
+ function renderFiles(files) {
789
+ const fileGrid = document.getElementById('fileGrid');
790
+
791
+ // 确保视图类名正确
792
+ if (currentView === 'list') {
793
+ fileGrid.classList.add('list-view');
794
+ } else {
795
+ fileGrid.classList.remove('list-view');
796
+ }
797
+
798
+ if (!files || files.length === 0) {
799
+ fileGrid.innerHTML = '<div class="empty-state"><p>该目录为空</p></div>';
800
+ return;
801
+ }
802
+
803
+ let html = '';
804
+ files.forEach(file => {
805
+ const icon = getFileIcon(file);
806
+ const size = formatSize(file.size);
807
+ const mtime = formatDate(file.mtime);
808
+ const hiddenClass = file.isHidden ? 'hidden-file' : '';
809
+
810
+ if (currentView === 'list') {
811
+ html += `
812
+ <div class="file-item ${hiddenClass}">
813
+ <div class="file-icon" onclick="handleItemClick('${file.path}', ${
814
+ file.isDirectory
815
+ })">${icon}</div>
816
+ <div class="file-info" onclick="handleItemClick('${file.path}', ${file.isDirectory})">
817
+ <div class="file-name">${escapeHtml(file.name)}</div>
818
+ <div class="file-meta">
819
+ ${file.isDirectory ? '<span>目录</span>' : `<span>${size}</span>`}
820
+ <span>${mtime}</span>
821
+ </div>
822
+ </div>
823
+ <div class="action-buttons">
824
+ <button class="copy-btn" onclick="event.stopPropagation(); copyToClipboard('${
825
+ file.path
826
+ }')" title="复制路径">📋</button>
827
+ <button class="action-btn open" onclick="event.stopPropagation(); openInExplorer('${
828
+ file.path
829
+ }')" title="在文件管理器中打开">📂</button>
830
+ ${
831
+ !file.isDirectory
832
+ ? `<button class="action-btn download" onclick="event.stopPropagation(); downloadFile('${
833
+ file.path
834
+ }', '${escapeHtml(file.name)}')" title="下载">⬇️</button>`
835
+ : ''
836
+ }
837
+ </div>
838
+ </div>
839
+ `;
840
+ } else {
841
+ html += `
842
+ <div class="file-item ${hiddenClass}" onclick="handleItemClick('${file.path}', ${
843
+ file.isDirectory
844
+ })">
845
+ <div class="file-icon">${icon}</div>
846
+ <div class="file-info">
847
+ <div class="file-name">${escapeHtml(file.name)}</div>
848
+ <div class="file-meta">
849
+ ${file.isDirectory ? '目录' : size}
850
+ </div>
851
+ </div>
852
+ </div>
853
+ `;
854
+ }
855
+ });
856
+
857
+ // 强制更新 DOM
858
+ fileGrid.innerHTML = html;
859
+ }
860
+
861
+ function handleItemClick(path, isDirectory) {
862
+ if (isDirectory) {
863
+ loadDirectory(path);
864
+ } else {
865
+ previewFile(path);
866
+ }
867
+ }
868
+
869
+ async function copyText(text) {
870
+ try {
871
+ if (navigator.clipboard && navigator.clipboard.writeText) {
872
+ await navigator.clipboard.writeText(text);
873
+ showToast('已复制到剪贴板');
874
+ } else {
875
+ const textarea = document.createElement('textarea');
876
+ textarea.value = text;
877
+ textarea.style.position = 'fixed';
878
+ textarea.style.top = '-9999px';
879
+ document.body.appendChild(textarea);
880
+ textarea.select();
881
+ document.execCommand('copy');
882
+ document.body.removeChild(textarea);
883
+ showToast('已复制到剪贴板');
884
+ }
885
+ } catch (error) {
886
+ console.error('复制失败:', error);
887
+ showToast('复制失败');
888
+ }
889
+ }
890
+
891
+ function copyPreviewContent() {
892
+ if (currentPreviewContent !== '') {
893
+ copyText(currentPreviewContent);
894
+ }
895
+ }
896
+
897
+ function showToast(message) {
898
+ const toast = document.createElement('div');
899
+ toast.className = 'toast';
900
+ toast.textContent = message;
901
+ toast.style.cssText = `
902
+ position: fixed;
903
+ top: 20px;
904
+ right: 20px;
905
+ background: rgba(139, 92, 246, 0.95);
906
+ color: white;
907
+ padding: 12px 24px;
908
+ border-radius: 8px;
909
+ font-size: 14px;
910
+ z-index: 10000;
911
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
912
+ animation: fadeInOut 2s ease-in-out;
913
+ `;
914
+ document.body.appendChild(toast);
915
+
916
+ const style = document.createElement('style');
917
+ style.textContent = `
918
+ @keyframes fadeInOut {
919
+ 0% { opacity: 0; transform: translateY(-20px); }
920
+ 15% { opacity: 1; transform: translateY(0); }
921
+ 85% { opacity: 1; transform: translateY(0); }
922
+ 100% { opacity: 0; transform: translateY(-20px); }
923
+ }
924
+ `;
925
+ document.head.appendChild(style);
926
+
927
+ setTimeout(() => {
928
+ if (toast.parentNode) {
929
+ document.body.removeChild(toast);
930
+ }
931
+ if (style.parentNode) {
932
+ document.head.removeChild(style);
933
+ }
934
+ }, 2000);
935
+ }
936
+
937
+ async function previewFile(path) {
938
+ const modal = document.getElementById('previewModal');
939
+ const title = document.getElementById('previewTitle');
940
+ const body = document.getElementById('previewBody');
941
+
942
+ title.textContent = path.split('/').pop();
943
+ body.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
944
+ modal.classList.add('active');
945
+
946
+ try {
947
+ const ext = path.split('.').pop().toLowerCase();
948
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
949
+
950
+ if (imageExts.includes(ext)) {
951
+ body.innerHTML = `<img src="/__api/file?path=${encodeURIComponent(
952
+ path
953
+ )}" class="image-preview" alt="Preview">`;
954
+ } else {
955
+ const response = await fetch(`/__api/file?path=${encodeURIComponent(path)}`);
956
+ const contentType = response.headers.get('content-type');
957
+
958
+ if (contentType && contentType.includes('application/json')) {
959
+ const data = await response.json();
960
+ if (data.type === 'text') {
961
+ currentPreviewContent = data.content;
962
+ body.innerHTML = `
963
+ <div class="preview-container">
964
+ <div class="preview-header">
965
+ <button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
966
+ </div>
967
+ <pre class="text-preview">${escapeHtml(data.content)}</pre>
968
+ </div>
969
+ `;
970
+ } else {
971
+ body.innerHTML = `<div class="empty-state"><p>该文件类型暂不支持预览</p><p><a href="/__api/file?path=${encodeURIComponent(
972
+ path
973
+ )}" download>点击下载</a></p></div>`;
974
+ }
975
+ } else {
976
+ const blob = await response.blob();
977
+ const url = URL.createObjectURL(blob);
978
+ body.innerHTML = `<div class="empty-state"><p>该文件类型暂不支持预览</p><p><a href="${url}" download="${path
979
+ .split('/')
980
+ .pop()}">点击下载</a></p></div>`;
981
+ }
982
+ }
983
+ } catch (error) {
984
+ body.innerHTML = `<div class="empty-state"><p>预览失败: ${error.message}</p></div>`;
985
+ }
986
+ }
987
+
988
+ function closeModal() {
989
+ document.getElementById('previewModal').classList.remove('active');
990
+ }
991
+
992
+ function goBack() {
993
+ if (currentPath !== '/') {
994
+ const dirname = currentPath.split('/').slice(0, -1).join('/') || '/';
995
+ loadDirectory(dirname);
996
+ }
997
+ }
998
+
999
+ function updateBackButton(parentPath) {
1000
+ document.getElementById('backBtn').disabled = !parentPath;
1001
+ }
1002
+
1003
+ function setView(view) {
1004
+ currentView = view;
1005
+ const fileGrid = document.getElementById('fileGrid');
1006
+ const gridBtn = document.getElementById('gridViewBtn');
1007
+ const listBtn = document.getElementById('listViewBtn');
1008
+
1009
+ if (view === 'list') {
1010
+ fileGrid.classList.add('list-view');
1011
+ listBtn.classList.add('active');
1012
+ gridBtn.classList.remove('active');
1013
+ } else {
1014
+ fileGrid.classList.remove('list-view');
1015
+ gridBtn.classList.add('active');
1016
+ listBtn.classList.remove('active');
1017
+ }
1018
+
1019
+ renderFiles(currentFiles);
1020
+ }
1021
+
1022
+ function getFileIcon(file) {
1023
+ if (file.isDirectory) {
1024
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="#667eea" stroke-width="2">
1025
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
1026
+ </svg>`;
1027
+ }
1028
+
1029
+ const ext = file.name.split('.').pop().toLowerCase();
1030
+ const iconColors = {
1031
+ js: '#f7df1e',
1032
+ json: '#292929',
1033
+ html: '#e34f26',
1034
+ css: '#264de4',
1035
+ md: '#083fa1',
1036
+ jpg: '#ff6b6b',
1037
+ jpeg: '#ff6b6b',
1038
+ png: '#ff6b6b',
1039
+ gif: '#ff6b6b',
1040
+ svg: '#ff6b6b',
1041
+ zip: '#4ecdc4',
1042
+ '7z': '#4ecdc4',
1043
+ rar: '#4ecdc4',
1044
+ tar: '#4ecdc4',
1045
+ gz: '#4ecdc4',
1046
+ txt: '#95a5a6',
1047
+ log: '#95a5a6',
1048
+ py: '#3776ab',
1049
+ java: '#007396',
1050
+ go: '#00add8',
1051
+ rs: '#dea584',
1052
+ ts: '#3178c6'
1053
+ };
1054
+
1055
+ const color = iconColors[ext] || '#95a5a6';
1056
+
1057
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2">
1058
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
1059
+ <polyline points="14 2 14 8 20 8"></polyline>
1060
+ </svg>`;
1061
+ }
1062
+
1063
+ function formatSize(bytes) {
1064
+ if (bytes === 0) return '0 B';
1065
+ const k = 1024;
1066
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1067
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1068
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1069
+ }
1070
+
1071
+ function formatDate(dateStr) {
1072
+ const date = new Date(dateStr);
1073
+ return date.toLocaleString('zh-CN');
1074
+ }
1075
+
1076
+ function escapeHtml(text) {
1077
+ const div = document.createElement('div');
1078
+ div.textContent = text;
1079
+ return div.innerHTML;
1080
+ }
1081
+
1082
+ document.getElementById('previewModal').addEventListener('click', e => {
1083
+ if (e.target === e.currentTarget) {
1084
+ closeModal();
1085
+ }
1086
+ });
1087
+
1088
+ document.addEventListener('keydown', e => {
1089
+ if (e.key === 'Escape') {
1090
+ closeModal();
1091
+ }
1092
+ if (e.key === 'Backspace' && document.activeElement.tagName !== 'INPUT') {
1093
+ e.preventDefault();
1094
+ goBack();
1095
+ }
1096
+ });
1097
+ </script>
1098
+ </body>
1099
+ </html>