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 -->
|
<!-- 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user