feat(workspace): 重构工作区组件,优化路径选择和面包屑导航功能

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Wenjie Zhang 2026-04-30 22:40:55 +08:00
parent 9ff925e4df
commit 6fcfb090de
4 changed files with 77 additions and 54 deletions

View File

@ -2,22 +2,34 @@
<section class="workspace-file-list">
<div class="file-list-header">
<div class="path-line">
<button
type="button"
class="path-action"
:disabled="currentPath === '/'"
aria-label="返回上级目录"
@click="$emit('go-parent')"
>
<ChevronLeft :size="16" />
</button>
<span class="path-text" :title="currentPath">{{ currentPath }}</span>
<a-breadcrumb class="path-breadcrumb">
<a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.path">
<button
type="button"
class="breadcrumb-action"
:class="{ current: item.path === currentPath }"
:disabled="item.path === currentPath"
:title="item.path"
@click="$emit('select-path', item.path)"
>
{{ item.name }}
</button>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<div class="list-actions">
<span class="entry-count">{{ entries.length }} </span>
<a-button size="small" :type="selectionMode ? 'primary' : 'default'" @click="toggleSelectionMode">
多选
</a-button>
<a-tooltip title="多选">
<a-button
size="small"
class="lucide-icon-btn"
:type="selectionMode ? 'primary' : 'default'"
aria-label="多选"
@click="toggleSelectionMode"
>
<ListChecks :size="14" />
</a-button>
</a-tooltip>
<a-button
v-if="selectionMode"
size="small"
@ -122,7 +134,7 @@
<script setup>
import { computed } from 'vue'
import { ChevronLeft, Download, Folder, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import { Download, Folder, ListChecks, MoreHorizontal, Trash2 } from 'lucide-vue-next'
import { formatFileSize, formatRelativeTime, getFileIcon, getFileIconColor } from '@/utils/file_utils'
const props = defineProps({
@ -137,7 +149,7 @@ const props = defineProps({
const emit = defineEmits([
'select-entry',
'go-parent',
'select-path',
'update:selectedPaths',
'update:selectionMode',
'delete-selected',
@ -148,6 +160,23 @@ const emit = defineEmits([
const selectedPathSet = computed(() => new Set(props.selectedPaths))
const deletingPathSet = computed(() => new Set(props.deletingPaths))
const entryPaths = computed(() => props.entries.map((entry) => entry.path))
const breadcrumbItems = computed(() => {
const normalizedPath = props.currentPath || '/'
if (normalizedPath === '/') {
return [{ name: '工作区', path: '/' }]
}
const segments = normalizedPath.split('/').filter(Boolean)
return segments.reduce(
(items, segment) => {
const parentPath = items[items.length - 1].path
const path = parentPath === '/' ? `/${segment}` : `${parentPath}/${segment}`
items.push({ name: segment, path })
return items
},
[{ name: '工作区', path: '/' }]
)
})
const allSelected = computed(() => {
return entryPaths.value.length > 0 && entryPaths.value.every((path) => selectedPathSet.value.has(path))
@ -213,37 +242,34 @@ const toggleEntrySelection = (path, checked) => {
flex: 0 0 auto;
}
.path-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: 1px solid var(--gray-150);
border-radius: 8px;
background: var(--gray-0);
color: var(--gray-600);
cursor: pointer;
&:disabled {
color: var(--gray-300);
cursor: not-allowed;
}
&:hover:not(:disabled) {
background: var(--main-20);
color: var(--main-color);
}
}
.path-text {
.path-breadcrumb {
min-width: 0;
overflow: hidden;
color: var(--gray-900);
font-size: 14px;
font-weight: 600;
}
.breadcrumb-action {
max-width: 180px;
padding: 0;
overflow: hidden;
border: 0;
background: transparent;
color: var(--main-800);
cursor: pointer;
font: inherit;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
&:hover:not(:disabled) {
color: var(--main-600);
}
&.current,
&:disabled {
color: var(--gray-900);
cursor: default;
}
}
.entry-count {

View File

@ -5,10 +5,12 @@
:file="file"
:file-path="filePath"
:show-download="false"
:show-close="true"
:show-fullscreen="true"
:full-height="true"
container-class="workspace-preview-container"
content-class="workspace-preview-content"
@close="$emit('close')"
/>
<div v-else-if="loading" class="preview-state">
<a-spin />
@ -31,6 +33,8 @@ defineProps({
filePath: { type: String, default: '' },
loading: { type: Boolean, default: false }
})
defineEmits(['close'])
</script>
<style scoped lang="less">

View File

@ -104,8 +104,9 @@ defineEmits(['select-personal', 'select-database', 'select-path'])
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: var(--gray-700);
color: var(--gray-600);
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition:
@ -118,13 +119,13 @@ defineEmits(['select-personal', 'select-database', 'select-path'])
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
&:hover:not(:disabled),
&.active {
border-color: transparent;
background: var(--main-20);
color: var(--main-color);
color: var(--main-600);
}
&.secondary {

View File

@ -59,7 +59,7 @@
:selection-mode="selectionMode"
:loading="loadingTree"
@select-entry="handleSelectEntry"
@go-parent="goParentDirectory"
@select-path="selectWorkspacePath"
@update:selected-paths="selectedPaths = $event"
@update:selection-mode="handleSelectionModeChange"
@delete-selected="confirmDeleteEntries(selectedEntries)"
@ -79,6 +79,7 @@
:file="previewFile"
:file-path="selectedEntry?.path || ''"
:loading="loadingPreview"
@close="closePreview"
/>
</template>
@ -338,15 +339,6 @@ const closePreview = () => {
revokePreviewObjectUrl()
}
const goParentDirectory = async () => {
if (currentPath.value === '/') return
const trimmed = currentPath.value.replace(/\/$/, '')
const parent = trimmed.slice(0, trimmed.lastIndexOf('/')) || '/'
closePreview()
clearWorkspaceSelection()
await loadWorkspaceEntries(parent)
}
const openCreateDirectoryModal = () => {
if (activeSourceKey.value !== 'personal') return
newDirectoryName.value = ''