@huyooo/file-explorer-frontend-react 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 (46) hide show
  1. package/dist/index.css +1740 -0
  2. package/dist/index.css.map +1 -0
  3. package/dist/index.d.ts +562 -0
  4. package/dist/index.js +3453 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/style.css +3 -0
  7. package/package.json +58 -0
  8. package/src/components/Breadcrumb.css +61 -0
  9. package/src/components/Breadcrumb.tsx +38 -0
  10. package/src/components/CompressDialog.css +264 -0
  11. package/src/components/CompressDialog.tsx +222 -0
  12. package/src/components/ContextMenu.css +155 -0
  13. package/src/components/ContextMenu.tsx +375 -0
  14. package/src/components/FileGrid.css +267 -0
  15. package/src/components/FileGrid.tsx +277 -0
  16. package/src/components/FileIcon.css +41 -0
  17. package/src/components/FileIcon.tsx +86 -0
  18. package/src/components/FileInfoDialog.css +252 -0
  19. package/src/components/FileInfoDialog.tsx +202 -0
  20. package/src/components/FileList.css +226 -0
  21. package/src/components/FileList.tsx +228 -0
  22. package/src/components/FileListView.css +36 -0
  23. package/src/components/FileListView.tsx +355 -0
  24. package/src/components/FileSidebar.css +94 -0
  25. package/src/components/FileSidebar.tsx +66 -0
  26. package/src/components/ProgressDialog.css +211 -0
  27. package/src/components/ProgressDialog.tsx +183 -0
  28. package/src/components/SortIndicator.css +7 -0
  29. package/src/components/SortIndicator.tsx +19 -0
  30. package/src/components/StatusBar.css +20 -0
  31. package/src/components/StatusBar.tsx +21 -0
  32. package/src/components/Toolbar.css +150 -0
  33. package/src/components/Toolbar.tsx +127 -0
  34. package/src/components/Window.css +246 -0
  35. package/src/components/Window.tsx +335 -0
  36. package/src/hooks/useApplicationIcon.ts +80 -0
  37. package/src/hooks/useDragAndDrop.ts +104 -0
  38. package/src/hooks/useMediaPlayer.ts +164 -0
  39. package/src/hooks/useSelection.ts +112 -0
  40. package/src/hooks/useWindowDrag.ts +60 -0
  41. package/src/hooks/useWindowResize.ts +126 -0
  42. package/src/index.css +3 -0
  43. package/src/index.ts +34 -0
  44. package/src/types/index.ts +274 -0
  45. package/src/utils/fileTypeIcon.ts +309 -0
  46. package/src/utils/folderTypeIcon.ts +132 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * FileGrid - 网格视图样式
