@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.
- package/dist/components/Breadcrumb.vue.d.ts +11 -0
- package/dist/components/Breadcrumb.vue.d.ts.map +1 -0
- package/dist/components/CompressDialog.vue.d.ts +16 -0
- package/dist/components/CompressDialog.vue.d.ts.map +1 -0
- package/dist/components/ContextMenu.vue.d.ts +18 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/FileGrid.vue.d.ts +40 -0
- package/dist/components/FileGrid.vue.d.ts.map +1 -0
- package/dist/components/FileIcon.vue.d.ts +13 -0
- package/dist/components/FileIcon.vue.d.ts.map +1 -0
- package/dist/components/FileInfoDialog.vue.d.ts +14 -0
- package/dist/components/FileInfoDialog.vue.d.ts.map +1 -0
- package/dist/components/FileList.vue.d.ts +37 -0
- package/dist/components/FileList.vue.d.ts.map +1 -0
- package/dist/components/FileListView.vue.d.ts +43 -0
- package/dist/components/FileListView.vue.d.ts.map +1 -0
- package/dist/components/FileSidebar.vue.d.ts +17 -0
- package/dist/components/FileSidebar.vue.d.ts.map +1 -0
- package/dist/components/ProgressDialog.vue.d.ts +28 -0
- package/dist/components/ProgressDialog.vue.d.ts.map +1 -0
- package/dist/components/SortIndicator.vue.d.ts +6 -0
- package/dist/components/SortIndicator.vue.d.ts.map +1 -0
- package/dist/components/StatusBar.vue.d.ts +27 -0
- package/dist/components/StatusBar.vue.d.ts.map +1 -0
- package/dist/components/Toolbar.vue.d.ts +60 -0
- package/dist/components/Toolbar.vue.d.ts.map +1 -0
- package/dist/components/Window.vue.d.ts +65 -0
- package/dist/components/Window.vue.d.ts.map +1 -0
- package/dist/composables/useApplicationIcon.d.ts +16 -0
- package/dist/composables/useApplicationIcon.d.ts.map +1 -0
- package/dist/composables/useDragAndDrop.d.ts +14 -0
- package/dist/composables/useDragAndDrop.d.ts.map +1 -0
- package/dist/composables/useMediaPlayer.d.ts +24 -0
- package/dist/composables/useMediaPlayer.d.ts.map +1 -0
- package/dist/composables/useSelection.d.ts +15 -0
- package/dist/composables/useSelection.d.ts.map +1 -0
- package/dist/composables/useWindowDrag.d.ts +18 -0
- package/dist/composables/useWindowDrag.d.ts.map +1 -0
- package/dist/composables/useWindowResize.d.ts +12 -0
- package/dist/composables/useWindowResize.d.ts.map +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4051 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +268 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/fileTypeIcon.d.ts +6 -0
- package/dist/utils/fileTypeIcon.d.ts.map +1 -0
- package/dist/utils/folderTypeIcon.d.ts +14 -0
- package/dist/utils/folderTypeIcon.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/components/Breadcrumb.vue +111 -0
- package/src/components/CompressDialog.vue +478 -0
- package/src/components/ContextMenu.vue +550 -0
- package/src/components/FileGrid.vue +504 -0
- package/src/components/FileIcon.vue +132 -0
- package/src/components/FileInfoDialog.vue +465 -0
- package/src/components/FileList.vue +421 -0
- package/src/components/FileListView.vue +321 -0
- package/src/components/FileSidebar.vue +158 -0
- package/src/components/ProgressDialog.vue +368 -0
- package/src/components/SortIndicator.vue +22 -0
- package/src/components/StatusBar.vue +43 -0
- package/src/components/Toolbar.vue +271 -0
- package/src/components/Window.vue +561 -0
- package/src/composables/useApplicationIcon.ts +79 -0
- package/src/composables/useDragAndDrop.ts +103 -0
- package/src/composables/useMediaPlayer.ts +174 -0
- package/src/composables/useSelection.ts +107 -0
- package/src/composables/useWindowDrag.ts +66 -0
- package/src/composables/useWindowResize.ts +134 -0
- package/src/index.ts +32 -0
- package/src/types/index.ts +273 -0
- package/src/utils/fileTypeIcon.ts +309 -0
- 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>
|