Private
Public Access
1
0

新增:收藏夹折叠+帮助文档区块+拖拽排序修复

- Sidebar 双区块架构:收藏夹(可折叠) + 帮助文档(默认折叠)
- 帮助内容:5条常用快捷键静态展示
- 折叠动画:max-height + opacity 过渡,自适应视口高度
- 修复拖拽死锁:draggable 条件改为 pressedIndex || isDragging
- 修复长按误触:200ms 时延防单击触发 draggable
- 修复排序持久化:sortFavorites 仅分组保序,不再覆盖拖拽顺序
- 清理死代码:.sidebar-divider、dataTransfer.setData
This commit is contained in:
2026-04-30 23:01:47 +08:00
parent 3d5a1e5892
commit 44847e0d40
2 changed files with 186 additions and 85 deletions

View File

@@ -1,11 +1,15 @@
<template>
<transition name="slide">
<div v-show="config.visible" class="sidebar">
<div class="sidebar-header">
<span class="sidebar-title"> 收藏夹</span>
<span class="sidebar-count">{{ config.favoriteFiles.length }}</span>
<!-- 收藏夹区块 -->
<div class="sidebar-section">
<div class="section-header" @click="favCollapsed = !favCollapsed">
<span class="section-title"> 收藏夹</span>
<span class="section-count">{{ config.favoriteFiles.length }}</span>
<icon-down v-if="!favCollapsed" class="section-toggle" />
<icon-right v-else class="section-toggle" />
</div>
<div class="sidebar-content">
<div class="section-content" :class="{ collapsed: favCollapsed }">
<div
v-for="(fav, index) in config.favoriteFiles"
:key="fav.path"
@@ -17,7 +21,7 @@
'sidebar-item-dragging': config.draggingState.isDragging && config.draggingState.draggedIndex === index,
'sidebar-item-drag-over': config.draggingState.isDragging && config.draggingState.draggedIndex !== index
}"
:draggable="config.draggingState.isDragging && config.draggingState.draggedIndex === index"
:draggable="config.draggingState.pressedIndex === index || config.draggingState.isDragging"
@click="handleOpenFavorite(fav)"
@mousedown="handleLongPressStart($event, index)"
@mouseup="handleLongPressCancel"
@@ -61,11 +65,27 @@
</div>
</div>
</div>
<!-- 帮助文档区块 -->
<div class="sidebar-section">
<div class="section-header" @click="helpCollapsed = !helpCollapsed">
<span class="section-title">📖 帮助</span>
<icon-down v-if="!helpCollapsed" class="section-toggle" />
<icon-right v-else class="section-toggle" />
</div>
<div class="section-content help-content" :class="{ collapsed: helpCollapsed }">
<div class="help-item" v-for="item in helpItems" :key="item.key">
<span class="help-key">{{ item.key }}</span>
<span class="help-desc">{{ item.desc }}</span>
</div>
</div>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed } from 'vue'
import type { SidebarConfig, FavoriteFile } from '@/types/file-system'
// Props
@@ -75,6 +95,10 @@ interface Props {
const props = defineProps<Props>()
// 折叠状态(组件内部,不污染父组件)
const favCollapsed = ref(false)
const helpCollapsed = ref(true)
// 计算第一个和最后一个置顶项的索引
const pinnedIndices = computed(() => {
return props.config.favoriteFiles
@@ -85,6 +109,15 @@ const pinnedIndices = computed(() => {
const firstPinnedIndex = computed(() => pinnedIndices.value[0] ?? -1)
const lastPinnedIndex = computed(() => pinnedIndices.value[pinnedIndices.value.length - 1] ?? -1)
// 帮助内容
const helpItems = [
{ key: 'Ctrl+B', desc: '切换侧边栏' },
{ key: 'Ctrl+H', desc: '历史记录' },
{ key: 'Ctrl+F', desc: '聚焦搜索' },
{ key: 'Click ⭐', desc: '收藏文件' },
{ key: 'Drag', desc: '排序收藏' },
]
// Emits
interface Emits {
(e: 'openFavorite', file: FavoriteFile): void
@@ -101,7 +134,7 @@ interface Emits {
const emit = defineEmits<Emits>()
// 图标导入
import { IconStar, IconClose, IconPushpin } from '@arco-design/web-vue/es/icon'
import { IconStar, IconClose, IconPushpin, IconDown, IconRight } from '@arco-design/web-vue/es/icon'
import { getFileIcon } from '@/utils/fileUtils'
// 事件处理
@@ -151,34 +184,100 @@ const handleDragEnd = () => {
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
}
.sidebar-header {
padding: 6px 12px;
border-bottom: 1px solid var(--color-border);
/* 区块 */
.sidebar-section {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-bg-2);
flex-direction: column;
}
.sidebar-title {
/* 区块头部 - 可点击折叠 */
.section-header {
padding: 5px 12px;
background: var(--color-bg-2);
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
flex-shrink: 0;
}
.section-header:hover {
background: var(--color-fill-1);
}
.section-title {
font-size: 13px;
font-weight: 500;
color: var(--color-text-1);
}
.sidebar-count {
.section-count {
font-size: 12px;
color: var(--color-text-3);
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 8px;
.section-toggle {
font-size: 12px;
color: var(--color-text-3);
margin-left: auto;
transition: transform 0.2s;
}
/* 区块内容 - 可折叠 */
.section-content {
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.2s ease;
max-height: calc(100vh - 80px);
opacity: 1;
}
.section-content.collapsed {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
/* 收藏夹内容 */
.section-content:not(.help-content) {
flex: 1;
min-height: 0;
padding: 4px 8px 0;
}
/* 帮助内容 */
.help-content {
padding: 4px 12px;
}
.help-item {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 0;
font-size: 12px;
}
.help-key {
font-family: 'Consolas', 'Monaco', monospace;
background: var(--color-fill-2);
padding: 1px 6px;
border-radius: 3px;
color: var(--color-text-2);
white-space: nowrap;
min-width: 56px;
text-align: center;
}
.help-desc {
color: var(--color-text-3);
}
/* 收藏项 */
.sidebar-item {
display: flex;
align-items: center;
@@ -254,6 +353,7 @@ const handleDragEnd = () => {
opacity: 1;
}
/* 空状态 */
.sidebar-empty {
display: flex;
flex-direction: column;
@@ -280,7 +380,7 @@ const handleDragEnd = () => {
margin-top: 4px;
}
/* 滑动动画 */
/* 侧边栏整体滑入滑出动画 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;

View File

@@ -21,18 +21,12 @@ export function useFavorites() {
})
/**
* 排序收藏列表:置顶项在前(按 pinnedAt 降序),非置顶项按添加时间降序
* 排序收藏列表:置顶项归到前面,组内保持原有顺序(尊重拖拽)
*/
const sortFavorites = () => {
favorites.value = [...favorites.value].sort((a, b) => {
// 置顶项优先
if (a.pinnedAt && !b.pinnedAt) return -1
if (!a.pinnedAt && b.pinnedAt) return 1
// 都是置顶项,按置顶时间降序
if (a.pinnedAt && b.pinnedAt) return b.pinnedAt - a.pinnedAt
// 都不是置顶项,按添加时间降序(最新在前)
return b.addedAt - a.addedAt
})
const pinned = favorites.value.filter(f => f.pinnedAt)
const unpinned = favorites.value.filter(f => !f.pinnedAt)
favorites.value = [...pinned, ...unpinned]
}
/**
@@ -50,7 +44,7 @@ export function useFavorites() {
isDir: fav.isDir ?? (fav as any).is_dir ?? false
}))
//
// 仅排序(置顶项归组到前面),保持用户拖拽顺
sortFavorites()
}
} catch (error) {
@@ -171,15 +165,23 @@ export function useFavorites() {
}
// 拖拽方法
const longPressTimer = ref<ReturnType<typeof setTimeout> | null>(null)
const onLongPressStart = (event: MouseEvent | TouchEvent, index: number) => {
if (event instanceof MouseEvent && event.button !== 0) return
if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent)) return
longPressTimer = setTimeout(() => {
draggingState.value.pressedIndex = index
draggingState.value.draggedIndex = index
}, 200)
}
const onLongPressCancel = () => {
if (longPressTimer) {
clearTimeout(longPressTimer)
longPressTimer = null
}
if (!draggingState.value.isDragging) {
draggingState.value.pressedIndex = -1
draggingState.value.draggedIndex = -1
@@ -191,7 +193,6 @@ export function useFavorites() {
draggingState.value.draggedIndex = index
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', index.toString())
}
}