优化: 收藏栏宽度可调与操作菜单收敛
- 侧边栏宽度拖拽调整180-420并持久化 - 收藏操作收敛为三点菜单加右键菜单,移除悬停按钮组 - 右键与三点菜单互斥,打开其一收起另一 - 置顶项背景改清晰浅蓝
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="slide">
|
<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">
|
<template v-for="section in sectionOrder" :key="section">
|
||||||
<!-- 服务器区块 -->
|
<!-- 服务器区块 -->
|
||||||
<div v-if="section === 'server' && showServer" class="sidebar-section">
|
<div v-if="section === 'server' && showServer" class="sidebar-section">
|
||||||
@@ -54,65 +54,79 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section-content-wrap" :class="{ collapsed: favCollapsed }">
|
<div class="section-content-wrap" :class="{ collapsed: favCollapsed }">
|
||||||
<div class="section-content fav-content">
|
<div class="section-content fav-content">
|
||||||
<div
|
<a-dropdown
|
||||||
v-for="(fav, index) in config.favoriteFiles"
|
v-for="(fav, index) in config.favoriteFiles"
|
||||||
:key="fav.path"
|
:key="fav.path"
|
||||||
class="sidebar-item"
|
trigger="contextMenu"
|
||||||
:class="{
|
align-point
|
||||||
'sidebar-item-pinned': fav.pinnedAt,
|
:popup-visible="favCtxMenuIndex === index"
|
||||||
'sidebar-item-pinned-first': index === firstPinnedIndex,
|
@popup-visible-change="(v: boolean) => onFavCtxVisible(index, v)"
|
||||||
'sidebar-item-pinned-last': index === lastPinnedIndex,
|
|
||||||
'sidebar-item-dragging': config.draggingState.isDragging && config.draggingState.draggedIndex === index,
|
|
||||||
'sidebar-item-drag-over': config.draggingState.isDragging && config.draggingState.draggedIndex !== index
|
|
||||||
}"
|
|
||||||
:draggable="config.draggingState.pressedIndex === index || config.draggingState.isDragging"
|
|
||||||
@click="handleOpenFavorite(fav)"
|
|
||||||
@mousedown="handleLongPressStart($event, index)"
|
|
||||||
@mouseup="handleLongPressCancel"
|
|
||||||
@mouseleave="handleLongPressCancel"
|
|
||||||
@touchstart="handleLongPressStart($event, index)"
|
|
||||||
@touchend="handleLongPressCancel"
|
|
||||||
@touchcancel="handleLongPressCancel"
|
|
||||||
@dragstart="handleDragStart($event, index)"
|
|
||||||
@dragover="handleDragOver($event)"
|
|
||||||
@drop="handleDrop($event, index)"
|
|
||||||
@dragend="handleDragEnd"
|
|
||||||
>
|
>
|
||||||
<span class="sidebar-item-icon">{{ getFileIcon(fav) }}</span>
|
<div
|
||||||
<span class="sidebar-item-name" :title="favoriteTooltip(fav)">{{ favoriteName(fav) }}</span>
|
class="sidebar-item"
|
||||||
<a-button
|
:class="{
|
||||||
type="text"
|
'sidebar-item-pinned': fav.pinnedAt,
|
||||||
size="mini"
|
'sidebar-item-pinned-first': index === firstPinnedIndex,
|
||||||
@click.stop="handleTogglePin(fav)"
|
'sidebar-item-pinned-last': index === lastPinnedIndex,
|
||||||
class="sidebar-item-pin"
|
'sidebar-item-dragging': config.draggingState.isDragging && config.draggingState.draggedIndex === index,
|
||||||
:class="{ 'is-pinned': fav.pinnedAt }"
|
'sidebar-item-drag-over': config.draggingState.isDragging && config.draggingState.draggedIndex !== index
|
||||||
|
}"
|
||||||
|
:draggable="config.draggingState.pressedIndex === index || config.draggingState.isDragging"
|
||||||
|
@click="handleOpenFavorite(fav)"
|
||||||
|
@mousedown="handleLongPressStart($event, index)"
|
||||||
|
@mouseup="handleLongPressCancel"
|
||||||
|
@mouseleave="handleLongPressCancel"
|
||||||
|
@touchstart="handleLongPressStart($event, index)"
|
||||||
|
@touchend="handleLongPressCancel"
|
||||||
|
@touchcancel="handleLongPressCancel"
|
||||||
|
@dragstart="handleDragStart($event, index)"
|
||||||
|
@dragover="handleDragOver($event)"
|
||||||
|
@drop="handleDrop($event, index)"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<span class="sidebar-item-icon">{{ getFileIcon({ is_dir: fav.isDir, name: fav.name }) }}</span>
|
||||||
<icon-pushpin :style="{ opacity: fav.pinnedAt ? 1 : 0.4 }" />
|
<span class="sidebar-item-name" :title="favoriteTooltip(fav)">{{ favoriteName(fav) }}</span>
|
||||||
</template>
|
<a-dropdown
|
||||||
</a-button>
|
trigger="click"
|
||||||
<a-button
|
:popup-visible="favMenuIndex === index"
|
||||||
type="text"
|
@popup-visible-change="(v: boolean) => onFavMenuVisible(index, v)"
|
||||||
size="mini"
|
>
|
||||||
title="重命名收藏"
|
<a-button type="text" size="mini" title="更多操作" class="sidebar-item-more" @click.stop>
|
||||||
@click.stop="openRename(fav)"
|
<template #icon>
|
||||||
class="sidebar-item-rename"
|
<icon-more />
|
||||||
>
|
</template>
|
||||||
<template #icon>
|
</a-button>
|
||||||
<icon-edit />
|
<template #content>
|
||||||
</template>
|
<a-doption @click="openRename(fav)">
|
||||||
</a-button>
|
<template #icon><icon-edit /></template>
|
||||||
<a-button
|
重命名
|
||||||
type="text"
|
</a-doption>
|
||||||
size="mini"
|
<a-doption @click="handleTogglePin(fav)">
|
||||||
@click.stop="handleRemoveFavorite(fav)"
|
<template #icon><icon-pushpin /></template>
|
||||||
class="sidebar-item-remove"
|
{{ fav.pinnedAt ? '取消置顶' : '置顶' }}
|
||||||
>
|
</a-doption>
|
||||||
<template #icon>
|
<a-doption class="danger" @click="handleRemoveFavorite(fav)">
|
||||||
<icon-close />
|
<template #icon><icon-close /></template>
|
||||||
</template>
|
删除
|
||||||
</a-button>
|
</a-doption>
|
||||||
</div>
|
</template>
|
||||||
|
</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">
|
<div v-if="config.favoriteFiles.length === 0" class="sidebar-empty">
|
||||||
<icon-star />
|
<icon-star />
|
||||||
<span>暂无收藏</span>
|
<span>暂无收藏</span>
|
||||||
@@ -158,6 +172,7 @@
|
|||||||
:placeholder="renameOriginal"
|
:placeholder="renameOriginal"
|
||||||
:max-length="255"
|
:max-length="255"
|
||||||
allow-clear
|
allow-clear
|
||||||
|
@keydown.enter.prevent="confirmRename"
|
||||||
/>
|
/>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
@@ -200,7 +215,7 @@ import type { SidebarConfig, FavoriteFile } from '@/types/file-system'
|
|||||||
import type { SystemInfo } from '@/api/connection-manager'
|
import type { SystemInfo } from '@/api/connection-manager'
|
||||||
import { connectionManager } from '@/api/connection-manager'
|
import { connectionManager } from '@/api/connection-manager'
|
||||||
import { useConfigStore } from '@/stores/config'
|
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'
|
import { getFileIcon } from '@/utils/fileUtils'
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
@@ -208,6 +223,7 @@ const configStore = useConfigStore()
|
|||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
config: SidebarConfig
|
config: SidebarConfig
|
||||||
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -399,6 +415,20 @@ const handleTogglePin = (file: FavoriteFile) => {
|
|||||||
emit('togglePin', file.path)
|
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 显示原名)
|
// 重命名收藏弹窗(预填展示名,placeholder 显示原名)
|
||||||
const renameVisible = ref(false)
|
const renameVisible = ref(false)
|
||||||
const renameValue = ref('')
|
const renameValue = ref('')
|
||||||
@@ -429,6 +459,11 @@ const handleRenameOk = (done: (closed?: boolean) => void) => {
|
|||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 输入框回车与点击确定走同一提交逻辑
|
||||||
|
const confirmRename = () => {
|
||||||
|
handleRenameOk(() => { renameVisible.value = false })
|
||||||
|
}
|
||||||
|
|
||||||
// 展示名优先,未设置回退原名
|
// 展示名优先,未设置回退原名
|
||||||
const favoriteName = (fav: FavoriteFile): string => fav.displayName || fav.name
|
const favoriteName = (fav: FavoriteFile): string => fav.displayName || fav.name
|
||||||
|
|
||||||
@@ -891,17 +926,8 @@ function handleDelete(p: { id: string; name: string }) {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item-remove {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item:hover .sidebar-item-remove {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-pinned {
|
.sidebar-item-pinned {
|
||||||
background: var(--color-fill-1);
|
background: var(--arcoblue-1);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,21 +943,13 @@ function handleDelete(p: { id: string; name: string }) {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item-pin {
|
/* 「...」操作菜单按钮,悬停收藏项时显示 */
|
||||||
|
.sidebar-item-more {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item:hover .sidebar-item-pin {
|
.sidebar-item:hover .sidebar-item-more {
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-rename {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item:hover .sidebar-item-rename {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -979,6 +997,17 @@ function handleDelete(p: { id: string; name: string }) {
|
|||||||
}
|
}
|
||||||
</style>
|
</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 的面板样式 -->
|
<!-- 非 scoped:Teleport 到 body 的面板样式 -->
|
||||||
<style>
|
<style>
|
||||||
/* 弹出面板公共 */
|
/* 弹出面板公共 */
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="file-system-container">
|
<div class="file-system-container">
|
||||||
<!-- 主内容区:左侧边栏 + 右侧工作区 -->
|
<!-- 主内容区:左侧边栏 + 右侧工作区 -->
|
||||||
<div class="main-content">
|
<div ref="mainContentRef" class="main-content">
|
||||||
<!-- 左侧收藏夹侧边栏(全高) -->
|
<!-- 左侧收藏夹侧边栏(全高) -->
|
||||||
<Sidebar
|
<Sidebar
|
||||||
v-if="showSidebar"
|
v-if="showSidebar"
|
||||||
:config="sidebarConfig"
|
:config="sidebarConfig"
|
||||||
|
:width="sidebarWidth"
|
||||||
@open-favorite="handleOpenFavorite"
|
@open-favorite="handleOpenFavorite"
|
||||||
@remove-favorite="handleRemoveFavorite"
|
@remove-favorite="handleRemoveFavorite"
|
||||||
@toggle-pin="handleToggleFavPin"
|
@toggle-pin="handleToggleFavPin"
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
@server-select="handleConnectionChanged"
|
@server-select="handleConnectionChanged"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 侧边栏拖宽把手 -->
|
||||||
|
<div v-if="showSidebar" class="sidebar-resizer" @mousedown="handleSidebarResizeWithOverlay"></div>
|
||||||
|
|
||||||
<!-- 右侧工作区:面包屑工具栏 + 文件列表/编辑器 -->
|
<!-- 右侧工作区:面包屑工具栏 + 文件列表/编辑器 -->
|
||||||
<div class="workspace-area">
|
<div class="workspace-area">
|
||||||
<!-- 面包屑导航工具栏 -->
|
<!-- 面包屑导航工具栏 -->
|
||||||
@@ -272,6 +276,58 @@ const savePanelWidth = (width: { left: number; right: number }) => {
|
|||||||
const panelWidth = ref(restorePanelWidth())
|
const panelWidth = ref(restorePanelWidth())
|
||||||
const workspaceRef = ref<HTMLElement | null>(null)
|
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)
|
// 系统路径(使用 composable)
|
||||||
const { commonPaths, systemPaths, loadCommonPaths } = useCommonPaths()
|
const { commonPaths, systemPaths, loadCommonPaths } = useCommonPaths()
|
||||||
|
|
||||||
@@ -2094,6 +2150,19 @@ watch(() => themeStore.isDark, async () => {
|
|||||||
background: var(--color-primary-light-1);
|
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 {
|
.resize-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const STORAGE_KEYS = {
|
|||||||
PATH_HISTORY: 'app-filesystem-path-history',
|
PATH_HISTORY: 'app-filesystem-path-history',
|
||||||
FILE_CONTENT_HEIGHT: 'app-filesystem-file-content-height',
|
FILE_CONTENT_HEIGHT: 'app-filesystem-file-content-height',
|
||||||
PANEL_WIDTH: 'app-filesystem-panel-width',
|
PANEL_WIDTH: 'app-filesystem-panel-width',
|
||||||
|
SIDEBAR_WIDTH: 'app-filesystem-sidebar-width',
|
||||||
SIDEBAR_VISIBLE: 'app-filesystem-sidebar-visible',
|
SIDEBAR_VISIBLE: 'app-filesystem-sidebar-visible',
|
||||||
FAVORITE_FILES: 'app-filesystem-favorite-files',
|
FAVORITE_FILES: 'app-filesystem-favorite-files',
|
||||||
EDIT_MODE: 'app-filesystem-edit-mode', // HTML/Markdown 编辑模式状态
|
EDIT_MODE: 'app-filesystem-edit-mode', // HTML/Markdown 编辑模式状态
|
||||||
@@ -334,8 +335,12 @@ export const DEFAULTS = {
|
|||||||
MAX_PANEL_WIDTH: 80,
|
MAX_PANEL_WIDTH: 80,
|
||||||
DEFAULT_PANEL_WIDTH: 50,
|
DEFAULT_PANEL_WIDTH: 50,
|
||||||
|
|
||||||
// 侧边栏宽度(px)
|
// 侧边栏默认宽度(px)
|
||||||
SIDEBAR_WIDTH: 220,
|
SIDEBAR_WIDTH: 220,
|
||||||
|
|
||||||
|
// 侧边栏宽度范围(px)
|
||||||
|
MIN_SIDEBAR_WIDTH: 180,
|
||||||
|
MAX_SIDEBAR_WIDTH: 420,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user