import React, { useCallback, useState, useEffect, useRef } from 'react' import { useTabStore } from '../../stores/tabStore' import { useConfirm } from '../../hooks/useConfirm' import { getFileName } from '../../lib/fileUtils' import { Close, Plus } from '../Icons' import { ConfirmDialog } from '../ConfirmDialog/ConfirmDialog' interface ContextMenuState { visible: boolean x: number y: number tabId: string } export const TabBar = React.memo(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 moveTab = useTabStore(s => s.moveTab) const tabListRef = useRef(null) const [menu, setMenu] = useState({ visible: false, x: 0, y: 0, tabId: '' }) const [dragOverIndex, setDragOverIndex] = useState(null) const dragTabIdRef = useRef(null) const { confirm, confirmDialogProps } = useConfirm() // 滚动到活动标签 const scrollToActiveTab = useCallback(() => { const tabList = tabListRef.current if (!tabList) return const activeTab = tabList.querySelector('.tab-item.active') as HTMLElement if (!activeTab) return const listRect = tabList.getBoundingClientRect() const tabRect = activeTab.getBoundingClientRect() // 如果标签在可视区域左侧之外 if (tabRect.left < listRect.left) { tabList.scrollLeft -= (listRect.left - tabRect.left + 20) } // 如果标签在可视区域右侧之外 else if (tabRect.right > listRect.right) { tabList.scrollLeft += (tabRect.right - listRect.right + 20) } }, []) // 自动滚动到活动标签 useEffect(() => { requestAnimationFrame(scrollToActiveTab) }, [activeTabId, scrollToActiveTab]) // 支持鼠标滚轮水平滚动标签栏 useEffect(() => { const tabList = tabListRef.current if (!tabList) return const handleWheel = (e: WheelEvent) => { // 检查是否有水平溢出 if (tabList.scrollWidth <= tabList.clientWidth) return // 阻止默认滚动 e.preventDefault() // 计算滚动量:支持触控板 deltaX 和鼠标滚轮 deltaY const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY tabList.scrollLeft += delta } // 直接绑定到 tabList,使用 passive: false 允许 preventDefault tabList.addEventListener('wheel', handleWheel, { passive: false }) return () => tabList.removeEventListener('wheel', handleWheel) }, []) const handleClose = useCallback(async (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) : '未命名' const confirmed = await confirm({ title: '关闭标签', message: `"${name}" 尚未保存,确定要关闭吗?`, variant: 'warning', confirmLabel: '关闭' }) if (!confirmed) return } closeTab(tabId) }, [tabs, closeTab, confirm]) // 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(async () => { const tab = tabs.find(t => t.id === menu.tabId) if (tab?.isModified) { const name = tab.filePath ? getFileName(tab.filePath) : '未命名' const confirmed = await confirm({ title: '关闭标签', message: `"${name}" 尚未保存,确定要关闭吗?`, variant: 'warning', confirmLabel: '关闭' }) if (!confirmed) return } closeTab(menu.tabId) setMenu(prev => ({ ...prev, visible: false })) }, [tabs, menu.tabId, closeTab, confirm]) const handleMenuCloseOthers = useCallback(async () => { 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('、') const confirmed = await confirm({ title: '关闭其他标签', message: `以下文件尚未保存:${names},确定要关闭吗?`, variant: 'warning', confirmLabel: '关闭' }) if (!confirmed) return } closeOtherTabs(menu.tabId) setMenu(prev => ({ ...prev, visible: false })) }, [tabs, menu.tabId, closeOtherTabs, confirm]) const handleMenuCloseAll = useCallback(async () => { const modified = tabs.filter(t => t.isModified) if (modified.length > 0) { const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、') const confirmed = await confirm({ title: '关闭全部标签', message: `以下文件尚未保存:${names},确定要关闭吗?`, variant: 'warning', confirmLabel: '关闭' }) if (!confirmed) return } closeAllTabs() setMenu(prev => ({ ...prev, visible: false })) }, [tabs, closeAllTabs, confirm]) const handleMenuCloseRight = useCallback(async () => { 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('、') const confirmed = await confirm({ title: '关闭右侧标签', message: `以下文件尚未保存:${names},确定要关闭吗?`, variant: 'warning', confirmLabel: '关闭' }) if (!confirmed) return } closeTabsToRight(menu.tabId) setMenu(prev => ({ ...prev, visible: false })) }, [tabs, menu.tabId, closeTabsToRight, confirm]) // D1: 拖拽排序事件处理 const handleDragStart = useCallback((e: React.DragEvent, tabId: string) => { dragTabIdRef.current = tabId e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', tabId) // 延迟添加 dragging 类,避免拖拽图像被 CSS 捕获 requestAnimationFrame(() => { const el = document.querySelector(`[data-tab-id="${tabId}"]`) as HTMLElement el?.classList.add('dragging') }) }, []) const handleDragOver = useCallback((e: React.DragEvent, index: number) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' setDragOverIndex(index) }, []) const handleDragLeave = useCallback(() => { setDragOverIndex(null) }, []) const handleDrop = useCallback((e: React.DragEvent, toIndex: number) => { e.preventDefault() setDragOverIndex(null) const fromId = dragTabIdRef.current if (fromId) { moveTab(fromId, toIndex) } dragTabIdRef.current = null // 清理 dragging 类 document.querySelectorAll('.tab-item.dragging').forEach(el => el.classList.remove('dragging')) }, [moveTab]) const handleDragEnd = useCallback(() => { setDragOverIndex(null) document.querySelectorAll('.tab-item.dragging').forEach(el => el.classList.remove('dragging')) dragTabIdRef.current = null }, []) 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 ( <>
{tabs.map((tab, index) => (
switchToTab(tab.id)} onContextMenu={(e) => handleContextMenu(e, tab.id)} onDragStart={(e) => handleDragStart(e, tab.id)} onDragOver={(e) => handleDragOver(e, index)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, index)} onDragEnd={handleDragEnd} > {tab.filePath ? getFileName(tab.filePath) : '未命名'}
))}
{/* 右键菜单 */} {menu.visible && (
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} >
关闭
{tabs.length > 1 && (
关闭其他标签
)} {hasRightTabs && (
关闭右侧标签
)}
关闭全部标签
)}
) }) TabBar.displayName = 'TabBar'