新增:收藏夹折叠+帮助文档区块+拖拽排序修复
- Sidebar 双区块架构:收藏夹(可折叠) + 帮助文档(默认折叠) - 帮助内容:5条常用快捷键静态展示 - 折叠动画:max-height + opacity 过渡,自适应视口高度 - 修复拖拽死锁:draggable 条件改为 pressedIndex || isDragging - 修复长按误触:200ms 时延防单击触发 draggable - 修复排序持久化:sortFavorites 仅分组保序,不再覆盖拖拽顺序 - 清理死代码:.sidebar-divider、dataTransfer.setData
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user