3
+ * 参考 macOS Finder 和 Windows Explorer 的设计
4
+ */
5
+
6
+ .file-grid {
7
+ display: grid;
8
+ /* macOS Finder 默认约 90-100px */
9
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
10
+ gap: 6px;
11
+ padding: 12px;
12
+ align-content: start;
13
+ grid-auto-rows: min-content;
14
+ overflow-y: auto;
15
+ overflow-x: hidden;
16
+ }
17
+
18
+ /* 网格项 - 更紧凑的布局 */
19
+ .file-grid-item {
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: center;
23
+ padding: 4px;
24
+ border-radius: 6px;
25
+ cursor: pointer;
26
+ border: 1px solid transparent;
27
+ transition: background-color 120ms ease, border-color 120ms ease;
28
+ position: relative;
29
+ overflow: hidden;
30
+ min-width: 0;
31
+ width: 100%;
32
+ }
33
+
34
+ /* 拖拽时的鼠标样式 */
35
+ .file-grid-item[draggable="true"] {
36
+ cursor: grab;
37
+ }
38
+
39
+ .file-grid-item:active {
40
+ cursor: grabbing;
41
+ }
42
+
43
+ /* 悬停效果 - 类似 macOS */
44
+ .file-grid-item--normal:hover {
45
+ background: rgba(0, 0, 0, 0.04);
46
+ }
47
+
48
+ /* 选中状态 - macOS 风格的蓝色高亮 */
49
+ .file-grid-item--selected {
50
+ background: rgba(0, 122, 255, 0.12);
51
+ border-color: rgba(0, 122, 255, 0.2);
52
+ }
53
+
54
+ .file-grid-item--selected:hover {
55
+ background: rgba(0, 122, 255, 0.16);
56
+ }
57
+
58
+ /* 拖拽悬停 */
59
+ .file-grid-item--drag-over {
60
+ background: rgba(0, 122, 255, 0.2);
61
+ border-color: rgba(0, 122, 255, 0.4);
62
+ }
63
+
64
+ /* 图标/缩略图容器 - 统一尺寸 */
65
+ .file-grid-item-icon {
66
+ position: relative;
67
+ pointer-events: none;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ /* 统一 48x48 尺寸,更紧凑 */
72
+ width: 48px;
73
+ height: 48px;
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ /* 缩略图样式 - 统一尺寸和圆角 */
78
+ /* 缩略图保持原图比例,使用 object-fit: cover 填充固定尺寸容器 */
79
+ .file-grid-item-thumbnail {
80
+ width: 48px;
81
+ height: 48px;
82
+ object-fit: cover;
83
+ object-position: center;
84
+ border-radius: 4px;
85
+ /* 轻微阴影增加层次感 */
86
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
87
+ background: #f5f5f5;
88
+ /* 确保图片能作为加载占位符,平滑过渡到原图 */
89
+ display: block;
90
+ }
91
+
92
+ /* 应用程序图标 - 无背景 */
93
+ .file-grid-item-thumbnail--application {
94
+ object-fit: contain;
95
+ background: transparent;
96
+ box-shadow: none;
97
+ border-radius: 10px;
98
+ }
99
+
100
+ .file-grid-item-thumbnail--selected {
101
+ box-shadow: 0 2px 6px rgba(0, 122, 255, 0.2), 0 0 0 2px rgba(0, 122, 255, 0.3);
102
+ }
103
+
104
+ /* 视频缩略图容器 */
105
+ .file-grid-item-thumbnail--video {
106
+ position: relative;
107
+ border-radius: 4px;
108
+ overflow: hidden;
109
+ background: #000;
110
+ width: 48px;
111
+ height: 48px;
112
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ }
117
+
118
+ /* 视频缩略图内的图片 - 保持比例,填充容器 */
119
+ .file-grid-item-thumbnail--video > img {
120
+ width: 100%;
121
+ height: 100%;
122
+ object-fit: cover;
123
+ object-position: center;
124
+ display: block;
125
+ }
126
+
127
+ /* 视频播放图标 */
128
+ .file-grid-item-video-play {
129
+ position: absolute;
130
+ inset: 0;
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ pointer-events: none;
135
+ opacity: 0.9;
136
+ transition: opacity 150ms;
137
+ }
138
+
139
+ .file-grid-item-thumbnail--video:hover .file-grid-item-video-play {
140
+ opacity: 0;
141
+ }
142
+
143
+ .file-grid-item-video-play-icon {
144
+ width: 20px;
145
+ height: 20px;
146
+ border-radius: 50%;
147
+ background: rgba(0, 0, 0, 0.5);
148
+ backdrop-filter: blur(4px);
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ }
153
+
154
+ .file-grid-item-video-play-icon::before {
155
+ content: "";
156
+ width: 0;
157
+ height: 0;
158
+ border-top: 4px solid transparent;
159
+ border-left: 7px solid white;
160
+ border-bottom: 4px solid transparent;
161
+ margin-left: 2px;
162
+ }
163
+
164
+ /* 文件名容器 */
165
+ .file-grid-item-name-wrapper {
166
+ display: flex;
167
+ align-items: flex-start;
168
+ justify-content: center;
169
+ width: 100%;
170
+ min-height: 0;
171
+ }
172
+
173
+ /* 文件名 - macOS 风格,最多2行,保留扩展名 */
174
+ .file-grid-item-name {
175
+ font-size: 12px;
176
+ line-height: 1.3;
177
+ text-align: center;
178
+ padding: 2px;
179
+ border-radius: 3px;
180
+ user-select: none;
181
+ cursor: default;
182
+ transition: color 100ms;
183
+ color: #1d1d1f;
184
+ width: 100%;
185
+ /* 使用 flexbox 布局,让扩展名始终显示 */
186
+ display: flex;
187
+ flex-direction: column;
188
+ align-items: center;
189
+ justify-content: flex-start;
190
+ min-height: 0;
191
+ }
192
+
193
+ /* 文件名主体部分 - 可换行,最多1行 */
194
+ .file-grid-item-name-base {
195
+ display: -webkit-box;
196
+ -webkit-line-clamp: 1;
197
+ -webkit-box-orient: vertical;
198
+ overflow: hidden;
199
+ word-break: break-word;
200
+ text-overflow: ellipsis;
201
+ width: 100%;
202
+ text-align: center;
203
+ }
204
+
205
+ /* 扩展名部分 - 始终显示,不换行 */
206
+ .file-grid-item-name-ext {
207
+ display: block;
208
+ white-space: nowrap;
209
+ flex-shrink: 0;
210
+ text-align: center;
211
+ }
212
+
213
+ /* 选中后点击文件名可编辑 */
214
+ .file-grid-item-name--selected {
215
+ cursor: text;
216
+ }
217
+
218
+ /* 重命名输入框 */
219
+ .file-grid-item-rename-input {
220
+ width: 100%;
221
+ font-size: 11px;
222
+ line-height: 1.3;
223
+ text-align: center;
224
+ padding: 2px 4px;
225
+ border: 1px solid #007aff;
226
+ border-radius: 3px;
227
+ outline: none;
228
+ box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.2);
229
+ background: white;
230
+ cursor: text;
231
+ }
232
+
233
+ /* 深色模式支持 */
234
+ @media (prefers-color-scheme: dark) {
235
+ .file-grid-item--normal:hover {
236
+ background: rgba(255, 255, 255, 0.06);
237
+ }
238
+
239
+ .file-grid-item--selected {
240
+ background: rgba(10, 132, 255, 0.25);
241
+ border-color: rgba(10, 132, 255, 0.3);
242
+ }
243
+
244
+ .file-grid-item--selected:hover {
245
+ background: rgba(10, 132, 255, 0.3);
246
+ }
247
+
248
+ .file-grid-item--drag-over {
249
+ background: rgba(10, 132, 255, 0.35);
250
+ border-color: rgba(10, 132, 255, 0.5);
251
+ }
252
+
253
+ .file-grid-item-thumbnail {
254
+ background: #2c2c2e;
255
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.05);
256
+ }
257
+
258
+ .file-grid-item-name {
259
+ color: #f5f5f7;
260
+ }
261
+
262
+ .file-grid-item-rename-input {
263
+ background: #1c1c1e;
264
+ border-color: #0a84ff;
265
+ color: #f5f5f7;
266
+ }
267
+ }
@@ -0,0 +1,277 @@
1
+ import { useRef, useMemo } from 'react';
2
+ import type { FileItem } from '../types';
3
+ import { FileType } from '../types';
4
+ import { FileIcon } from './FileIcon';
5
+ import './FileGrid.css';
6
+
7
+ /**
8
+ * 分离文件名和扩展名
9
+ * 文件夹或无扩展名文件返回 { baseName: name, ext: '' }
10
+ */
11
+ function splitFileName(name: string, isFolder: boolean): { baseName: string; ext: string } {
12
+ if (isFolder) {
13
+ return { baseName: name, ext: '' };
14
+ }
15
+ const lastDot = name.lastIndexOf('.');
16
+ // 没有扩展名,或者是隐藏文件(如 .gitignore)
17
+ if (lastDot <= 0) {
18
+ return { baseName: name, ext: '' };
19
+ }
20
+ return {
21
+ baseName: name.substring(0, lastDot),
22
+ ext: name.substring(lastDot), // 包含点号
23
+ };
24
+ }
25
+
26
+ /**
27
+ * 文件名组件 - 保留扩展名显示,只截断文件名部分
28
+ */
29
+ function FileName({
30
+ name,
31
+ isFolder,
32
+ isSelected,
33
+ onClick
34
+ }: {
35
+ name: string;
36
+ isFolder: boolean;
37
+ isSelected: boolean;
38
+ onClick: (e: React.MouseEvent) => void;
39
+ }) {
40
+ const { baseName, ext } = useMemo(() => splitFileName(name, isFolder), [name, isFolder]);
41
+
42
+ return (
43
+ <span
44
+ onClick={onClick}
45
+ className={`file-grid-item-name ${isSelected ? 'file-grid-item-name--selected' : ''}`}
46
+ title={name}
47
+ >
48
+ {ext ? (
49
+ <>
50
+ <span className="file-grid-item-name-base">{baseName}</span>
51
+ <span className="file-grid-item-name-ext">{ext}</span>
52
+ </>
53
+ ) : (
54
+ name
55
+ )}
56
+ </span>
57
+ );
58
+ }
59
+
60
+ interface FileGridProps {
61
+ items: FileItem[];
62
+ selectedIds: Set<string>;
63
+ editingId?: string | null;
64
+ dragOverId?: string | null;
65
+ getAppIconUrl?: (item: FileItem) => string | undefined;
66
+ onSelect?: (item: FileItem, e: React.MouseEvent) => void;
67
+ onOpen?: (item: FileItem) => void;
68
+ onContextMenu?: (item: FileItem, e: React.MouseEvent) => void;
69
+ onContextMenuEmpty?: (e: React.MouseEvent) => void;
70
+ onNameClick?: (item: FileItem, e: React.MouseEvent) => void;
71
+ onRename?: (item: FileItem, newName: string) => void;
72
+ onRenameCancel?: (item: FileItem) => void;
73
+ onDragStart?: (e: React.DragEvent, item: FileItem) => void;
74
+ onDragOver?: (e: React.DragEvent, item: FileItem) => void;
75
+ onDragLeave?: (e: React.DragEvent) => void;
76
+ onDrop?: (e: React.DragEvent, item: FileItem) => void;
77
+ onThumbnailError?: (item: FileItem, e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
78
+ }
79
+
80
+ export function FileGrid({
81
+ items,
82
+ selectedIds,
83
+ editingId,
84
+ dragOverId,
85
+ getAppIconUrl,
86
+ onSelect,
87
+ onOpen,
88
+ onContextMenu,
89
+ onContextMenuEmpty,
90
+ onNameClick,
91
+ onRename,
92
+ onRenameCancel,
93
+ onDragStart,
94
+ onDragOver,
95
+ onDragLeave,
96
+ onDrop,
97
+ onThumbnailError,
98
+ }: FileGridProps) {
99
+ const renameInputRef = useRef<HTMLInputElement>(null);
100
+
101
+ /**
102
+ * 空白处右键菜单
103
+ */
104
+ const handleEmptyContextMenu = (e: React.MouseEvent) => {
105
+ const target = e.target as HTMLElement;
106
+ if (!target.closest('.file-grid-item')) {
107
+ onContextMenuEmpty?.(e);
108
+ }
109
+ };
110
+
111
+ /**
112
+ * 判断是否有缩略图或应用程序图标
113
+ */
114
+ const hasThumbnail = (item: FileItem): boolean => {
115
+ // 应用程序图标
116
+ if (item.type === FileType.APPLICATION && getAppIconUrl?.(item)) {
117
+ return true;
118
+ }
119
+ // 图片:必须有缩略图才显示,永远不显示原始 URL
120
+ if (item.type === FileType.IMAGE) {
121
+ return !!item.thumbnailUrl;
122
+ }
123
+ // 视频:必须有缩略图才显示,如果没有缩略图说明不应该被识别为视频
124
+ if (item.type === FileType.VIDEO) {
125
+ return !!item.thumbnailUrl;
126
+ }
127
+ return false;
128
+ };
129
+
130
+ /**
131
+ * 视频悬停播放
132
+ */
133
+ const handleVideoHover = (e: React.SyntheticEvent<HTMLVideoElement>, isHover: boolean) => {
134
+ const video = e.currentTarget;
135
+ if (isHover) {
136
+ video.play().catch(() => {});
137
+ } else {
138
+ video.pause();
139
+ video.currentTime = 0;
140
+ }
141
+ };
142
+
143
+ /**
144
+ * 处理重命名(blur 事件)
145
+ */
146
+ const handleRename = (item: FileItem, e: React.FocusEvent<HTMLInputElement>) => {
147
+ const input = e.target;
148
+ const newName = input.value.trim();
149
+ if (newName && newName !== item.name) {
150
+ onRename?.(item, newName);
151
+ } else {
152
+ onRenameCancel?.(item);
153
+ }
154
+ };
155
+
156
+ /**
157
+ * Enter 键保存
158
+ */
159
+ const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
160
+ e.currentTarget.blur();
161
+ };
162
+
163
+ /**
164
+ * Escape 键取消
165
+ */
166
+ const handleEscapeKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
167
+ const input = e.currentTarget;
168
+ const item = items.find((i) => i.id === editingId);
169
+ if (item) {
170
+ input.value = item.name;
171
+ }
172
+ input.blur();
173
+ };
174
+
175
+ return (
176
+ <div className="file-grid" onContextMenu={(e) => { e.preventDefault(); handleEmptyContextMenu(e); }}>
177
+ {items.map((item) => {
178
+ const isSelected = selectedIds.has(item.id);
179
+ const isEditing = editingId === item.id;
180
+ const isDragOver = dragOverId === item.id;
181
+ const hasThumb = hasThumbnail(item);
182
+ const appIconUrl = item.type === FileType.APPLICATION ? getAppIconUrl?.(item) : undefined;
183
+
184
+ let itemClassName = 'file-grid-item';
185
+ if (isSelected && !isEditing) {
186
+ itemClassName += ' file-grid-item--selected';
187
+ } else if (isDragOver) {
188
+ itemClassName += ' file-grid-item--drag-over';
189
+ } else {
190
+ itemClassName += ' file-grid-item--normal';
191
+ }
192
+
193
+ return (
194
+ <div
195
+ key={item.id}
196
+ draggable={!isEditing}
197
+ onDragStart={(e) => onDragStart?.(e, item)}
198
+ onDragOver={(e) => { e.preventDefault(); onDragOver?.(e, item); }}
199
+ onDragLeave={(e) => onDragLeave?.(e)}
200
+ onDrop={(e) => { e.preventDefault(); onDrop?.(e, item); }}
201
+ onClick={(e) => { e.stopPropagation(); onSelect?.(item, e); }}
202
+ onDoubleClick={(e) => { e.stopPropagation(); onOpen?.(item); }}
203
+ onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(item, e); }}
204
+ className={itemClassName}
205
+ >
206
+ <div className="file-grid-item-icon">
207
+ {/* 应用程序图标 */}
208
+ {appIconUrl && (
209
+ <img
210
+ src={appIconUrl}
211
+ alt={item.name}
212
+ className={`file-grid-item-thumbnail file-grid-item-thumbnail--application ${
213
+ isSelected && !isEditing ? 'file-grid-item-thumbnail--selected' : ''
214
+ }`}
215
+ />
216
+ )}
217
+ {/* 视频缩略图 - 视频类型必须有缩略图,如果没有缩略图说明不应该被识别为视频,按普通文件处理 */}
218
+ {!appIconUrl && item.type === FileType.VIDEO && item.thumbnailUrl && hasThumb && (
219
+ <div className="file-grid-item-thumbnail file-grid-item-thumbnail--video">
220
+ <img
221
+ src={item.thumbnailUrl}
222
+ alt={item.name}
223
+ className="file-grid-item-thumbnail"
224
+ onError={(e) => onThumbnailError?.(item, e)}
225
+ />
226
+ <div className="file-grid-item-video-play">
227
+ <div className="file-grid-item-video-play-icon" />
228
+ </div>
229
+ </div>
230
+ )}
231
+ {/* 图片缩略图 - 永远只显示缩略图,不显示原始 URL */}
232
+ {!appIconUrl && item.type === FileType.IMAGE && item.thumbnailUrl && hasThumb && (
233
+ <img
234
+ src={item.thumbnailUrl}
235
+ alt={item.name}
236
+ className="file-grid-item-thumbnail"
237
+ onError={(e) => onThumbnailError?.(item, e)}
238
+ />
239
+ )}
240
+ {/* 默认图标 */}
241
+ {!appIconUrl && !hasThumb && (
242
+ <FileIcon type={item.type} name={item.name} size={48} />
243
+ )}
244
+ </div>
245
+
246
+ <div className="file-grid-item-name-wrapper">
247
+ {isEditing ? (
248
+ <input
249
+ ref={renameInputRef}
250
+ type="text"
251
+ className="file-grid-item-rename-input"
252
+ defaultValue={item.name}
253
+ onBlur={(e) => handleRename(item, e)}
254
+ onKeyDown={(e) => {
255
+ if (e.key === 'Enter') {
256
+ handleEnterKey(e);
257
+ } else if (e.key === 'Escape') {
258
+ handleEscapeKey(e);
259
+ }
260
+ }}
261
+ autoFocus
262
+ />
263
+ ) : (
264
+ <FileName
265
+ name={item.name}
266
+ isFolder={item.type === FileType.FOLDER}
267
+ isSelected={isSelected}
268
+ onClick={(e) => { e.stopPropagation(); onNameClick?.(item, e); }}
269
+ />
270
+ )}
271
+ </div>
272
+ </div>
273
+ );
274
+ })}
275
+ </div>
276
+ );
277
+ }
@@ -0,0 +1,41 @@
1
+ .file-icon--folder {
2
+ color: rgb(96, 165, 250);
3
+ fill: rgb(96, 165, 250);
4
+ }
5
+
6
+ .file-icon--image {
7
+ color: rgb(168, 85, 247);
8
+ }
9
+
10
+ .file-icon--text {
11
+ color: rgb(107, 114, 128);
12
+ }
13
+
14
+ .file-icon--code {
15
+ color: rgb(34, 197, 94);
16
+ }
17
+
18
+ .file-icon--music {
19
+ color: rgb(248, 113, 113);
20
+ }
21
+
22
+ .file-icon--video {
23
+ color: rgb(37, 99, 235);
24
+ }
25
+
26
+ .file-icon--pdf {
27
+ color: rgb(220, 38, 38);
28
+ }
29
+
30
+ .file-icon--application {
31
+ color: rgb(139, 92, 246);
32
+ }
33
+
34
+ .file-icon--archive {
35
+ color: rgb(234, 179, 8);
36
+ }
37
+
38
+ .file-icon--default {
39
+ color: rgb(156, 163, 175);
40
+ }
41
+
@@ -0,0 +1,86 @@
1
+ import { useMemo } from 'react';
2
+ import { Icon } from '@iconify/react';
3
+ import { FileType, type FileType as FileTypeType } from '../types';
4
+ import { getFileTypeIcon } from '../utils/fileTypeIcon';
5
+ import { getFolderTypeIcon } from '../utils/folderTypeIcon';
6
+ import './FileIcon.css';
7
+
8
+ interface FileIconProps {
9
+ type: FileTypeType;
10
+ name?: string;
11
+ className?: string;
12
+ size?: number;
13
+ }
14
+
15
+ export function FileIcon({ type, name, className = '', size = 24 }: FileIconProps) {
16
+ const iconName = useMemo(() => {
17
+ if (type === FileType.FOLDER) {
18
+ // 文件夹:先查特定类型图标,否则用默认 folder
19
+ const folderIcon = name ? getFolderTypeIcon(name) : undefined;
20
+ return folderIcon ?? 'flat-color-icons:folder';
21
+ }
22
+
23
+ // 如果有文件名,使用 material-icon-theme 图标映射(传递 type 作为兜底)
24
+ if (name) {
25
+ return getFileTypeIcon(name, type);
26
+ }
27
+
28
+ // 回退逻辑(没有文件名时)
29
+ switch (type) {
30
+ case FileType.IMAGE:
31
+ return 'material-icon-theme:image';
32
+ case FileType.TEXT:
33
+ return 'material-icon-theme:document';
34
+ case FileType.CODE:
35
+ return 'material-icon-theme:javascript';
36
+ case FileType.MUSIC:
37
+ return 'material-icon-theme:audio';
38
+ case FileType.VIDEO:
39
+ return 'material-icon-theme:video';
40
+ case FileType.PDF:
41
+ return 'material-icon-theme:pdf';
42
+ case FileType.DOCUMENT:
43
+ return 'material-icon-theme:word';
44
+ case FileType.APPLICATION:
45
+ return 'material-icon-theme:exe';
46
+ case FileType.ARCHIVE:
47
+ return 'material-icon-theme:zip';
48
+ default:
49
+ return 'material-icon-theme:document';
50
+ }
51
+ }, [type, name]);
52
+
53
+ const iconClass = useMemo(() => {
54
+ const base = 'file-icon';
55
+ switch (type) {
56
+ case FileType.FOLDER:
57
+ return `${base} file-icon--folder`;
58
+ case FileType.IMAGE:
59
+ return `${base} file-icon--image`;
60
+ case FileType.TEXT:
61
+ return `${base} file-icon--text`;
62
+ case FileType.CODE:
63
+ return `${base} file-icon--code`;
64
+ case FileType.MUSIC:
65
+ return `${base} file-icon--music`;
66
+ case FileType.VIDEO:
67
+ return `${base} file-icon--video`;
68
+ case FileType.PDF:
69
+ case FileType.DOCUMENT:
70
+ return `${base} file-icon--pdf`;
71
+ case FileType.APPLICATION:
72
+ return `${base} file-icon--application`;
73
+ case FileType.ARCHIVE:
74
+ return `${base} file-icon--archive`;
75
+ default:
76
+ return `${base} file-icon--default`;
77
+ }
78
+ }, [type]);
79
+
80
+ return (
81
+ <div className={className}>
82
+ <Icon icon={iconName} width={size} height={size} className={iconClass} />
83
+ </div>
84
+ );
85
+ }
86
+