@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,228 @@
1
+ import { useRef } from 'react';
2
+ import type { FileItem, SortConfig } from '../types';
3
+ import { FileType } from '../types';
4
+ import { FileIcon } from './FileIcon';
5
+ import { SortIndicator } from './SortIndicator';
6
+ import './FileList.css';
7
+
8
+ interface FileListProps {
9
+ items: FileItem[];
10
+ selectedIds: Set<string>;
11
+ sortConfig?: SortConfig;
12
+ editingId?: string | null;
13
+ dragOverId?: string | null;
14
+ onSelect?: (item: FileItem, e: React.MouseEvent) => void;
15
+ onOpen?: (item: FileItem) => void;
16
+ onContextMenu?: (item: FileItem, e: React.MouseEvent) => void;
17
+ onContextMenuEmpty?: (e: React.MouseEvent) => void;
18
+ onNameClick?: (item: FileItem, e: React.MouseEvent) => void;
19
+ onRename?: (item: FileItem, newName: string) => void;
20
+ onRenameCancel?: (item: FileItem) => void;
21
+ onSort?: (field: string) => void;
22
+ onDragStart?: (e: React.DragEvent, item: FileItem) => void;
23
+ onDragOver?: (e: React.DragEvent, item: FileItem) => void;
24
+ onDragLeave?: (e: React.DragEvent) => void;
25
+ onDrop?: (e: React.DragEvent, item: FileItem) => void;
26
+ }
27
+
28
+ /**
29
+ * 获取类型标签
30
+ */
31
+ function getTypeLabel(type: FileType): string {
32
+ const labels: Record<FileType, string> = {
33
+ [FileType.FOLDER]: '文件夹',
34
+ [FileType.FILE]: '文件',
35
+ [FileType.IMAGE]: '图片',
36
+ [FileType.VIDEO]: '视频',
37
+ [FileType.MUSIC]: '音频',
38
+ [FileType.DOCUMENT]: '文档',
39
+ [FileType.CODE]: '代码',
40
+ [FileType.TEXT]: '文本',
41
+ [FileType.PDF]: 'PDF',
42
+ [FileType.ARCHIVE]: '压缩包',
43
+ [FileType.APPLICATION]: '应用程序',
44
+ [FileType.UNKNOWN]: '未知',
45
+ };
46
+ return labels[type] || type;
47
+ }
48
+
49
+ export function FileList({
50
+ items,
51
+ selectedIds,
52
+ sortConfig,
53
+ editingId,
54
+ dragOverId,
55
+ onSelect,
56
+ onOpen,
57
+ onContextMenu,
58
+ onContextMenuEmpty,
59
+ onNameClick,
60
+ onRename,
61
+ onRenameCancel,
62
+ onSort,
63
+ onDragStart,
64
+ onDragOver,
65
+ onDragLeave,
66
+ onDrop,
67
+ }: FileListProps) {
68
+ const renameInputRef = useRef<HTMLInputElement>(null);
69
+
70
+ /**
71
+ * 空白处右键菜单
72
+ */
73
+ const handleEmptyContextMenu = (e: React.MouseEvent) => {
74
+ const target = e.target as HTMLElement;
75
+ if (!target.closest('tr')) {
76
+ onContextMenuEmpty?.(e);
77
+ }
78
+ };
79
+
80
+ /**
81
+ * 处理重命名(blur 事件)
82
+ */
83
+ const handleRename = (item: FileItem, e: React.FocusEvent<HTMLInputElement>) => {
84
+ const input = e.target;
85
+ const newName = input.value.trim();
86
+ if (newName && newName !== item.name) {
87
+ onRename?.(item, newName);
88
+ } else {
89
+ onRenameCancel?.(item);
90
+ }
91
+ };
92
+
93
+ /**
94
+ * Enter 键保存
95
+ */
96
+ const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
97
+ e.currentTarget.blur();
98
+ };
99
+
100
+ /**
101
+ * Escape 键取消
102
+ */
103
+ const handleEscapeKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
104
+ const input = e.currentTarget;
105
+ const item = items.find((i) => i.id === editingId);
106
+ if (item) {
107
+ input.value = item.name;
108
+ }
109
+ input.blur();
110
+ };
111
+
112
+ return (
113
+ <div className="file-list" onContextMenu={(e) => { e.preventDefault(); handleEmptyContextMenu(e); }}>
114
+ <table className="file-list-table">
115
+ <thead className="file-list-header">
116
+ <tr>
117
+ <th
118
+ className="file-list-header-cell file-list-header-cell--name"
119
+ onClick={() => onSort?.('name')}
120
+ >
121
+ 名称
122
+ {sortConfig?.field === 'name' && (
123
+ <SortIndicator direction={sortConfig.direction} />
124
+ )}
125
+ </th>
126
+ <th
127
+ className="file-list-header-cell"
128
+ onClick={() => onSort?.('dateModified')}
129
+ >
130
+ 修改日期
131
+ {sortConfig?.field === 'dateModified' && (
132
+ <SortIndicator direction={sortConfig.direction} />
133
+ )}
134
+ </th>
135
+ <th
136
+ className="file-list-header-cell"
137
+ onClick={() => onSort?.('size')}
138
+ >
139
+ 大小
140
+ {sortConfig?.field === 'size' && (
141
+ <SortIndicator direction={sortConfig.direction} />
142
+ )}
143
+ </th>
144
+ <th
145
+ className="file-list-header-cell"
146
+ onClick={() => onSort?.('type')}
147
+ >
148
+ 类型
149
+ {sortConfig?.field === 'type' && (
150
+ <SortIndicator direction={sortConfig.direction} />
151
+ )}
152
+ </th>
153
+ </tr>
154
+ </thead>
155
+ <tbody className="file-list-body">
156
+ {items.map((item, index) => {
157
+ const isSelected = selectedIds.has(item.id);
158
+ const isEditing = editingId === item.id;
159
+ const isDragOver = dragOverId === item.id;
160
+
161
+ let rowClassName = 'file-list-row';
162
+ if (isSelected) {
163
+ rowClassName += ' file-list-row--selected';
164
+ } else if (isDragOver) {
165
+ rowClassName += ' file-list-row--drag-over';
166
+ } else if (index % 2 === 0) {
167
+ rowClassName += ' file-list-row--even';
168
+ } else {
169
+ rowClassName += ' file-list-row--odd';
170
+ }
171
+
172
+ return (
173
+ <tr
174
+ key={item.id}
175
+ draggable={!isEditing}
176
+ onDragStart={(e) => onDragStart?.(e, item)}
177
+ onDragOver={(e) => { e.preventDefault(); onDragOver?.(e, item); }}
178
+ onDragLeave={(e) => onDragLeave?.(e)}
179
+ onDrop={(e) => { e.preventDefault(); onDrop?.(e, item); }}
180
+ onClick={(e) => { e.stopPropagation(); onSelect?.(item, e); }}
181
+ onDoubleClick={(e) => { e.stopPropagation(); onOpen?.(item); }}
182
+ onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(item, e); }}
183
+ className={rowClassName}
184
+ >
185
+ <td className={`file-list-cell file-list-cell--name ${isSelected ? 'file-list-cell--selected' : ''}`}>
186
+ <FileIcon type={item.type} name={item.name} size={16} />
187
+ {isEditing ? (
188
+ <input
189
+ ref={renameInputRef}
190
+ type="text"
191
+ className="file-list-rename-input"
192
+ defaultValue={item.name}
193
+ onBlur={(e) => handleRename(item, e)}
194
+ onKeyDown={(e) => {
195
+ if (e.key === 'Enter') {
196
+ handleEnterKey(e);
197
+ } else if (e.key === 'Escape') {
198
+ handleEscapeKey(e);
199
+ }
200
+ }}
201
+ autoFocus
202
+ />
203
+ ) : (
204
+ <span
205
+ onClick={(e) => { e.stopPropagation(); onNameClick?.(item, e); }}
206
+ className={`file-list-name ${isSelected ? 'file-list-name--selected' : ''}`}
207
+ >
208
+ {item.name}
209
+ </span>
210
+ )}
211
+ </td>
212
+ <td className={`file-list-cell ${isSelected ? 'file-list-cell--selected' : ''}`}>
213
+ {item.dateModified || '--'}
214
+ </td>
215
+ <td className={`file-list-cell file-list-cell--size ${isSelected ? 'file-list-cell--selected' : ''}`}>
216
+ {item.size || '--'}
217
+ </td>
218
+ <td className={`file-list-cell ${isSelected ? 'file-list-cell--selected' : ''}`}>
219
+ {getTypeLabel(item.type)}
220
+ </td>
221
+ </tr>
222
+ );
223
+ })}
224
+ </tbody>
225
+ </table>
226
+ </div>
227
+ );
228
+ }
@@ -0,0 +1,36 @@
1
+ .file-list-view {
2
+ flex: 1;
3
+ overflow: auto;
4
+ padding: 12px;
5
+ user-select: none;
6
+ min-height: 0;
7
+ }
8
+
9
+ .file-list-view-loading,
10
+ .file-list-view-empty {
11
+ display: flex;
12
+ flex-direction: column;
13
+ align-items: center;
14
+ justify-content: center;
15
+ height: 100%;
16
+ color: rgb(156, 163, 175);
17
+ gap: 16px;
18
+ }
19
+
20
+ .file-list-view-spinner {
21
+ width: 32px;
22
+ height: 32px;
23
+ border: 3px solid rgba(156, 163, 175, 0.2);
24
+ border-top-color: rgb(59, 130, 246);
25
+ border-radius: 50%;
26
+ animation: spin 0.8s linear infinite;
27
+ }
28
+
29
+ @keyframes spin {
30
+ to { transform: rotate(360deg); }
31
+ }
32
+
33
+ .file-list-view-empty-icon {
34
+ opacity: 0.3;
35
+ }
36
+
@@ -0,0 +1,355 @@
1
+ import { useState, useCallback, useImperativeHandle, forwardRef } from 'react';
2
+ import { Icon } from '@iconify/react';
3
+ import { FileGrid } from './FileGrid';
4
+ import { FileList } from './FileList';
5
+ import { FileType, type FileItem, type SortConfig, type FileExplorerAdapter } from '../types';
6
+ import './FileListView.css';
7
+
8
+ interface FileListViewProps {
9
+ items: FileItem[];
10
+ viewMode?: 'grid' | 'list';
11
+ loading?: boolean;
12
+ adapter?: FileExplorerAdapter;
13
+ currentPath?: string;
14
+ getAppIconUrl?: (item: FileItem) => string | undefined;
15
+ onOpen?: (item: FileItem) => void;
16
+ onSelectionChange?: (ids: Set<string>, items: FileItem[]) => void;
17
+ onContextMenu?: (event: React.MouseEvent, item: FileItem) => void;
18
+ onContextMenuEmpty?: (event: React.MouseEvent) => void;
19
+ onRename?: (item: FileItem, newName: string) => void;
20
+ onSortChange?: (config: SortConfig) => void;
21
+ onMove?: (sourceIds: string[], targetId: string) => void;
22
+ }
23
+
24
+ export interface FileListViewHandle {
25
+ clearSelection: () => void;
26
+ startRename: (id: string) => void;
27
+ selectAll: () => void;
28
+ selectedIds: Set<string>;
29
+ selectedItems: FileItem[];
30
+ }
31
+
32
+ export const FileListView = forwardRef<FileListViewHandle, FileListViewProps>(
33
+ (
34
+ {
35
+ items,
36
+ viewMode = 'grid',
37
+ loading = false,
38
+ adapter,
39
+ currentPath,
40
+ getAppIconUrl,
41
+ onOpen,
42
+ onSelectionChange,
43
+ onContextMenu,
44
+ onContextMenuEmpty,
45
+ onRename,
46
+ onSortChange,
47
+ onMove,
48
+ },
49
+ ref
50
+ ) => {
51
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
52
+ const [editingId, setEditingId] = useState<string | null>(null);
53
+ const [dragOverId, setDragOverId] = useState<string | null>(null);
54
+ const [sortConfig, setSortConfig] = useState<SortConfig>({
55
+ field: 'name',
56
+ direction: 'asc',
57
+ });
58
+
59
+ // 获取选中的文件项
60
+ const selectedItems = items.filter((item) => selectedIds.has(item.id));
61
+
62
+ // 选择处理
63
+ const handleSelect = useCallback(
64
+ (item: FileItem, e: React.MouseEvent) => {
65
+ if (e.metaKey || e.ctrlKey) {
66
+ // 多选
67
+ setSelectedIds((prev) => {
68
+ const newSet = new Set(prev);
69
+ if (newSet.has(item.id)) {
70
+ newSet.delete(item.id);
71
+ } else {
72
+ newSet.add(item.id);
73
+ }
74
+ const newItems = items.filter((i) => newSet.has(i.id));
75
+ onSelectionChange?.(newSet, newItems);
76
+ return newSet;
77
+ });
78
+ } else if (e.shiftKey && selectedIds.size > 0) {
79
+ // 范围选择
80
+ const lastId = Array.from(selectedIds).pop();
81
+ const lastIndex = items.findIndex((i) => i.id === lastId);
82
+ const currentIndex = items.findIndex((i) => i.id === item.id);
83
+ const start = Math.min(lastIndex, currentIndex);
84
+ const end = Math.max(lastIndex, currentIndex);
85
+ const newSet = new Set<string>();
86
+ for (let i = start; i <= end; i++) {
87
+ newSet.add(items[i]!.id);
88
+ }
89
+ setSelectedIds(newSet);
90
+ const newItems = items.filter((i) => newSet.has(i.id));
91
+ onSelectionChange?.(newSet, newItems);
92
+ } else {
93
+ // 单选
94
+ const newSet = new Set([item.id]);
95
+ setSelectedIds(newSet);
96
+ onSelectionChange?.(newSet, [item]);
97
+ }
98
+ },
99
+ [items, selectedIds, onSelectionChange]
100
+ );
101
+
102
+ const handleEmptyClick = useCallback(
103
+ (e: React.MouseEvent) => {
104
+ if (e.target === e.currentTarget) {
105
+ clearSelection();
106
+ }
107
+ },
108
+ []
109
+ );
110
+
111
+ // 清除选择
112
+ const clearSelection = useCallback(() => {
113
+ setSelectedIds(new Set());
114
+ onSelectionChange?.(new Set(), []);
115
+ }, [onSelectionChange]);
116
+
117
+ // 打开文件/文件夹
118
+ const handleOpen = useCallback(
119
+ (item: FileItem) => {
120
+ onOpen?.(item);
121
+ },
122
+ [onOpen]
123
+ );
124
+
125
+ // 右键菜单
126
+ const handleContextMenu = useCallback(
127
+ (item: FileItem, e: React.MouseEvent) => {
128
+ if (!selectedIds.has(item.id)) {
129
+ const newSet = new Set([item.id]);
130
+ setSelectedIds(newSet);
131
+ onSelectionChange?.(newSet, [item]);
132
+ }
133
+ onContextMenu?.(e, item);
134
+ },
135
+ [selectedIds, onSelectionChange, onContextMenu]
136
+ );
137
+
138
+ const handleEmptyContextMenu = useCallback(
139
+ (e: React.MouseEvent) => {
140
+ const target = e.target as HTMLElement;
141
+ if (
142
+ !target.closest('.file-grid-item') &&
143
+ !target.closest('.file-list-row')
144
+ ) {
145
+ clearSelection();
146
+ onContextMenuEmpty?.(e);
147
+ }
148
+ },
149
+ [clearSelection, onContextMenuEmpty]
150
+ );
151
+
152
+ // 从子组件触发的空白处右键菜单
153
+ const handleEmptyContextMenuFromChild = useCallback(
154
+ (e: React.MouseEvent) => {
155
+ clearSelection();
156
+ onContextMenuEmpty?.(e);
157
+ },
158
+ [clearSelection, onContextMenuEmpty]
159
+ );
160
+
161
+ // 名称点击(用于重命名)
162
+ const handleNameClick = useCallback(
163
+ (item: FileItem, e: React.MouseEvent) => {
164
+ if (selectedIds.has(item.id) && selectedIds.size === 1) {
165
+ setTimeout(() => {
166
+ if (selectedIds.has(item.id)) {
167
+ setEditingId(item.id);
168
+ }
169
+ }, 500);
170
+ }
171
+ },
172
+ [selectedIds]
173
+ );
174
+
175
+ // 重命名
176
+ const handleRename = useCallback(
177
+ (item: FileItem, newName: string) => {
178
+ if (newName && newName !== item.name) {
179
+ onRename?.(item, newName);
180
+ }
181
+ setEditingId(null);
182
+ },
183
+ [onRename]
184
+ );
185
+
186
+ const handleRenameCancel = useCallback(() => {
187
+ setEditingId(null);
188
+ }, []);
189
+
190
+ // 排序
191
+ const handleSort = useCallback(
192
+ (field: string) => {
193
+ setSortConfig((prev) => {
194
+ const newConfig: SortConfig =
195
+ prev.field === field
196
+ ? { ...prev, direction: prev.direction === 'asc' ? 'desc' : 'asc' }
197
+ : { field: field as SortConfig['field'], direction: 'asc' };
198
+ onSortChange?.(newConfig);
199
+ return newConfig;
200
+ });
201
+ },
202
+ [onSortChange]
203
+ );
204
+
205
+ // 拖拽
206
+ const handleDragStart = useCallback(
207
+ (e: React.DragEvent, item: FileItem) => {
208
+ if (!selectedIds.has(item.id)) {
209
+ const newSet = new Set([item.id]);
210
+ setSelectedIds(newSet);
211
+ onSelectionChange?.(newSet, [item]);
212
+ }
213
+ e.dataTransfer.setData(
214
+ 'text/plain',
215
+ JSON.stringify([...selectedIds])
216
+ );
217
+ },
218
+ [selectedIds, onSelectionChange]
219
+ );
220
+
221
+ const handleDragOver = useCallback(
222
+ (e: React.DragEvent, item: FileItem) => {
223
+ if (item.type === FileType.FOLDER && !selectedIds.has(item.id)) {
224
+ setDragOverId(item.id);
225
+ }
226
+ },
227
+ [selectedIds]
228
+ );
229
+
230
+ const handleDragLeave = useCallback(() => {
231
+ setDragOverId(null);
232
+ }, []);
233
+
234
+ const handleDrop = useCallback(
235
+ (e: React.DragEvent, targetItem: FileItem) => {
236
+ setDragOverId(null);
237
+
238
+ if (targetItem.type !== FileType.FOLDER) return;
239
+
240
+ const data = e.dataTransfer.getData('text/plain');
241
+ if (!data) return;
242
+
243
+ try {
244
+ const draggedIds: string[] = JSON.parse(data);
245
+ if (draggedIds.includes(targetItem.id)) return;
246
+
247
+ onMove?.(draggedIds, targetItem.id);
248
+ clearSelection();
249
+ } catch (error) {
250
+ console.error('拖拽解析失败:', error);
251
+ }
252
+ },
253
+ [onMove, clearSelection]
254
+ );
255
+
256
+ // 开始重命名(供外部调用)
257
+ const startRename = useCallback((id: string) => {
258
+ setEditingId(id);
259
+ }, []);
260
+
261
+ // 全选
262
+ const selectAll = useCallback(() => {
263
+ const newSet = new Set(items.map((i) => i.id));
264
+ setSelectedIds(newSet);
265
+ onSelectionChange?.(newSet, items);
266
+ }, [items, onSelectionChange]);
267
+
268
+ // 暴露方法
269
+ useImperativeHandle(
270
+ ref,
271
+ () => ({
272
+ clearSelection,
273
+ startRename,
274
+ selectAll,
275
+ selectedIds,
276
+ selectedItems,
277
+ }),
278
+ [selectedIds, selectedItems, clearSelection, startRename, selectAll]
279
+ );
280
+
281
+ return (
282
+ <div
283
+ className="file-list-view"
284
+ onClick={handleEmptyClick}
285
+ onContextMenu={(e) => {
286
+ e.preventDefault();
287
+ handleEmptyContextMenu(e);
288
+ }}
289
+ >
290
+ {/* 加载中 */}
291
+ {loading && (
292
+ <div className="file-list-view-loading">
293
+ <div className="file-list-view-spinner"></div>
294
+ <p>加载中...</p>
295
+ </div>
296
+ )}
297
+
298
+ {/* 空文件夹 */}
299
+ {!loading && items.length === 0 && (
300
+ <div className="file-list-view-empty">
301
+ <Icon icon="lucide:folder-open" width={64} height={64} className="file-list-view-empty-icon" />
302
+ <p>文件夹为空</p>
303
+ </div>
304
+ )}
305
+
306
+ {/* 网格视图 */}
307
+ {!loading && items.length > 0 && viewMode === 'grid' && (
308
+ <FileGrid
309
+ items={items}
310
+ selectedIds={selectedIds}
311
+ editingId={editingId}
312
+ dragOverId={dragOverId}
313
+ getAppIconUrl={getAppIconUrl}
314
+ onSelect={handleSelect}
315
+ onOpen={handleOpen}
316
+ onContextMenu={handleContextMenu}
317
+ onContextMenuEmpty={handleEmptyContextMenuFromChild}
318
+ onNameClick={handleNameClick}
319
+ onRename={handleRename}
320
+ onRenameCancel={handleRenameCancel}
321
+ onDragStart={handleDragStart}
322
+ onDragOver={handleDragOver}
323
+ onDragLeave={handleDragLeave}
324
+ onDrop={handleDrop}
325
+ />
326
+ )}
327
+
328
+ {/* 列表视图 */}
329
+ {!loading && items.length > 0 && viewMode === 'list' && (
330
+ <FileList
331
+ items={items}
332
+ selectedIds={selectedIds}
333
+ editingId={editingId}
334
+ dragOverId={dragOverId}
335
+ sortConfig={sortConfig}
336
+ onSelect={handleSelect}
337
+ onOpen={handleOpen}
338
+ onContextMenu={handleContextMenu}
339
+ onContextMenuEmpty={handleEmptyContextMenuFromChild}
340
+ onNameClick={handleNameClick}
341
+ onRename={handleRename}
342
+ onRenameCancel={handleRenameCancel}
343
+ onSort={handleSort}
344
+ onDragStart={handleDragStart}
345
+ onDragOver={handleDragOver}
346
+ onDragLeave={handleDragLeave}
347
+ onDrop={handleDrop}
348
+ />
349
+ )}
350
+ </div>
351
+ );
352
+ }
353
+ );
354
+
355
+ FileListView.displayName = 'FileListView';
@@ -0,0 +1,94 @@
1
+ .file-sidebar {
2
+ width: 12rem;
3
+ background: rgba(243, 244, 246, 0.5);
4
+ backdrop-filter: blur(24px);
5
+ border-right: 1px solid rgba(229, 231, 233, 0.5);
6
+ display: flex;
7
+ flex-direction: column;
8
+ padding-top: 2rem;
9
+ padding-bottom: 1rem;
10
+ height: 100%;
11
+ box-sizing: border-box;
12
+ overflow-y: auto;
13
+ overflow-x: hidden;
14
+ user-select: none;
15
+ -webkit-app-region: drag; /* 整个侧边栏可拖拽 */
16
+ }
17
+
18
+ /* 自定义滚动条样式 */
19
+ .file-sidebar::-webkit-scrollbar {
20
+ width: 6px;
21
+ }
22
+
23
+ .file-sidebar::-webkit-scrollbar-track {
24
+ background: transparent;
25
+ }
26
+
27
+ .file-sidebar::-webkit-scrollbar-thumb {
28
+ background: rgba(0, 0, 0, 0.2);
29
+ border-radius: 3px;
30
+ }
31
+
32
+ .file-sidebar::-webkit-scrollbar-thumb:hover {
33
+ background: rgba(0, 0, 0, 0.3);
34
+ }
35
+
36
+ .file-sidebar-section {
37
+ margin-bottom: 0;
38
+ -webkit-app-region: no-drag; /* section 内容区域不可拖拽 */
39
+ }
40
+
41
+ .file-sidebar-section-title {
42
+ padding: 0 1rem;
43
+ margin-bottom: 0.5rem;
44
+ font-size: 0.75rem;
45
+ font-weight: 600;
46
+ color: rgb(107, 114, 128);
47
+ text-transform: uppercase;
48
+ letter-spacing: 0.05em;
49
+ }
50
+
51
+ .file-sidebar-section + .file-sidebar-section .file-sidebar-section-title {
52
+ margin-top: 1.5rem;
53
+ }
54
+
55
+ .file-sidebar-list {
56
+ list-style: none;
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: 0.25rem;
60
+ padding: 0 0.5rem;
61
+ margin: 0;
62
+ }
63
+
64
+ .file-sidebar-item {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 0.75rem;
68
+ padding: 0.375rem 0.75rem;
69
+ border-radius: 0.375rem;
70
+ cursor: pointer;
71
+ font-size: 0.875rem;
72
+ font-weight: 500;
73
+ color: rgb(75, 85, 99);
74
+ transition: all 200ms;
75
+ /* no-drag 已由 section 继承 */
76
+ }
77
+
78
+ .file-sidebar-item:hover {
79
+ background: rgba(229, 231, 233, 0.5);
80
+ color: black;
81
+ }
82
+
83
+ .file-sidebar-item--active {
84
+ background: rgba(209, 213, 219, 0.6);
85
+ color: black;
86
+ }
87
+
88
+ .file-sidebar-item-icon {
89
+ color: rgb(107, 114, 128);
90
+ }
91
+
92
+ .file-sidebar-item-icon--active {
93
+ color: rgb(37, 99, 235);
94
+ }