@huyooo/file-explorer-frontend-vue 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/components/Breadcrumb.vue.d.ts +11 -0
  2. package/dist/components/Breadcrumb.vue.d.ts.map +1 -0
  3. package/dist/components/CompressDialog.vue.d.ts +16 -0
  4. package/dist/components/CompressDialog.vue.d.ts.map +1 -0
  5. package/dist/components/ContextMenu.vue.d.ts +18 -0
  6. package/dist/components/ContextMenu.vue.d.ts.map +1 -0
  7. package/dist/components/FileGrid.vue.d.ts +40 -0
  8. package/dist/components/FileGrid.vue.d.ts.map +1 -0
  9. package/dist/components/FileIcon.vue.d.ts +13 -0
  10. package/dist/components/FileIcon.vue.d.ts.map +1 -0
  11. package/dist/components/FileInfoDialog.vue.d.ts +14 -0
  12. package/dist/components/FileInfoDialog.vue.d.ts.map +1 -0
  13. package/dist/components/FileList.vue.d.ts +37 -0
  14. package/dist/components/FileList.vue.d.ts.map +1 -0
  15. package/dist/components/FileListView.vue.d.ts +43 -0
  16. package/dist/components/FileListView.vue.d.ts.map +1 -0
  17. package/dist/components/FileSidebar.vue.d.ts +17 -0
  18. package/dist/components/FileSidebar.vue.d.ts.map +1 -0
  19. package/dist/components/ProgressDialog.vue.d.ts +28 -0
  20. package/dist/components/ProgressDialog.vue.d.ts.map +1 -0
  21. package/dist/components/SortIndicator.vue.d.ts +6 -0
  22. package/dist/components/SortIndicator.vue.d.ts.map +1 -0
  23. package/dist/components/StatusBar.vue.d.ts +27 -0
  24. package/dist/components/StatusBar.vue.d.ts.map +1 -0
  25. package/dist/components/Toolbar.vue.d.ts +60 -0
  26. package/dist/components/Toolbar.vue.d.ts.map +1 -0
  27. package/dist/components/Window.vue.d.ts +65 -0
  28. package/dist/components/Window.vue.d.ts.map +1 -0
  29. package/dist/composables/useApplicationIcon.d.ts +16 -0
  30. package/dist/composables/useApplicationIcon.d.ts.map +1 -0
  31. package/dist/composables/useDragAndDrop.d.ts +14 -0
  32. package/dist/composables/useDragAndDrop.d.ts.map +1 -0
  33. package/dist/composables/useMediaPlayer.d.ts +24 -0
  34. package/dist/composables/useMediaPlayer.d.ts.map +1 -0
  35. package/dist/composables/useSelection.d.ts +15 -0
  36. package/dist/composables/useSelection.d.ts.map +1 -0
  37. package/dist/composables/useWindowDrag.d.ts +18 -0
  38. package/dist/composables/useWindowDrag.d.ts.map +1 -0
  39. package/dist/composables/useWindowResize.d.ts +12 -0
  40. package/dist/composables/useWindowResize.d.ts.map +1 -0
  41. package/dist/index.css +1 -0
  42. package/dist/index.d.ts +22 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +4051 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/types/index.d.ts +268 -0
  47. package/dist/types/index.d.ts.map +1 -0
  48. package/dist/utils/fileTypeIcon.d.ts +6 -0
  49. package/dist/utils/fileTypeIcon.d.ts.map +1 -0
  50. package/dist/utils/folderTypeIcon.d.ts +14 -0
  51. package/dist/utils/folderTypeIcon.d.ts.map +1 -0
  52. package/package.json +55 -0
  53. package/src/components/Breadcrumb.vue +111 -0
  54. package/src/components/CompressDialog.vue +478 -0
  55. package/src/components/ContextMenu.vue +550 -0
  56. package/src/components/FileGrid.vue +504 -0
  57. package/src/components/FileIcon.vue +132 -0
  58. package/src/components/FileInfoDialog.vue +465 -0
  59. package/src/components/FileList.vue +421 -0
  60. package/src/components/FileListView.vue +321 -0
  61. package/src/components/FileSidebar.vue +158 -0
  62. package/src/components/ProgressDialog.vue +368 -0
  63. package/src/components/SortIndicator.vue +22 -0
  64. package/src/components/StatusBar.vue +43 -0
  65. package/src/components/Toolbar.vue +271 -0
  66. package/src/components/Window.vue +561 -0
  67. package/src/composables/useApplicationIcon.ts +79 -0
  68. package/src/composables/useDragAndDrop.ts +103 -0
  69. package/src/composables/useMediaPlayer.ts +174 -0
  70. package/src/composables/useSelection.ts +107 -0
  71. package/src/composables/useWindowDrag.ts +66 -0
  72. package/src/composables/useWindowResize.ts +134 -0
  73. package/src/index.ts +32 -0
  74. package/src/types/index.ts +273 -0
  75. package/src/utils/fileTypeIcon.ts +309 -0
  76. package/src/utils/folderTypeIcon.ts +132 -0
