@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,321 @@
1
+ <template>
2
+ <div
3
+ class="file-list-view"
4
+ @click="handleEmptyClick"
5
+ @contextmenu.prevent="handleEmptyContextMenu"
6
+ >
7
+ <!-- 加载中 -->
8
+ <div v-if="loading" class="file-list-view-loading">
9
+ <div class="file-list-view-spinner"></div>
10
+ <p>加载中...</p>
11
+ </div>
12
+
13
+ <!-- 空文件夹 -->
14
+ <div v-else-if="items.length === 0" class="file-list-view-empty">
15
+ <FolderOpen :size="64" class="file-list-view-empty-icon" />
16
+ <p>文件夹为空</p>
17
+ </div>
18
+
19
+ <!-- 网格视图 -->
20
+ <FileGrid
21
+ v-else-if="viewMode === 'grid'"
22
+ :items="items"
23
+ :selected-ids="selectedIds"
24
+ :editing-id="editingId"
25
+ :drag-over-id="dragOverId"
26
+ :get-app-icon-url="getAppIconUrl"
27
+ @select="handleSelect"
28
+ @open="handleOpen"
29
+ @context-menu="handleContextMenu"
30
+ @context-menu-empty="handleEmptyContextMenuFromChild"
31
+ @name-click="handleNameClick"
32
+ @rename="handleRename"
33
+ @rename-cancel="handleRenameCancel"
34
+ @drag-start="handleDragStart"
35
+ @drag-over="handleDragOver"
36
+ @drag-leave="handleDragLeave"
37
+ @drop="handleDrop"
38
+ />
39
+
40
+ <!-- 列表视图 -->
41
+ <FileList
42
+ v-else
43
+ :items="items"
44
+ :selected-ids="selectedIds"
45
+ :editing-id="editingId"
46
+ :drag-over-id="dragOverId"
47
+ :sort-config="sortConfig"
48
+ @select="handleSelect"
49
+ @open="handleOpen"
50
+ @context-menu="handleContextMenu"
51
+ @context-menu-empty="handleEmptyContextMenuFromChild"
52
+ @name-click="handleNameClick"
53
+ @rename="handleRename"
54
+ @rename-cancel="handleRenameCancel"
55
+ @sort="handleSort"
56
+ @drag-start="handleDragStart"
57
+ @drag-over="handleDragOver"
58
+ @drag-leave="handleDragLeave"
59
+ @drop="handleDrop"
60
+ />
61
+ </div>
62
+ </template>
63
+
64
+ <script setup lang="ts">
65
+ import { ref, watch, computed } from 'vue';
66
+ import { FolderOpen } from 'lucide-vue-next';
67
+ import FileGrid from './FileGrid.vue';
68
+ import FileList from './FileList.vue';
69
+ import { FileType, type FileItem, type SortConfig, type FileExplorerAdapter } from '../types';
70
+
71
+ // Props
72
+ const props = withDefaults(defineProps<{
73
+ /** 文件列表 */
74
+ items: FileItem[];
75
+ /** 视图模式 */
76
+ viewMode?: 'grid' | 'list';
77
+ /** 是否加载中 */
78
+ loading?: boolean;
79
+ /** 文件操作适配器(可选,用于内置操作) */
80
+ adapter?: FileExplorerAdapter;
81
+ /** 当前路径(用于内置操作) */
82
+ currentPath?: string;
83
+ /** 获取应用程序图标 URL 的函数 */
84
+ getAppIconUrl?: (item: FileItem) => string | undefined;
85
+ }>(), {
86
+ viewMode: 'grid',
87
+ loading: false,
88
+ });
89
+
90
+ // Events
91
+ const emit = defineEmits<{
92
+ /** 打开文件/文件夹 */
93
+ (e: 'open', item: FileItem): void;
94
+ /** 选中项变化 */
95
+ (e: 'selection-change', ids: Set<string>, items: FileItem[]): void;
96
+ /** 右键菜单(文件项) */
97
+ (e: 'context-menu', event: MouseEvent, item: FileItem): void;
98
+ /** 右键菜单(空白处) */
99
+ (e: 'context-menu-empty', event: MouseEvent): void;
100
+ /** 重命名 */
101
+ (e: 'rename', item: FileItem, newName: string): void;
102
+ /** 排序变化 */
103
+ (e: 'sort-change', config: SortConfig): void;
104
+ /** 拖拽移动文件 */
105
+ (e: 'move', sourceIds: string[], targetId: string): void;
106
+ }>();
107
+
108
+ // 内部状态
109
+ const selectedIds = ref<Set<string>>(new Set());
110
+ const editingId = ref<string | null>(null);
111
+ const dragOverId = ref<string | null>(null);
112
+ const sortConfig = ref<SortConfig>({ field: 'name', direction: 'asc' });
113
+
114
+ // 获取选中的文件项
115
+ const selectedItems = computed(() =>
116
+ props.items.filter(item => selectedIds.value.has(item.id))
117
+ );
118
+
119
+ // 选择处理
120
+ const handleSelect = (item: FileItem, e: MouseEvent) => {
121
+ if (e.metaKey || e.ctrlKey) {
122
+ // 多选
123
+ const newSet = new Set(selectedIds.value);
124
+ if (newSet.has(item.id)) {
125
+ newSet.delete(item.id);
126
+ } else {
127
+ newSet.add(item.id);
128
+ }
129
+ selectedIds.value = newSet;
130
+ } else if (e.shiftKey && selectedIds.value.size > 0) {
131
+ // 范围选择
132
+ const lastId = Array.from(selectedIds.value).pop();
133
+ const lastIndex = props.items.findIndex(i => i.id === lastId);
134
+ const currentIndex = props.items.findIndex(i => i.id === item.id);
135
+ const start = Math.min(lastIndex, currentIndex);
136
+ const end = Math.max(lastIndex, currentIndex);
137
+ const newSet = new Set<string>();
138
+ for (let i = start; i <= end; i++) {
139
+ newSet.add(props.items[i]!.id);
140
+ }
141
+ selectedIds.value = newSet;
142
+ } else {
143
+ // 单选
144
+ selectedIds.value = new Set([item.id]);
145
+ }
146
+
147
+ emit('selection-change', selectedIds.value, selectedItems.value);
148
+ };
149
+
150
+ const handleEmptyClick = (e: MouseEvent) => {
151
+ // 只有点击空白处才清除选择
152
+ if (e.target === e.currentTarget) {
153
+ clearSelection();
154
+ }
155
+ };
156
+
157
+ // 清除选择
158
+ const clearSelection = () => {
159
+ selectedIds.value = new Set();
160
+ emit('selection-change', selectedIds.value, []);
161
+ };
162
+
163
+ // 打开文件/文件夹
164
+ const handleOpen = (item: FileItem) => {
165
+ emit('open', item);
166
+ };
167
+
168
+ // 右键菜单
169
+ const handleContextMenu = (item: FileItem, e: MouseEvent) => {
170
+ if (!selectedIds.value.has(item.id)) {
171
+ selectedIds.value = new Set([item.id]);
172
+ emit('selection-change', selectedIds.value, [item]);
173
+ }
174
+ emit('context-menu', e, item);
175
+ };
176
+
177
+ const handleEmptyContextMenu = (e: MouseEvent) => {
178
+ // 检查是否点击了文件项
179
+ const target = e.target as HTMLElement;
180
+ if (!target.closest('.file-grid-item') && !target.closest('.file-list-row')) {
181
+ clearSelection();
182
+ emit('context-menu-empty', e);
183
+ }
184
+ };
185
+
186
+ // 从子组件触发的空白处右键菜单
187
+ const handleEmptyContextMenuFromChild = (e: MouseEvent) => {
188
+ clearSelection();
189
+ emit('context-menu-empty', e);
190
+ };
191
+
192
+ // 名称点击(用于重命名)
193
+ const handleNameClick = (item: FileItem, e: MouseEvent) => {
194
+ if (selectedIds.value.has(item.id) && selectedIds.value.size === 1) {
195
+ setTimeout(() => {
196
+ if (selectedIds.value.has(item.id)) {
197
+ editingId.value = item.id;
198
+ }
199
+ }, 500);
200
+ }
201
+ };
202
+
203
+ // 重命名
204
+ const handleRename = (item: FileItem, newName: string) => {
205
+ if (newName && newName !== item.name) {
206
+ emit('rename', item, newName);
207
+ }
208
+ editingId.value = null;
209
+ };
210
+
211
+ const handleRenameCancel = () => {
212
+ editingId.value = null;
213
+ };
214
+
215
+ // 排序
216
+ const handleSort = (field: string) => {
217
+ if (sortConfig.value.field === field) {
218
+ sortConfig.value.direction = sortConfig.value.direction === 'asc' ? 'desc' : 'asc';
219
+ } else {
220
+ sortConfig.value.field = field as SortConfig['field'];
221
+ sortConfig.value.direction = 'asc';
222
+ }
223
+ emit('sort-change', { ...sortConfig.value });
224
+ };
225
+
226
+ // 拖拽
227
+ const handleDragStart = (e: DragEvent, item: FileItem) => {
228
+ if (!selectedIds.value.has(item.id)) {
229
+ selectedIds.value = new Set([item.id]);
230
+ emit('selection-change', selectedIds.value, [item]);
231
+ }
232
+ e.dataTransfer?.setData('text/plain', JSON.stringify([...selectedIds.value]));
233
+ };
234
+
235
+ const handleDragOver = (e: DragEvent, item: FileItem) => {
236
+ if (item.type === FileType.FOLDER && !selectedIds.value.has(item.id)) {
237
+ dragOverId.value = item.id;
238
+ }
239
+ };
240
+
241
+ const handleDragLeave = () => {
242
+ dragOverId.value = null;
243
+ };
244
+
245
+ const handleDrop = (e: DragEvent, targetItem: FileItem) => {
246
+ dragOverId.value = null;
247
+
248
+ if (targetItem.type !== FileType.FOLDER) return;
249
+
250
+ const data = e.dataTransfer?.getData('text/plain');
251
+ if (!data) return;
252
+
253
+ try {
254
+ const draggedIds: string[] = JSON.parse(data);
255
+ if (draggedIds.includes(targetItem.id)) return;
256
+
257
+ emit('move', draggedIds, targetItem.id);
258
+ clearSelection();
259
+ } catch (error) {
260
+ console.error('拖拽解析失败:', error);
261
+ }
262
+ };
263
+
264
+ // 开始重命名(供外部调用)
265
+ const startRename = (id: string) => {
266
+ editingId.value = id;
267
+ };
268
+
269
+ // 全选
270
+ const selectAll = () => {
271
+ selectedIds.value = new Set(props.items.map(i => i.id));
272
+ emit('selection-change', selectedIds.value, props.items);
273
+ };
274
+
275
+ // 暴露方法
276
+ defineExpose({
277
+ clearSelection,
278
+ startRename,
279
+ selectAll,
280
+ selectedIds,
281
+ selectedItems,
282
+ });
283
+ </script>
284
+
285
+ <style scoped>
286
+ .file-list-view {
287
+ flex: 1;
288
+ overflow: auto;
289
+ padding: 12px;
290
+ user-select: none;
291
+ min-height: 0;
292
+ }
293
+
294
+ .file-list-view-loading,
295
+ .file-list-view-empty {
296
+ display: flex;
297
+ flex-direction: column;
298
+ align-items: center;
299
+ justify-content: center;
300
+ height: 100%;
301
+ color: rgb(156, 163, 175);
302
+ gap: 16px;
303
+ }
304
+
305
+ .file-list-view-spinner {
306
+ width: 32px;
307
+ height: 32px;
308
+ border: 3px solid rgba(156, 163, 175, 0.2);
309
+ border-top-color: rgb(59, 130, 246);
310
+ border-radius: 50%;
311
+ animation: spin 0.8s linear infinite;
312
+ }
313
+
314
+ @keyframes spin {
315
+ to { transform: rotate(360deg); }
316
+ }
317
+
318
+ .file-list-view-empty-icon {
319
+ opacity: 0.3;
320
+ }
321
+ </style>
@@ -0,0 +1,158 @@
1
+ <template>
2
+ <div class="file-sidebar">
3
+ <div v-for="section in sections" :key="section.id" class="file-sidebar-section">
4
+ <div class="file-sidebar-section-title">{{ section.title }}</div>
5
+ <ul class="file-sidebar-list">
6
+ <li
7
+ v-for="item in section.items"
8
+ :key="item.id"
9
+ @click="handleNavigate(item)"
10
+ :class="[
11
+ 'file-sidebar-item',
12
+ activeId === item.id ? 'file-sidebar-item--active' : ''
13
+ ]"
14
+ >
15
+ <Icon
16
+ :icon="getIconName(item.icon)"
17
+ :width="18"
18
+ :height="18"
19
+ :class="activeId === item.id ? 'file-sidebar-item-icon--active' : 'file-sidebar-item-icon'"
20
+ />
21
+ <span>{{ item.label }}</span>
22
+ </li>
23
+ </ul>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { Icon } from '@iconify/vue';
30
+ import type { SidebarItem } from '../types';
31
+
32
+ interface SidebarSection {
33
+ id: string;
34
+ title: string;
35
+ items: SidebarItem[];
36
+ }
37
+
38
+ interface Props {
39
+ sections: SidebarSection[];
40
+ activeId?: string;
41
+ }
42
+
43
+ defineProps<Props>();
44
+
45
+ const emit = defineEmits<{
46
+ navigate: [item: SidebarItem];
47
+ }>();
48
+
49
+ // 将 Lucide 图标名称转换为 Iconify 格式
50
+ const getIconName = (iconName?: string): string => {
51
+ if (!iconName) return 'mdi:folder';
52
+ // 如果已经是 Iconify 格式,直接返回
53
+ if (iconName.includes(':')) return iconName;
54
+ // 否则转换为 lucide: 格式
55
+ return `lucide:${iconName.toLowerCase()}`;
56
+ };
57
+
58
+ const handleNavigate = (item: SidebarItem) => {
59
+ emit('navigate', item);
60
+ };
61
+ </script>
62
+
63
+ <style scoped>
64
+ .file-sidebar {
65
+ width: 12rem;
66
+ background: rgba(243, 244, 246, 0.5);
67
+ backdrop-filter: blur(24px);
68
+ border-right: 1px solid rgba(229, 231, 233, 0.5);
69
+ display: flex;
70
+ flex-direction: column;
71
+ padding-top: 2rem;
72
+ padding-bottom: 1rem;
73
+ height: 100%;
74
+ box-sizing: border-box;
75
+ overflow-y: auto;
76
+ overflow-x: hidden;
77
+ user-select: none;
78
+ -webkit-app-region: drag; /* 整个侧边栏可拖拽 */
79
+ }
80
+
81
+ /* 自定义滚动条样式 */
82
+ .file-sidebar::-webkit-scrollbar {
83
+ width: 6px;
84
+ }
85
+
86
+ .file-sidebar::-webkit-scrollbar-track {
87
+ background: transparent;
88
+ }
89
+
90
+ .file-sidebar::-webkit-scrollbar-thumb {
91
+ background: rgba(0, 0, 0, 0.2);
92
+ border-radius: 3px;
93
+ }
94
+
95
+ .file-sidebar::-webkit-scrollbar-thumb:hover {
96
+ background: rgba(0, 0, 0, 0.3);
97
+ }
98
+
99
+ .file-sidebar-section {
100
+ margin-bottom: 0;
101
+ -webkit-app-region: no-drag; /* section 内容区域不可拖拽 */
102
+ }
103
+
104
+ .file-sidebar-section-title {
105
+ padding: 0 1rem;
106
+ margin-bottom: 0.5rem;
107
+ font-size: 0.75rem;
108
+ font-weight: 600;
109
+ color: rgb(107, 114, 128);
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.05em;
112
+ }
113
+
114
+ .file-sidebar-section + .file-sidebar-section .file-sidebar-section-title {
115
+ margin-top: 1.5rem;
116
+ }
117
+
118
+ .file-sidebar-list {
119
+ list-style: none;
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 0.25rem;
123
+ padding: 0 0.5rem;
124
+ margin: 0;
125
+ }
126
+
127
+ .file-sidebar-item {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 0.75rem;
131
+ padding: 0.375rem 0.75rem;
132
+ border-radius: 0.375rem;
133
+ cursor: pointer;
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ color: rgb(75, 85, 99);
137
+ transition: all 200ms;
138
+ /* no-drag 已由 section 继承 */
139
+ }
140
+
141
+ .file-sidebar-item:hover {
142
+ background: rgba(229, 231, 233, 0.5);
143
+ color: black;
144
+ }
145
+
146
+ .file-sidebar-item--active {
147
+ background: rgba(209, 213, 219, 0.6);
148
+ color: black;
149
+ }
150
+
151
+ .file-sidebar-item-icon {
152
+ color: rgb(107, 114, 128);
153
+ }
154
+
155
+ .file-sidebar-item-icon--active {
156
+ color: rgb(37, 99, 235);
157
+ }
158
+ </style>