Files
MarkLite/src/renderer/components/TabBar/TabBar.tsx
T
thzxx 80dc7406e3 fix: 标签栏滚轮滚动和自动定位问题
- 修复滚轮不生效:使用 capture 阶段监听 wheel 事件
- 修复滚轮不生效:添加 stopPropagation 防止事件被拦截
- 新增自动滚动到活动标签:切换/打开标签时自动定位到可见区域
- 优化 CSS:移除 #tab-bar 的 overflow,由 #tab-list 统一负责滚动
- 添加平滑滚动:scroll-behavior: smooth
2026-05-28 14:30:09 +08:00

198 lines
6.9 KiB
TypeScript

import React, { useCallback, useState, useEffect, useRef } from 'react'
import { useTabStore } from '../../stores/tabStore'
import { getFileName } from '../../lib/fileUtils'
import { Close, Plus } from '../Icons'
interface ContextMenuState {
visible: boolean
x: number
y: number
tabId: string
}
export function TabBar() {
const tabs = useTabStore(s => s.tabs)
const activeTabId = useTabStore(s => s.activeTabId)
const switchToTab = useTabStore(s => s.switchToTab)
const closeTab = useTabStore(s => s.closeTab)
const createTab = useTabStore(s => s.createTab)
const closeOtherTabs = useTabStore(s => s.closeOtherTabs)
const closeAllTabs = useTabStore(s => s.closeAllTabs)
const closeTabsToRight = useTabStore(s => s.closeTabsToRight)
const tabListRef = useRef<HTMLDivElement>(null)
const [menu, setMenu] = useState<ContextMenuState>({ visible: false, x: 0, y: 0, tabId: '' })
// 支持鼠标滚轮水平滚动标签栏
useEffect(() => {
const tabList = tabListRef.current
if (!tabList) return
const handleWheel = (e: WheelEvent) => {
// 只有当标签列表有水平滚动空间时才处理
const hasHorizontalScroll = tabList.scrollWidth > tabList.clientWidth
if (!hasHorizontalScroll) return
// 阻止默认的垂直滚动
e.preventDefault()
e.stopPropagation()
// 将垂直滚动转换为水平滚动
tabList.scrollLeft += e.deltaY
}
// 使用 capture 阶段,确保事件能被捕获
tabList.addEventListener('wheel', handleWheel, { passive: false, capture: true })
return () => tabList.removeEventListener('wheel', handleWheel, { capture: true })
}, [])
// 自动滚动到活动标签
useEffect(() => {
if (!tabListRef.current || !activeTabId) return
// 使用 requestAnimationFrame 确保 DOM 已更新
requestAnimationFrame(() => {
const activeTab = tabListRef.current?.querySelector('.tab-item.active')
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}
})
}, [activeTabId])
const handleClose = useCallback((e: React.MouseEvent, tabId: string) => {
e.stopPropagation()
const tab = tabs.find(t => t.id === tabId)
if (tab?.isModified) {
const name = tab.filePath ? getFileName(tab.filePath) : '未命名'
if (!confirm(`"${name}" 尚未保存,确定要关闭吗?`)) return
}
closeTab(tabId)
}, [tabs, closeTab])
// C-06: 右键菜单(带边界修正)
const handleContextMenu = useCallback((e: React.MouseEvent, tabId: string) => {
e.preventDefault()
e.stopPropagation()
const MENU_WIDTH = 170
const MENU_HEIGHT = 140
const x = Math.min(e.clientX, window.innerWidth - MENU_WIDTH)
const y = Math.min(e.clientY, window.innerHeight - MENU_HEIGHT)
setMenu({ visible: true, x: Math.max(0, x), y: Math.max(0, y), tabId })
}, [])
useEffect(() => {
if (!menu.visible) return
const handleClick = () => setMenu(prev => ({ ...prev, visible: false }))
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}, [menu.visible])
const handleMenuClose = useCallback(() => {
const tab = tabs.find(t => t.id === menu.tabId)
if (tab?.isModified) {
const name = tab.filePath ? getFileName(tab.filePath) : '未命名'
if (!confirm(`"${name}" 尚未保存,确定要关闭吗?`)) return
}
closeTab(menu.tabId)
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, menu.tabId, closeTab])
const handleMenuCloseOthers = useCallback(() => {
const otherModified = tabs.filter(t => t.id !== menu.tabId && t.isModified)
if (otherModified.length > 0) {
const names = otherModified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return
}
closeOtherTabs(menu.tabId)
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, menu.tabId, closeOtherTabs])
const handleMenuCloseAll = useCallback(() => {
const modified = tabs.filter(t => t.isModified)
if (modified.length > 0) {
const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return
}
closeAllTabs()
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, closeAllTabs])
const handleMenuCloseRight = useCallback(() => {
const index = tabs.findIndex(t => t.id === menu.tabId)
const rightTabs = tabs.slice(index + 1)
const modified = rightTabs.filter(t => t.isModified)
if (modified.length > 0) {
const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、')
if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return
}
closeTabsToRight(menu.tabId)
setMenu(prev => ({ ...prev, visible: false }))
}, [tabs, menu.tabId, closeTabsToRight])
const hasRightTabs = menu.visible && (() => {
const index = tabs.findIndex(t => t.id === menu.tabId)
return index < tabs.length - 1
})()
if (tabs.length === 0) return null
return (
<div id="tab-bar">
<div id="tab-list" ref={tabListRef}>
{tabs.map(tab => (
<div
key={tab.id}
className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`}
onClick={() => switchToTab(tab.id)}
onContextMenu={(e) => handleContextMenu(e, tab.id)}
>
<span className="tab-name">
{tab.filePath ? getFileName(tab.filePath) : '未命名'}
</span>
<button
className="tab-close"
onClick={(e) => handleClose(e, tab.id)}
>
<Close size={10} />
</button>
</div>
))}
</div>
<button
className="tab-add-btn"
onClick={() => createTab(null, '')}
title="新建标签页 (Ctrl+T)"
>
<Plus size={14} />
</button>
{/* 右键菜单 */}
{menu.visible && (
<div
className="tab-context-menu"
style={{ left: menu.x, top: menu.y }}
onClick={(e) => e.stopPropagation()}
>
<div className="tab-context-item" onClick={handleMenuClose}>
关闭
</div>
{tabs.length > 1 && (
<div className="tab-context-item" onClick={handleMenuCloseOthers}>
关闭其他标签
</div>
)}
{hasRightTabs && (
<div className="tab-context-item" onClick={handleMenuCloseRight}>
关闭右侧标签
</div>
)}
<div className="tab-context-divider" />
<div className="tab-context-item" onClick={handleMenuCloseAll}>
关闭全部标签
</div>
</div>
)}
</div>
)
}