@@ -0,0 +1,561 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ class="window-overlay"
5
+ @click="handleBackdropClick"
6
+ >
7
+ <div
8
+ ref="windowContainerRef"
9
+ class="window-container"
10
+ :style="windowStyle"
11
+ @click.stop
12
+ >
13
+ <!-- Title Bar -->
14
+ <div
15
+ v-if="showTitleBar"
16
+ class="window-title-bar draggable-area"
17
+ @mousedown="handleDragStart"
18
+ >
19
+ <div class="window-controls">
20
+ <button
21
+ @click.stop="handleClose"
22
+ class="window-control-button window-control-button--close"
23
+ >
24
+ <X :size="7" class="window-control-icon" />
25
+ </button>
26
+ <button
27
+ v-if="showMinimize"
28
+ @click.stop="handleMinimize"
29
+ class="window-control-button window-control-button--minimize"
30
+ >
31
+ <Minus :size="7" class="window-control-icon" />
32
+ </button>
33
+ <button
34
+ v-if="showMaximize"
35
+ @click.stop="handleMaximize"
36
+ class="window-control-button window-control-button--maximize"
37
+ >
38
+ <Maximize2 :size="7" class="window-control-icon" />
39
+ </button>
40
+ </div>
41
+
42
+ <div class="window-title-info">
43
+ <slot name="title">
44
+ <span class="window-title-text">{{ title }}</span>
45
+ </slot>
46
+ </div>
47
+
48
+ <div class="window-title-actions">
49
+ <slot name="actions"></slot>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Content -->
54
+ <div class="window-content">
55
+ <slot></slot>
56
+ </div>
57
+
58
+ <!-- Resize Handles -->
59
+ <template v-if="resizable">
60
+ <div
61
+ class="window-resize-handle window-resize-handle--n"
62
+ @mousedown="(e) => handleResizeStart(e, 'n')"
63
+ ></div>
64
+ <div
65
+ class="window-resize-handle window-resize-handle--s"
66
+ @mousedown="(e) => handleResizeStart(e, 's')"
67
+ ></div>
68
+ <div
69
+ class="window-resize-handle window-resize-handle--e"
70
+ @mousedown="(e) => handleResizeStart(e, 'e')"
71
+ ></div>
72
+ <div
73
+ class="window-resize-handle window-resize-handle--w"
74
+ @mousedown="(e) => handleResizeStart(e, 'w')"
75
+ ></div>
76
+ <div
77
+ class="window-resize-handle window-resize-handle--ne"
78
+ @mousedown="(e) => handleResizeStart(e, 'ne')"
79
+ ></div>
80
+ <div
81
+ class="window-resize-handle window-resize-handle--nw"
82
+ @mousedown="(e) => handleResizeStart(e, 'nw')"
83
+ ></div>
84
+ <div
85
+ class="window-resize-handle window-resize-handle--se"
86
+ @mousedown="(e) => handleResizeStart(e, 'se')"
87
+ ></div>
88
+ <div
89
+ class="window-resize-handle window-resize-handle--sw"
90
+ @mousedown="(e) => handleResizeStart(e, 'sw')"
91
+ ></div>
92
+ </template>
93
+ </div>
94
+ </div>
95
+ </Teleport>
96
+ </template>
97
+
98
+ <script setup lang="ts">
99
+ import { computed, ref, onMounted, onUnmounted } from 'vue';
100
+ import { X, Minus, Maximize2 } from 'lucide-vue-next';
101
+ import { useWindowDrag } from '../composables/useWindowDrag';
102
+ import { useWindowResize } from '../composables/useWindowResize';
103
+
104
+ interface Props {
105
+ title?: string;
106
+ showTitleBar?: boolean;
107
+ showMinimize?: boolean;
108
+ showMaximize?: boolean;
109
+ draggable?: boolean;
110
+ resizable?: boolean;
111
+ /** 窗口宽度,'auto' 表示自适应内容,'fit-content' 也表示自适应 */
112
+ width?: string | number;
113
+ /** 窗口高度,'auto' 表示自适应内容,'fit-content' 也表示自适应 */
114
+ height?: string | number;
115
+ minWidth?: string | number;
116
+ minHeight?: string | number;
117
+ maxWidth?: string | number;
118
+ maxHeight?: string | number;
119
+ closeOnBackdrop?: boolean;
120
+ /** 是否自适应内容大小(不使用固定初始尺寸) */
121
+ fitContent?: boolean;
122
+ }
123
+
124
+ const props = withDefaults(defineProps<Props>(), {
125
+ showTitleBar: true,
126
+ showMinimize: false,
127
+ showMaximize: false,
128
+ draggable: true,
129
+ resizable: true,
130
+ closeOnBackdrop: true,
131
+ width: 'auto',
132
+ height: 'auto',
133
+ minWidth: 300,
134
+ minHeight: 200,
135
+ maxWidth: '80vw',
136
+ maxHeight: '80vh',
137
+ fitContent: false
138
+ });
139
+
140
+ const emit = defineEmits<{
141
+ close: [];
142
+ minimize: [];
143
+ maximize: [];
144
+ }>();
145
+
146
+ const windowContainerRef = ref<HTMLElement | null>(null);
147
+ const windowDrag = props.draggable ? useWindowDrag() : null;
148
+
149
+ /**
150
+ * 是否使用自适应内容模式
151
+ */
152
+ const isAutoSize = computed(() => {
153
+ return props.fitContent ||
154
+ props.width === 'auto' ||
155
+ props.width === 'fit-content' ||
156
+ props.height === 'auto' ||
157
+ props.height === 'fit-content';
158
+ });
159
+
160
+ /**
161
+ * 计算初始尺寸
162
+ */
163
+ const getInitialSize = () => {
164
+ // 自适应模式不设置固定初始尺寸
165
+ if (isAutoSize.value) {
166
+ return { initialWidth: 0, initialHeight: 0 };
167
+ }
168
+
169
+ const defaultWidth = 600;
170
+ const defaultHeight = 500;
171
+
172
+ let initialWidth = defaultWidth;
173
+ let initialHeight = defaultHeight;
174
+
175
+ if (props.width !== 'auto' && props.width !== 'fit-content') {
176
+ initialWidth = typeof props.width === 'number' ? props.width : parseInt(props.width);
177
+ }
178
+ if (props.height !== 'auto' && props.height !== 'fit-content') {
179
+ initialHeight = typeof props.height === 'number' ? props.height : parseInt(props.height);
180
+ }
181
+
182
+ return { initialWidth, initialHeight };
183
+ };
184
+
185
+ /**
186
+ * 解析尺寸限制
187
+ */
188
+ const parseSize = (size: string | number, defaultPx: number): number => {
189
+ if (typeof size === 'number') return size;
190
+ if (size.endsWith('px')) return parseInt(size);
191
+ if (size.endsWith('vw')) return (parseInt(size) / 100) * window.innerWidth;
192
+ if (size.endsWith('vh')) return (parseInt(size) / 100) * window.innerHeight;
193
+ return defaultPx;
194
+ };
195
+
196
+ const { initialWidth, initialHeight } = getInitialSize();
197
+ const minW = parseSize(props.minWidth, 500);
198
+ const minH = parseSize(props.minHeight, 350);
199
+ const maxW = parseSize(props.maxWidth, window.innerWidth * 0.8);
200
+ const maxH = parseSize(props.maxHeight, window.innerHeight * 0.8);
201
+
202
+ const windowResize = props.resizable
203
+ ? useWindowResize(initialWidth, initialHeight, minW, minH, maxW, maxH)
204
+ : null;
205
+
206
+ const windowStyle = computed(() => {
207
+ const baseStyle: Record<string, string> = {
208
+ left: '50%',
209
+ top: '50%',
210
+ };
211
+
212
+ // 计算位置和缩放(组合 transform)
213
+ let translateX = '-50%';
214
+ let translateY = '-50%';
215
+
216
+ if (props.draggable && windowDrag) {
217
+ translateX = `calc(-50% + ${windowDrag.position.value.x}px)`;
218
+ translateY = `calc(-50% + ${windowDrag.position.value.y}px)`;
219
+ }
220
+
221
+ baseStyle.transform = `translate(${translateX}, ${translateY})`;
222
+ baseStyle.transformOrigin = 'center center';
223
+
224
+ // 自适应模式:不设置固定宽高,让内容决定
225
+ if (isAutoSize.value) {
226
+ // 只有在用户手动调整过大小时才应用
227
+ if (props.resizable && windowResize && windowResize.width.value > 0) {
228
+ baseStyle.width = `${windowResize.width.value}px`;
229
+ baseStyle.height = `${windowResize.height.value}px`;
230
+ }
231
+ // 否则不设置 width/height,让 CSS fit-content 生效
232
+ } else {
233
+ // 固定尺寸模式
234
+ if (props.resizable && windowResize) {
235
+ baseStyle.width = `${windowResize.width.value}px`;
236
+ baseStyle.height = `${windowResize.height.value}px`;
237
+ } else {
238
+ if (props.width !== 'auto' && props.width !== 'fit-content') {
239
+ baseStyle.width = typeof props.width === 'number' ? `${props.width}px` : props.width;
240
+ }
241
+ if (props.height !== 'auto' && props.height !== 'fit-content') {
242
+ baseStyle.height = typeof props.height === 'number' ? `${props.height}px` : props.height;
243
+ }
244
+ }
245
+ }
246
+
247
+ if (props.minWidth) {
248
+ baseStyle.minWidth = typeof props.minWidth === 'number' ? `${props.minWidth}px` : props.minWidth;
249
+ }
250
+ if (props.minHeight) {
251
+ baseStyle.minHeight = typeof props.minHeight === 'number' ? `${props.minHeight}px` : props.minHeight;
252
+ }
253
+ if (props.maxWidth) {
254
+ baseStyle.maxWidth = typeof props.maxWidth === 'number' ? `${props.maxWidth}px` : props.maxWidth;
255
+ }
256
+ if (props.maxHeight) {
257
+ baseStyle.maxHeight = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight;
258
+ }
259
+
260
+ return baseStyle;
261
+ });
262
+
263
+ const handleBackdropClick = (e: MouseEvent) => {
264
+ if (props.closeOnBackdrop && e.target === e.currentTarget) {
265
+ emit('close');
266
+ }
267
+ };
268
+
269
+ const handleDragStart = (e: MouseEvent) => {
270
+ if (props.draggable && windowDrag) {
271
+ windowDrag.startDrag(e);
272
+ }
273
+ };
274
+
275
+ const handleClose = () => {
276
+ emit('close');
277
+ };
278
+
279
+ const handleMinimize = () => {
280
+ emit('minimize');
281
+ };
282
+
283
+ const handleMaximize = () => {
284
+ emit('maximize');
285
+ };
286
+
287
+ const handleResizeStart = (e: MouseEvent, direction: 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw') => {
288
+ if (!props.resizable || !windowResize || !windowContainerRef.value) return;
289
+
290
+ const rect = windowContainerRef.value.getBoundingClientRect();
291
+ const currentWidth = rect.width;
292
+ const currentHeight = rect.height;
293
+
294
+ windowResize.startResize(e, direction, currentWidth, currentHeight);
295
+ };
296
+
297
+ /**
298
+ * ESC 键关闭
299
+ */
300
+ onMounted(() => {
301
+ const handleEsc = (e: KeyboardEvent) => {
302
+ if (e.key === 'Escape') {
303
+ e.preventDefault();
304
+ emit('close');
305
+ }
306
+ };
307
+ window.addEventListener('keydown', handleEsc);
308
+
309
+ onUnmounted(() => {
310
+ window.removeEventListener('keydown', handleEsc);
311
+ });
312
+ });
313
+ </script>
314
+
315
+ <style scoped>
316
+ .window-overlay {
317
+ position: fixed;
318
+ inset: 0;
319
+ z-index: 100;
320
+ background: rgba(0, 0, 0, 0.3);
321
+ animation: fade-in 200ms ease-out;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ }
326
+
327
+ .window-container {
328
+ position: absolute;
329
+ display: flex;
330
+ flex-direction: column;
331
+ background: rgba(255, 255, 255, 0.95);
332
+ backdrop-filter: blur(24px);
333
+ border: 1px solid rgb(229, 231, 233);
334
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
335
+ border-radius: 0.5rem;
336
+ overflow: hidden;
337
+ animation: window-fade-in 200ms ease-out;
338
+ }
339
+
340
+ .window-title-bar {
341
+ height: 2.5rem;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: space-between;
345
+ padding: 0 0.75rem;
346
+ border-bottom: 1px solid rgb(229, 231, 233);
347
+ flex-shrink: 0;
348
+ user-select: none;
349
+ background: rgba(249, 250, 251, 0.8);
350
+ z-index: 20;
351
+ cursor: grab;
352
+ }
353
+
354
+ .window-title-bar:active {
355
+ cursor: grabbing;
356
+ }
357
+
358
+ .window-controls {
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 0.5rem;
362
+ }
363
+
364
+ .window-control-button {
365
+ width: 0.75rem;
366
+ height: 0.75rem;
367
+ border-radius: 50%;
368
+ display: flex;
369
+ align-items: center;
370
+ justify-content: center;
371
+ border: 1px solid;
372
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
373
+ outline: none;
374
+ cursor: pointer;
375
+ padding: 0;
376
+ background: transparent;
377
+ flex-shrink: 0;
378
+ position: relative;
379
+ }
380
+
381
+ .window-control-button svg {
382
+ width: 7px !important;
383
+ height: 7px !important;
384
+ display: block;
385
+ flex-shrink: 0;
386
+ }
387
+
388
+ .window-control-button .window-control-icon {
389
+ width: 7px !important;
390
+ height: 7px !important;
391
+ min-width: 7px;
392
+ min-height: 7px;
393
+ }
394
+
395
+ .window-control-button--close {
396
+ background-color: rgb(255, 95, 87);
397
+ border-color: rgb(224, 68, 62);
398
+ }
399
+
400
+ .window-control-button--minimize {
401
+ background-color: rgb(254, 188, 46);
402
+ border-color: rgb(216, 158, 36);
403
+ }
404
+
405
+ .window-control-button--maximize {
406
+ background-color: rgb(40, 200, 64);
407
+ border-color: rgb(26, 171, 41);
408
+ }
409
+
410
+ .window-control-icon {
411
+ color: rgba(0, 0, 0, 0.5);
412
+ opacity: 0;
413
+ transition: opacity 200ms;
414
+ width: 7px;
415
+ height: 7px;
416
+ flex-shrink: 0;
417
+ }
418
+
419
+ .window-controls:hover .window-control-icon {
420
+ opacity: 1;
421
+ }
422
+
423
+ .window-control-button--close:hover {
424
+ background-color: rgba(255, 95, 87, 0.8);
425
+ }
426
+
427
+ .window-control-button--minimize:hover {
428
+ background-color: rgba(254, 188, 46, 0.8);
429
+ }
430
+
431
+ .window-control-button--maximize:hover {
432
+ background-color: rgba(40, 200, 64, 0.8);
433
+ }
434
+
435
+ .window-title-info {
436
+ display: flex;
437
+ flex-direction: column;
438
+ align-items: center;
439
+ justify-content: center;
440
+ flex: 1;
441
+ margin: 0 1rem;
442
+ overflow: hidden;
443
+ pointer-events: none;
444
+ }
445
+
446
+ .window-title-text {
447
+ font-weight: 500;
448
+ font-size: 0.75rem;
449
+ color: rgb(75, 85, 99);
450
+ overflow: hidden;
451
+ text-overflow: ellipsis;
452
+ white-space: nowrap;
453
+ width: 100%;
454
+ text-align: center;
455
+ }
456
+
457
+ .window-title-actions {
458
+ display: flex;
459
+ align-items: center;
460
+ justify-content: flex-end;
461
+ min-width: 4rem;
462
+ flex-shrink: 0;
463
+ }
464
+
465
+ .window-content {
466
+ flex: 1;
467
+ display: flex;
468
+ flex-direction: column;
469
+ overflow: hidden;
470
+ position: relative;
471
+ width: 100%;
472
+ height: 100%;
473
+ }
474
+
475
+ /* Resize Handles */
476
+ .window-resize-handle {
477
+ position: absolute;
478
+ background: transparent;
479
+ z-index: 30;
480
+ }
481
+
482
+ .window-resize-handle--n {
483
+ top: 0;
484
+ left: 0;
485
+ right: 0;
486
+ height: 4px;
487
+ cursor: n-resize;
488
+ }
489
+
490
+ .window-resize-handle--s {
491
+ bottom: 0;
492
+ left: 0;
493
+ right: 0;
494
+ height: 4px;
495
+ cursor: s-resize;
496
+ }
497
+
498
+ .window-resize-handle--e {
499
+ top: 0;
500
+ right: 0;
501
+ bottom: 0;
502
+ width: 4px;
503
+ cursor: e-resize;
504
+ }
505
+
506
+ .window-resize-handle--w {
507
+ top: 0;
508
+ left: 0;
509
+ bottom: 0;
510
+ width: 4px;
511
+ cursor: w-resize;
512
+ }
513
+
514
+ .window-resize-handle--ne {
515
+ top: 0;
516
+ right: 0;
517
+ width: 8px;
518
+ height: 8px;
519
+ cursor: ne-resize;
520
+ }
521
+
522
+ .window-resize-handle--nw {
523
+ top: 0;
524
+ left: 0;
525
+ width: 8px;
526
+ height: 8px;
527
+ cursor: nw-resize;
528
+ }
529
+
530
+ .window-resize-handle--se {
531
+ bottom: 0;
532
+ right: 0;
533
+ width: 8px;
534
+ height: 8px;
535
+ cursor: se-resize;
536
+ }
537
+
538
+ .window-resize-handle--sw {
539
+ bottom: 0;
540
+ left: 0;
541
+ width: 8px;
542
+ height: 8px;
543
+ cursor: sw-resize;
544
+ }
545
+
546
+ @keyframes fade-in {
547
+ from { opacity: 0; }
548
+ to { opacity: 1; }
549
+ }
550
+
551
+ @keyframes window-fade-in {
552
+ from {
553
+ opacity: 0;
554
+ transform: translate(-50%, -50%) scale(0.95);
555
+ }
556
+ to {
557
+ opacity: 1;
558
+ transform: translate(-50%, -50%) scale(1);
559
+ }
560
+ }
561
+ </style>
@@ -0,0 +1,79 @@
1
+ import { ref, watch } from 'vue';
2
+ import type { FileItem } from '../types';
3
+ import { FileType } from '../types';
4
+
5
+ // 扩展 Window 类型
6
+ declare global {
7
+ interface Window {
8
+ fileExplorerAPI?: {
9
+ getApplicationIcon?: (appPath: string) => Promise<string | null>;
10
+ };
11
+ }
12
+ }
13
+
14
+ /**
15
+ * 应用程序图标管理 composable
16
+ */
17
+ export function useApplicationIcon(items: () => FileItem[]) {
18
+ // 应用程序图标缓存(避免重复请求)
19
+ const appIconCache = new Map<string, string>();
20
+
21
+ // 使用响应式的图标 URL 映射
22
+ const appIconUrls = ref<Map<string, string>>(new Map());
23
+
24
+ /**
25
+ * 为应用程序获取图标
26
+ */
27
+ const loadApplicationIcon = async (item: FileItem) => {
28
+ if (item.type !== FileType.APPLICATION || !item.id) return;
29
+
30
+ // 检查缓存
31
+ if (appIconCache.has(item.id)) {
32
+ const cachedUrl = appIconCache.get(item.id);
33
+ if (cachedUrl) {
34
+ appIconUrls.value.set(item.id, cachedUrl);
35
+ }
36
+ return;
37
+ }
38
+
39
+ // 检查响应式映射
40
+ if (appIconUrls.value.has(item.id)) {
41
+ return;
42
+ }
43
+
44
+ if (typeof window.fileExplorerAPI !== 'undefined' && window.fileExplorerAPI.getApplicationIcon) {
45
+ try {
46
+ const iconUrl = await window.fileExplorerAPI.getApplicationIcon(item.id);
47
+ if (iconUrl) {
48
+ appIconCache.set(item.id, iconUrl);
49
+ appIconUrls.value.set(item.id, iconUrl);
50
+ }
51
+ } catch (error) {
52
+ console.error(`Failed to load application icon for ${item.name}:`, error);
53
+ }
54
+ }
55
+ };
56
+
57
+ /**
58
+ * 监听 items 变化,为应用程序加载图标
59
+ */
60
+ watch(items, (newItems) => {
61
+ newItems.forEach(item => {
62
+ if (item.type === FileType.APPLICATION && !appIconUrls.value.has(item.id)) {
63
+ loadApplicationIcon(item);
64
+ }
65
+ });
66
+ }, { immediate: true, deep: true });
67
+
68
+ /**
69
+ * 获取应用程序图标的响应式 URL
70
+ */
71
+ const getAppIconUrl = (item: FileItem): string | undefined => {
72
+ return appIconUrls.value.get(item.id);
73
+ };
74
+
75
+ return {
76
+ getAppIconUrl,
77
+ loadApplicationIcon
78
+ };
79
+ }