feat: 添加鼠标侧键导航功能,支持文件夹浏览历史前进/后退
## 新增功能 ### 历史记录管理 (FilePane.vue) - 添加 history 和 historyIndex 状态管理浏览历史 - 实现 addToHistory() 方法,智能去重避免重复记录 - 实现 goBack() 和 goForward() 方法用于历史导航 - 所有导航方法(navigateToFolder、navigateToIndex、navigateToBreadcrumbFolder、applyPathEdit)自动记录历史 ### 鼠标侧键支持 (FieldView.vue) - 全局监听鼠标侧键事件(button 3=后退,button 4=前进) - 完全阻止浏览器默认的前进/后退行为 - 自动识别鼠标所在面板并激活 - 支持单面板、左右分栏、上下分栏模式 ### 面板联动 (FileManager.vue) - 添加 leftPaneRef 和 rightPaneRef 引用 - 实现 goBack() 和 goForward() 方法转发到激活的面板 - 通过 defineExpose 暴露方法给父组件 ## 技术细节 ### 事件处理策略 - mousedown: 阻止默认行为,不执行导航 - mouseup: 阻止默认行为 + 执行导航(确保只执行一次) - 使用事件捕获阶段(capture: true)确保最早拦截 ### 历史记录逻辑 - 路径变化时自动添加到历史 - 通过 JSON.stringify 比对避免重复路径 - 在历史中间导航后,新导航会清除后续历史 - goBack/goForward 不添加新历史,只改变 historyIndex ### 面板激活机制 - 检测鼠标点击位置的 .file-pane 元素 - 通过 click() 触发面板激活 - 使用 setTimeout 确保激活完成后再执行导航 ## 用户体验改进 - 鼠标在哪个面板,侧键就操作哪个面板 - 每个面板独立维护自己的浏览历史 - 导航操作流畅,无多次触发问题 - 不影响浏览器在其他页面的前进/后退功能 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
bf03ca9535
commit
86aa8affc3
3 files changed
+146
-2
No files matched your search
@@ -72,6 +72,7 @@
|
||||
<!-- Main Content -->
|
||||
<div :class="['main-content', `layout-${layoutMode}`]">
|
||||
<FilePane
|
||||
ref="leftPaneRef"
|
||||
pane-id="left"
|
||||
:view-mode="paneConfigs.left.viewMode"
|
||||
:thumbnail-size="paneConfigs.left.thumbnailSize"
|
||||
@@ -87,6 +88,7 @@
|
||||
|
||||
<FilePane
|
||||
v-if="layoutMode === 'horizontal' || layoutMode === 'vertical'"
|
||||
ref="rightPaneRef"
|
||||
pane-id="right"
|
||||
:view-mode="paneConfigs.right.viewMode"
|
||||
:thumbnail-size="paneConfigs.right.thumbnailSize"
|
||||
@@ -102,6 +104,10 @@ import { ref, computed } from 'vue'
|
||||
import FilePane from './FilePane.vue'
|
||||
import type { ViewMode, ThumbnailSize, LayoutMode, PaneId } from './types'
|
||||
|
||||
// FilePane refs
|
||||
const leftPaneRef = ref<InstanceType<typeof FilePane>>()
|
||||
const rightPaneRef = ref<InstanceType<typeof FilePane>>()
|
||||
|
||||
// 每个面板的独立配置
|
||||
const paneConfigs = ref({
|
||||
left: {
|
||||
@@ -174,6 +180,26 @@ function handleSplitterMouseDown(event: MouseEvent) {
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
function goBack() {
|
||||
const activePaneRef = activePane.value === 'left' ? leftPaneRef.value : rightPaneRef.value
|
||||
if (activePaneRef && typeof activePaneRef.goBack === 'function') {
|
||||
activePaneRef.goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function goForward() {
|
||||
const activePaneRef = activePane.value === 'left' ? leftPaneRef.value : rightPaneRef.value
|
||||
if (activePaneRef && typeof activePaneRef.goForward === 'function') {
|
||||
activePaneRef.goForward()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
goBack,
|
||||
goForward
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="['file-pane', { active: isActive, inactive: !isActive }]" @click="$emit('activate')">
|
||||
<div :class="['file-pane', { active: isActive, inactive: !isActive }]" @click="$emit('activate')" @mousedown="handlePaneMouseDown">
|
||||
<!-- Top indicator -->
|
||||
<div v-if="isActive" class="active-indicator"></div>
|
||||
|
||||
@@ -218,6 +218,10 @@ const pathEditMode = ref(false)
|
||||
const pathEditValue = ref('')
|
||||
const zoomLevel = ref(100) // 缩放级别:50-200%
|
||||
|
||||
// History management
|
||||
const history = ref<string[][]>([['Home']]) // 存储路径历史
|
||||
const historyIndex = ref(0) // 当前历史位置
|
||||
|
||||
// Context Menu state
|
||||
const contextMenu = ref({
|
||||
visible: false,
|
||||
@@ -280,11 +284,45 @@ function highlightText(text: string): string {
|
||||
|
||||
function navigateToIndex(index: number) {
|
||||
currentPath.value = currentPath.value.slice(0, index + 1)
|
||||
addToHistory(currentPath.value)
|
||||
selection.clearSelection()
|
||||
searchQuery.value = ''
|
||||
breadcrumbDropdown.value.visible = false
|
||||
}
|
||||
|
||||
// History navigation functions
|
||||
function addToHistory(path: string[]) {
|
||||
// 检查新路径是否与当前历史位置的路径相同
|
||||
const currentHistoryPath = history.value[historyIndex.value]
|
||||
if (currentHistoryPath && JSON.stringify(currentHistoryPath) === JSON.stringify(path)) {
|
||||
return // 路径相同,不添加重复历史
|
||||
}
|
||||
|
||||
// 删除当前位置之后的所有历史(用户在历史中间位置进行了新的导航)
|
||||
history.value = history.value.slice(0, historyIndex.value + 1)
|
||||
// 添加新路径
|
||||
history.value.push([...path])
|
||||
historyIndex.value = history.value.length - 1
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (historyIndex.value > 0) {
|
||||
historyIndex.value--
|
||||
currentPath.value = [...history.value[historyIndex.value]]
|
||||
selection.clearSelection()
|
||||
searchQuery.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function goForward() {
|
||||
if (historyIndex.value < history.value.length - 1) {
|
||||
historyIndex.value++
|
||||
currentPath.value = [...history.value[historyIndex.value]]
|
||||
selection.clearSelection()
|
||||
searchQuery.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Breadcrumb dropdown
|
||||
function toggleBreadcrumbDropdown(index: number) {
|
||||
if (breadcrumbDropdown.value.visible && breadcrumbDropdown.value.index === index) {
|
||||
@@ -336,6 +374,7 @@ function navigateToBreadcrumbFolder(folderName: string) {
|
||||
// 导航到选中的文件夹
|
||||
const newPath = [...currentPath.value.slice(0, index + 1), folderName]
|
||||
currentPath.value = newPath
|
||||
addToHistory(currentPath.value)
|
||||
selection.clearSelection()
|
||||
searchQuery.value = ''
|
||||
breadcrumbDropdown.value.visible = false
|
||||
@@ -385,6 +424,7 @@ function applyPathEdit() {
|
||||
|
||||
// 应用路径
|
||||
currentPath.value = segments
|
||||
addToHistory(currentPath.value)
|
||||
selection.clearSelection()
|
||||
searchQuery.value = ''
|
||||
pathEditMode.value = false
|
||||
@@ -397,6 +437,7 @@ function cancelPathEdit() {
|
||||
|
||||
function navigateToFolder(folderName: string) {
|
||||
currentPath.value.push(folderName)
|
||||
addToHistory(currentPath.value)
|
||||
selection.clearSelection()
|
||||
searchQuery.value = ''
|
||||
}
|
||||
@@ -432,6 +473,11 @@ function handleItemDoubleClick(item: FileItem) {
|
||||
}
|
||||
}
|
||||
|
||||
// 面板级别的鼠标按下处理(已由 FieldView 统一处理,此处仅作为备用)
|
||||
function handlePaneMouseDown(event: MouseEvent) {
|
||||
// 鼠标侧键由 FieldView 全局处理,这里不再需要处理
|
||||
}
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement
|
||||
if (target.closest('.file-item') || target.closest('.list-item')) {
|
||||
@@ -946,6 +992,12 @@ useKeyboardNav({
|
||||
},
|
||||
getGridColumns
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
goBack,
|
||||
goForward
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,11 +1,77 @@
|
||||
<template>
|
||||
<div class="field-view">
|
||||
<FileManager />
|
||||
<FileManager ref="fileManagerRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FileManager from '@/components/FileManager/FileManager.vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const fileManagerRef = ref<InstanceType<typeof FileManager>>()
|
||||
|
||||
// 使用全局事件监听来阻止浏览器的前进/后退
|
||||
// 使用 mouseup 事件,确保只在按键抬起时执行一次
|
||||
function handleGlobalMouseUp(event: MouseEvent) {
|
||||
// 检查是否在 field-view 区域内
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.field-view')) {
|
||||
return
|
||||
}
|
||||
|
||||
// 鼠标侧键:button 3 = 后退, button 4 = 前进
|
||||
if (event.button === 3 || event.button === 4) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// 找到鼠标所在的面板并激活它
|
||||
const filePane = target.closest('.file-pane') as HTMLElement
|
||||
if (filePane) {
|
||||
filePane.click()
|
||||
|
||||
// 确保面板激活后再执行导航
|
||||
setTimeout(() => {
|
||||
if (event.button === 3) {
|
||||
fileManagerRef.value?.goBack()
|
||||
} else if (event.button === 4) {
|
||||
fileManagerRef.value?.goForward()
|
||||
}
|
||||
}, 0)
|
||||
} else {
|
||||
// 如果不在面板内,直接对当前激活的面板执行操作
|
||||
if (event.button === 3) {
|
||||
fileManagerRef.value?.goBack()
|
||||
} else if (event.button === 4) {
|
||||
fileManagerRef.value?.goForward()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在 mousedown 阶段也需要阻止默认行为,但不执行导航
|
||||
function handleGlobalMouseDown(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.field-view')) {
|
||||
return
|
||||
}
|
||||
|
||||
// 鼠标侧键:只阻止默认行为,不执行导航
|
||||
if (event.button === 3 || event.button === 4) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 在捕获阶段监听,确保最早拦截
|
||||
document.addEventListener('mousedown', handleGlobalMouseDown, true)
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousedown', handleGlobalMouseDown, true)
|
||||
document.removeEventListener('mouseup', handleGlobalMouseUp, true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in new issue
Block a user