import React, { useEffect, useMemo } from 'react'; import { closestCenter, DndContext, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Button, Dropdown, Tabs, type TabsProps } from 'antd'; import { DownOutlined } from '@ant-design/icons'; import type { Location } from 'react-router'; import type { AppMenuItem } from '../../auth/menu-policy'; import { useAppStore } from '../../store'; import { upsertDockTab } from './dockTabs'; interface RouteDockProps { location: Location; menuItems: readonly AppMenuItem[]; onNavigate: (path: string) => void; draggable: boolean; } interface DraggableTabNodeProps extends React.HTMLAttributes { 'data-node-key': string; } function findMenuLabel(items: readonly AppMenuItem[], pathname: string): string | undefined { for (const item of items) { if (item.key === pathname) return item.label; if (item.children) { const label = findMenuLabel(item.children, pathname); if (label) return label; } } return undefined; } function getRouteLabel(items: readonly AppMenuItem[], pathname: string): string { const menuLabel = findMenuLabel(items, pathname); if (menuLabel) return menuLabel; if (/^\/students\/\d+\/profile$/.test(pathname)) return '学生档案'; if (/^\/classes\/\d+$/.test(pathname)) return '班级详情'; return pathname === '/' ? '首页' : '页面'; } const DraggableTabNode: React.FC> = ({ ...props }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: props['data-node-key'], }); const child = props.children as React.ReactElement<{ style?: React.CSSProperties }>; return React.cloneElement(child, { ref: setNodeRef, style: { ...child.props.style, transform: CSS.Translate.toString(transform), transition, cursor: isDragging ? 'grabbing' : 'grab', zIndex: isDragging ? 1 : undefined, opacity: isDragging ? 0.92 : undefined, boxShadow: isDragging ? '0 8px 20px rgba(29, 29, 31, 0.14)' : undefined, }, ...attributes, ...listeners, } as React.HTMLAttributes); }; const RouteDock: React.FC = ({ location, menuItems, onNavigate, draggable }) => { // 与 RouteKeeper 缓存 key 保持一致:只按 pathname 建 tab,避免 query 变化产生重复页签。 const activeKey = location.pathname; const tabs = useAppStore((state) => state.routeDockTabs); const setRouteDockTabs = useAppStore((state) => state.setRouteDockTabs); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); useEffect(() => { if (location.pathname === '/') return; const label = getRouteLabel(menuItems, location.pathname); setRouteDockTabs((currentTabs) => upsertDockTab(currentTabs, activeKey, label)); }, [activeKey, location.pathname, menuItems, setRouteDockTabs]); const tabItems = useMemo>( () => tabs.map((tab) => ({ key: tab.key, label: tab.label, closable: tabs.length > 1, })), [tabs], ); const closeTab = (targetKey: string) => { const targetIndex = tabs.findIndex((tab) => tab.key === targetKey); if (targetIndex < 0 || tabs.length === 1) return; const nextTabs = tabs.filter((tab) => tab.key !== targetKey); setRouteDockTabs(nextTabs); if (targetKey === activeKey) { const nextActiveTab = nextTabs[Math.min(targetIndex, nextTabs.length - 1)]; if (nextActiveTab) onNavigate(nextActiveTab.key); } }; const closeOthers = () => { setRouteDockTabs(tabs.filter((tab) => tab.key === activeKey)); }; const closeLeft = () => { const index = tabs.findIndex((tab) => tab.key === activeKey); if (index <= 0) return; setRouteDockTabs(tabs.filter((_, i) => i >= index)); }; const closeRight = () => { const index = tabs.findIndex((tab) => tab.key === activeKey); if (index < 0 || index === tabs.length - 1) return; setRouteDockTabs(tabs.filter((_, i) => i <= index)); }; const closeAll = () => { setRouteDockTabs([]); onNavigate('/dashboard'); }; const handleDragEnd = ({ active, over }: DragEndEvent) => { if (!over || active.id === over.id) return; setRouteDockTabs((currentTabs) => { const activeIndex = currentTabs.findIndex((tab) => tab.key === active.id); const overIndex = currentTabs.findIndex((tab) => tab.key === over.id); return activeIndex < 0 || overIndex < 0 ? currentTabs : arrayMove(currentTabs, activeIndex, overIndex); }); }; const renderTabBar: TabsProps['renderTabBar'] = (tabBarProps, DefaultTabBar) => { const tabBar = ( {(node) => { if (!draggable) return node; return ( ).props} key={node.key} > {node} ); }} ); if (!draggable) return tabBar; return ( tab.key)} strategy={horizontalListSortingStrategy} > {tabBar} ); }; if (location.pathname === '/' || tabs.length === 0) return null; return ( ); }; export default RouteDock;