import React, { useMemo, useRef, useState } from 'react'; import { Tabs, Dropdown } from 'antd'; import type { MenuProps, TabsProps } from 'antd'; import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core'; import type { DragStartEvent, DragEndEvent } from '@dnd-kit/core'; import { SortableContext, useSortable, horizontalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { restrictToHorizontalAxis } from '@dnd-kit/modifiers'; import { useStore } from '../store'; import DataViewer from './DataViewer'; import QueryEditor from './QueryEditor'; import TableDesigner from './TableDesigner'; import RedisViewer from './RedisViewer'; import RedisCommandEditor from './RedisCommandEditor'; import TriggerViewer from './TriggerViewer'; import DefinitionViewer from './DefinitionViewer'; import type { TabData } from '../types'; const detectConnectionEnvLabel = (connectionName: string): string | null => { const tokens = connectionName.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean); if (tokens.includes('prod') || tokens.includes('production')) return 'PROD'; if (tokens.includes('uat')) return 'UAT'; if (tokens.includes('dev') || tokens.includes('development')) return 'DEV'; if (tokens.includes('sit')) return 'SIT'; if (tokens.includes('stg') || tokens.includes('stage') || tokens.includes('staging') || tokens.includes('pre')) return 'STG'; if (tokens.includes('test') || tokens.includes('qa')) return 'TEST'; return null; }; const buildTabDisplayTitle = (tab: TabData, connectionName: string | undefined): string => { if (tab.type !== 'table' && tab.type !== 'design') return tab.title; if (!connectionName) return tab.title; const prefix = detectConnectionEnvLabel(connectionName) || connectionName; return `[${prefix}] ${tab.title}`; }; type SortableTabLabelProps = { displayTitle: string; menuItems: MenuProps['items']; }; const SortableTabLabel: React.FC = ({ displayTitle, menuItems, }) => { return ( e.preventDefault()} title="拖拽调整标签顺序" > {displayTitle} ); }; type DraggableTabNodeProps = { node: React.ReactElement; }; const DraggableTabNode: React.FC = ({ node }) => { const tabId = String(node.key || '').trim(); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: tabId }); const style: React.CSSProperties = { ...(node.props.style || {}), transform: CSS.Transform.toString(transform), transition: transition || 'transform 180ms cubic-bezier(0.22, 1, 0.36, 1)', opacity: isDragging ? 0.88 : 1, cursor: isDragging ? 'grabbing' : 'grab', touchAction: 'none', zIndex: isDragging ? 2 : node.props.style?.zIndex, }; return React.cloneElement(node, { ref: setNodeRef, style, ...attributes, ...listeners, className: `${node.props.className || ''} tab-dnd-node${isDragging ? ' is-dragging' : ''}`, }); }; const TabManager: React.FC = () => { const tabs = useStore(state => state.tabs); const connections = useStore(state => state.connections); const theme = useStore(state => state.theme); const activeTabId = useStore(state => state.activeTabId); const setActiveTab = useStore(state => state.setActiveTab); const closeTab = useStore(state => state.closeTab); const closeOtherTabs = useStore(state => state.closeOtherTabs); const closeTabsToLeft = useStore(state => state.closeTabsToLeft); const closeTabsToRight = useStore(state => state.closeTabsToRight); const closeAllTabs = useStore(state => state.closeAllTabs); const moveTab = useStore(state => state.moveTab); const tabsNavBorderColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.09)' : 'rgba(0, 0, 0, 0.08)'; const [draggingTabId, setDraggingTabId] = useState(null); const suppressClickUntilRef = useRef(0); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 }, }) ); const onChange = (newActiveKey: string) => { setActiveTab(newActiveKey); }; const onEdit = (targetKey: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => { if (action === 'remove') { closeTab(targetKey as string); } }; const handleDragStart = (event: DragStartEvent) => { const sourceId = String(event.active.id || '').trim(); setDraggingTabId(sourceId || null); }; const handleDragEnd = (event: DragEndEvent) => { const sourceId = String(event.active.id || '').trim(); const targetId = String(event.over?.id || '').trim(); setDraggingTabId(null); if (!sourceId || !targetId || sourceId === targetId) { return; } suppressClickUntilRef.current = Date.now() + 120; moveTab(sourceId, targetId); }; const handleDragCancel = () => { setDraggingTabId(null); }; const tabIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]); const renderTabBar: TabsProps['renderTabBar'] = (tabBarProps, DefaultTabBar) => ( {(node) => } ); const items = useMemo(() => tabs.map((tab, index) => { const connectionName = connections.find((conn) => conn.id === tab.connectionId)?.name; const displayTitle = buildTabDisplayTitle(tab, connectionName); let content; if (tab.type === 'query') { content = ; } else if (tab.type === 'table') { content = ; } else if (tab.type === 'design') { content = ; } else if (tab.type === 'redis-keys') { content = ; } else if (tab.type === 'redis-command') { content = ; } else if (tab.type === 'trigger') { content = ; } else if (tab.type === 'view-def' || tab.type === 'routine-def') { content = ; } const menuItems: MenuProps['items'] = [ { key: 'close-other', label: '关闭其他页', disabled: tabs.length <= 1, onClick: () => closeOtherTabs(tab.id), }, { key: 'close-left', label: '关闭左侧', disabled: index === 0, onClick: () => closeTabsToLeft(tab.id), }, { key: 'close-right', label: '关闭右侧', disabled: index === tabs.length - 1, onClick: () => closeTabsToRight(tab.id), }, { type: 'divider' }, { key: 'close-all', label: '关闭所有', disabled: tabs.length === 0, onClick: () => closeAllTabs(), }, ]; return { label: ( ), key: tab.id, children: content, }; }), [tabs, connections, closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs]); return ( <> { if (Date.now() < suppressClickUntilRef.current) return; onChange(newActiveKey); }} activeKey={activeTabId || undefined} onEdit={onEdit} items={items} hideAdd renderTabBar={renderTabBar} /> ); }; export default TabManager;