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:
jimmyandClaude committed 2025-10-01 21:32:55 +08:00
1 parent bf03ca9535
commit 86aa8affc3
3 files changed
+146 -2

No files matched your search

@@ -72,6 +72,7 @@
<!-- Main Content --> <!-- Main Content -->
<div :class="['main-content', `layout-${layoutMode}`]"> <div :class="['main-content', `layout-${layoutMode}`]">
<FilePane <FilePane
ref="leftPaneRef"
pane-id="left" pane-id="left"
:view-mode="paneConfigs.left.viewMode" :view-mode="paneConfigs.left.viewMode"
:thumbnail-size="paneConfigs.left.thumbnailSize" :thumbnail-size="paneConfigs.left.thumbnailSize"
@@ -87,6 +88,7 @@
<FilePane <FilePane
v-if="layoutMode === 'horizontal' || layoutMode === 'vertical'" v-if="layoutMode === 'horizontal' || layoutMode === 'vertical'"
ref="rightPaneRef"
pane-id="right" pane-id="right"
:view-mode="paneConfigs.right.viewMode" :view-mode="paneConfigs.right.viewMode"
:thumbnail-size="paneConfigs.right.thumbnailSize" :thumbnail-size="paneConfigs.right.thumbnailSize"
@@ -102,6 +104,10 @@ import { ref, computed } from 'vue'
import FilePane from './FilePane.vue' import FilePane from './FilePane.vue'
import type { ViewMode, ThumbnailSize, LayoutMode, PaneId } from './types' 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({ const paneConfigs = ref({
left: { left: {
@@ -174,6 +180,26 @@ function handleSplitterMouseDown(event: MouseEvent) {
document.addEventListener('mousemove', onMouseMove) document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp) 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> </script>
<style scoped> <style scoped>
@@ -1,5 +1,5 @@
<template> <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 --> <!-- Top indicator -->
<div v-if="isActive" class="active-indicator"></div> <div v-if="isActive" class="active-indicator"></div>
@@ -218,6 +218,10 @@ const pathEditMode = ref(false)
const pathEditValue = ref('') const pathEditValue = ref('')
const zoomLevel = ref(100) // 缩放级别:50-200% const zoomLevel = ref(100) // 缩放级别:50-200%
// History management
const history = ref<string[][]>([['Home']]) // 存储路径历史
const historyIndex = ref(0) // 当前历史位置
// Context Menu state // Context Menu state
const contextMenu = ref({ const contextMenu = ref({
visible: false, visible: false,
@@ -280,11 +284,45 @@ function highlightText(text: string): string {
function navigateToIndex(index: number) { function navigateToIndex(index: number) {
currentPath.value = currentPath.value.slice(0, index + 1) currentPath.value = currentPath.value.slice(0, index + 1)
addToHistory(currentPath.value)
selection.clearSelection() selection.clearSelection()
searchQuery.value = '' searchQuery.value = ''
breadcrumbDropdown.value.visible = false 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 // Breadcrumb dropdown
function toggleBreadcrumbDropdown(index: number) { function toggleBreadcrumbDropdown(index: number) {
if (breadcrumbDropdown.value.visible && breadcrumbDropdown.value.index === index) { 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] const newPath = [...currentPath.value.slice(0, index + 1), folderName]
currentPath.value = newPath currentPath.value = newPath
addToHistory(currentPath.value)
selection.clearSelection() selection.clearSelection()
searchQuery.value = '' searchQuery.value = ''
breadcrumbDropdown.value.visible = false breadcrumbDropdown.value.visible = false
@@ -385,6 +424,7 @@ function applyPathEdit() {
// 应用路径 // 应用路径
currentPath.value = segments currentPath.value = segments
addToHistory(currentPath.value)
selection.clearSelection() selection.clearSelection()
searchQuery.value = '' searchQuery.value = ''
pathEditMode.value = false pathEditMode.value = false
@@ -397,6 +437,7 @@ function cancelPathEdit() {
function navigateToFolder(folderName: string) { function navigateToFolder(folderName: string) {
currentPath.value.push(folderName) currentPath.value.push(folderName)
addToHistory(currentPath.value)
selection.clearSelection() selection.clearSelection()
searchQuery.value = '' searchQuery.value = ''
} }
@@ -432,6 +473,11 @@ function handleItemDoubleClick(item: FileItem) {
} }
} }
// 面板级别的鼠标按下处理(已由 FieldView 统一处理,此处仅作为备用)
function handlePaneMouseDown(event: MouseEvent) {
// 鼠标侧键由 FieldView 全局处理,这里不再需要处理
}
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
const target = event.target as HTMLElement const target = event.target as HTMLElement
if (target.closest('.file-item') || target.closest('.list-item')) { if (target.closest('.file-item') || target.closest('.list-item')) {
@@ -946,6 +992,12 @@ useKeyboardNav({
}, },
getGridColumns getGridColumns
}) })
// 暴露方法给父组件
defineExpose({
goBack,
goForward
})
</script> </script>
<style scoped> <style scoped>
+67 -1
View File
@@ -1,11 +1,77 @@
<template> <template>
<div class="field-view"> <div class="field-view">
<FileManager /> <FileManager ref="fileManagerRef" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import FileManager from '@/components/FileManager/FileManager.vue' 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> </script>
<style scoped> <style scoped>