优化: 收藏栏宽度可调与操作菜单收敛

- 侧边栏宽度拖拽调整180-420并持久化
- 收藏操作收敛为三点菜单加右键菜单,移除悬停按钮组
- 右键与三点菜单互斥,打开其一收起另一
- 置顶项背景改清晰浅蓝
This commit is contained in:
lxy
2026-09-28 13:18:16 +08:00
parent 15dedfc563
commit 6c59f81b44
3 changed files with 183 additions and 80 deletions
@@ -1,6 +1,6 @@
<template>
<transition name="slide">
<div v-show="config.visible" class="sidebar">
<div v-show="config.visible" class="sidebar" :style="{ width: width + 'px' }">
<template v-for="section in sectionOrder" :key="section">
<!-- 服务器区块 -->
<div v-if="section === 'server' && showServer" class="sidebar-section">
@@ -54,9 +54,15 @@
</div>
<div class="section-content-wrap" :class="{ collapsed: favCollapsed }">
<div class="section-content fav-content">
<div
<a-dropdown
v-for="(fav, index) in config.favoriteFiles"
:key="fav.path"
trigger="contextMenu"
align-point
:popup-visible="favCtxMenuIndex === index"
@popup-visible-change="(v: boolean) => onFavCtxVisible(index, v)"
>
<div
class="sidebar-item"
:class="{
'sidebar-item-pinned': fav.pinnedAt,
@@ -78,41 +84,49 @@
@drop="handleDrop($event, index)"
@dragend="handleDragEnd"
>
<span class="sidebar-item-icon">{{ getFileIcon(fav) }}</span>
<span class="sidebar-item-icon">{{ getFileIcon({ is_dir: fav.isDir, name: fav.name }) }}</span>
<span class="sidebar-item-name" :title="favoriteTooltip(fav)">{{ favoriteName(fav) }}</span>
<a-button
type="text"
size="mini"
@click.stop="handleTogglePin(fav)"
class="sidebar-item-pin"
:class="{ 'is-pinned': fav.pinnedAt }"
<a-dropdown
trigger="click"
:popup-visible="favMenuIndex === index"
@popup-visible-change="(v: boolean) => onFavMenuVisible(index, v)"
>
<a-button type="text" size="mini" title="更多操作" class="sidebar-item-more" @click.stop>
<template #icon>
<icon-pushpin :style="{ opacity: fav.pinnedAt ? 1 : 0.4 }" />
<icon-more />
</template>
</a-button>
<a-button
type="text"
size="mini"
title="重命名收藏"
@click.stop="openRename(fav)"
class="sidebar-item-rename"
>
<template #icon>
<icon-edit />
<template #content>
<a-doption @click="openRename(fav)">
<template #icon><icon-edit /></template>
重命名
</a-doption>
<a-doption @click="handleTogglePin(fav)">
<template #icon><icon-pushpin /></template>
{{ fav.pinnedAt ? '取消置顶' : '置顶' }}
</a-doption>
<a-doption class="danger" @click="handleRemoveFavorite(fav)">
<template #icon><icon-close /></template>
删除
</a-doption>
</template>
</a-button>
<a-button
type="text"
size="mini"
@click.stop="handleRemoveFavorite(fav)"
class="sidebar-item-remove"
>
<template #icon>
<icon-close />
</template>
</a-button>
</a-dropdown>
</div>
<template #content>
<a-doption @click="openRename(fav)">
<template #icon><icon-edit /></template>
重命名
</a-doption>
<a-doption @click="handleTogglePin(fav)">
<template #icon><icon-pushpin /></template>
{{ fav.pinnedAt ? '取消置顶' : '置顶' }}
</a-doption>
<a-doption class="danger" @click="handleRemoveFavorite(fav)">
<template #icon><icon-close /></template>
删除
</a-doption>
</template>
</a-dropdown>
<div v-if="config.favoriteFiles.length === 0" class="sidebar-empty">
<icon-star />
<span>暂无收藏</span>
@@ -158,6 +172,7 @@
:placeholder="renameOriginal"
:max-length="255"
allow-clear
@keydown.enter.prevent="confirmRename"
/>
</a-modal>
@@ -200,7 +215,7 @@ import type { SidebarConfig, FavoriteFile } from '@/types/file-system'
import type { SystemInfo } from '@/api/connection-manager'
import { connectionManager } from '@/api/connection-manager'
import { useConfigStore } from '@/stores/config'
import { IconStar, IconClose, IconPushpin, IconEdit, IconDown, IconRight, IconStorage, IconComputer, IconDesktop, IconPlus, IconCheckCircle } from '@arco-design/web-vue/es/icon'
import { IconStar, IconClose, IconPushpin, IconEdit, IconMore, IconDown, IconRight, IconStorage, IconComputer, IconDesktop, IconPlus, IconCheckCircle } from '@arco-design/web-vue/es/icon'
import { getFileIcon } from '@/utils/fileUtils'
const configStore = useConfigStore()
@@ -208,6 +223,7 @@ const configStore = useConfigStore()
// Props
interface Props {
config: SidebarConfig
width: number
}
const props = defineProps<Props>()
@@ -399,6 +415,20 @@ const handleTogglePin = (file: FavoriteFile) => {
emit('togglePin', file.path)
}
// 收藏项双菜单受控互斥:右键菜单与「...」点击菜单,一方展开强制收起另一方
const favMenuIndex = ref(-1)
const favCtxMenuIndex = ref(-1)
function onFavCtxVisible(index: number, visible: boolean) {
favCtxMenuIndex.value = visible ? index : -1
if (visible) favMenuIndex.value = -1
}
function onFavMenuVisible(index: number, visible: boolean) {
favMenuIndex.value = visible ? index : -1
if (visible) favCtxMenuIndex.value = -1
}
// 重命名收藏弹窗(预填展示名,placeholder 显示原名)
const renameVisible = ref(false)
const renameValue = ref('')
@@ -429,6 +459,11 @@ const handleRenameOk = (done: (closed?: boolean) => void) => {
done()
}
// 输入框回车与点击确定走同一提交逻辑
const confirmRename = () => {
handleRenameOk(() => { renameVisible.value = false })
}
// 展示名优先,未设置回退原名
const favoriteName = (fav: FavoriteFile): string => fav.displayName || fav.name
@@ -891,17 +926,8 @@ function handleDelete(p: { id: string; name: string }) {
text-overflow: ellipsis;
}
.sidebar-item-remove {
opacity: 0;
transition: opacity 0.2s;
}
.sidebar-item:hover .sidebar-item-remove {
opacity: 1;
}
.sidebar-item-pinned {
background: var(--color-fill-1);
background: var(--arcoblue-1);
border-radius: 0;
}
@@ -917,21 +943,13 @@ function handleDelete(p: { id: string; name: string }) {
border-radius: 6px;
}
.sidebar-item-pin {
/* 「...」操作菜单按钮,悬停收藏项时显示 */
.sidebar-item-more {
opacity: 0;
transition: opacity 0.2s;
}
.sidebar-item:hover .sidebar-item-pin {
opacity: 1;
}
.sidebar-item-rename {
opacity: 0;
transition: opacity 0.2s;
}
.sidebar-item:hover .sidebar-item-rename {
.sidebar-item:hover .sidebar-item-more {
opacity: 1;
}
@@ -979,6 +997,17 @@ function handleDelete(p: { id: string; name: string }) {
}
</style>
<!-- 非 scoped:Arco 下拉菜单挂载在 body 下,删除项标红需全局样式 -->
<style>
.arco-dropdown-option.danger {
color: rgb(var(--danger-6));
}
.arco-dropdown-option.danger:hover {
background: rgb(var(--danger-1));
}
</style>
<!-- 非 scoped:Teleport 到 body 的面板样式 -->
<style>
/* 弹出面板公共 */
+70 -1
View File
@@ -1,11 +1,12 @@
<template>
<div class="file-system-container">
<!-- 主内容区:左侧边栏 + 右侧工作区 -->
<div class="main-content">
<div ref="mainContentRef" class="main-content">
<!-- 左侧收藏夹侧边栏(全高) -->
<Sidebar
v-if="showSidebar"
:config="sidebarConfig"
:width="sidebarWidth"
@open-favorite="handleOpenFavorite"
@remove-favorite="handleRemoveFavorite"
@toggle-pin="handleToggleFavPin"
@@ -20,6 +21,9 @@
@server-select="handleConnectionChanged"
/>
<!-- 侧边栏拖宽把手 -->
<div v-if="showSidebar" class="sidebar-resizer" @mousedown="handleSidebarResizeWithOverlay"></div>
<!-- 右侧工作区:面包屑工具栏 + 文件列表/编辑器 -->
<div class="workspace-area">
<!-- 面包屑导航工具栏 -->
@@ -272,6 +276,58 @@ const savePanelWidth = (width: { left: number; right: number }) => {
const panelWidth = ref(restorePanelWidth())
const workspaceRef = ref<HTMLElement | null>(null)
// 侧边栏宽度(拖拽调整,localStorage 持久化)
const restoreSidebarWidth = (): number => {
try {
const saved = localStorage.getItem(STORAGE_KEYS.FILESYSTEM.SIDEBAR_WIDTH)
if (saved) {
const width = Number(saved)
if (Number.isFinite(width)) {
return Math.min(DEFAULTS.MAX_SIDEBAR_WIDTH, Math.max(DEFAULTS.MIN_SIDEBAR_WIDTH, width))
}
}
} catch (error) {
console.error('恢复侧边栏宽度失败:', error)
}
return DEFAULTS.SIDEBAR_WIDTH
}
const saveSidebarWidth = (width: number) => {
try {
localStorage.setItem(STORAGE_KEYS.FILESYSTEM.SIDEBAR_WIDTH, String(width))
} catch (error) {
console.error('保存侧边栏宽度失败:', error)
}
}
const sidebarWidth = ref(restoreSidebarWidth())
const mainContentRef = ref<HTMLElement | null>(null)
const handleSidebarHorizontalResize = createResizeHandler(
() => mainContentRef.value,
() => sidebarWidth.value,
{
direction: 'horizontal',
outputMode: 'pixels',
minPercent: 0,
maxPercent: 100,
minPixels: DEFAULTS.MIN_SIDEBAR_WIDTH,
onResize: (px) => {
sidebarWidth.value = Math.min(DEFAULTS.MAX_SIDEBAR_WIDTH, Math.max(DEFAULTS.MIN_SIDEBAR_WIDTH, px))
},
onResizeEnd: () => {
isResizing.value = false
saveSidebarWidth(sidebarWidth.value)
},
}
)
// mousedown 时复用工作区遮罩,防止拖拽经过编辑器 iframe 时丢事件
const handleSidebarResizeWithOverlay = (e: MouseEvent) => {
isResizing.value = true
handleSidebarHorizontalResize(e)
}
// 系统路径(使用 composable)
const { commonPaths, systemPaths, loadCommonPaths } = useCommonPaths()
@@ -2094,6 +2150,19 @@ watch(() => themeStore.isDark, async () => {
background: var(--color-primary-light-1);
}
/* 侧边栏拖宽把手(与文件列表区分隔条同款) */
.sidebar-resizer {
width: 3px;
background: var(--color-border);
cursor: col-resize;
transition: background 0.2s;
flex-shrink: 0;
}
.sidebar-resizer:hover {
background: var(--color-primary-light-1);
}
.resize-overlay {
position: absolute;
inset: 0;
+6 -1
View File
@@ -23,6 +23,7 @@ export const STORAGE_KEYS = {
PATH_HISTORY: 'app-filesystem-path-history',
FILE_CONTENT_HEIGHT: 'app-filesystem-file-content-height',
PANEL_WIDTH: 'app-filesystem-panel-width',
SIDEBAR_WIDTH: 'app-filesystem-sidebar-width',
SIDEBAR_VISIBLE: 'app-filesystem-sidebar-visible',
FAVORITE_FILES: 'app-filesystem-favorite-files',
EDIT_MODE: 'app-filesystem-edit-mode', // HTML/Markdown 编辑模式状态
@@ -334,8 +335,12 @@ export const DEFAULTS = {
MAX_PANEL_WIDTH: 80,
DEFAULT_PANEL_WIDTH: 50,
// 侧边栏宽度(px)
// 侧边栏默认宽度(px)
SIDEBAR_WIDTH: 220,
// 侧边栏宽度范围(px)
MIN_SIDEBAR_WIDTH: 180,
MAX_SIDEBAR_WIDTH: 420,
}
/**