import * as React from 'react' import { Check, ChevronLeft, ChevronRight, Ellipsis, Layers2, PanelLeftClose, PanelRightClose, Trash2, X } from 'lucide-react' import { useCurrentRoute, type RouteNavigationTab } from '@/app/route-meta' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from '@/components/ui/context-menu' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' const STORAGE_KEY = 'ai-ticket-platform.opened-route-tabs.v2' const MAX_OPEN_TABS = 12 const SCROLL_EPSILON = 2 const WORKBENCH_TAB: RouteNavigationTab = { label: '工作台', path: '/' } type CloseScope = 'left' | 'right' | 'others' | 'all' type TabScrollState = { overflow: boolean canScrollLeft: boolean canScrollRight: boolean } function readOpenedTabs(): RouteNavigationTab[] { try { const stored: unknown = JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) ?? '[]') if (!Array.isArray(stored)) return [] return stored.filter((item): item is RouteNavigationTab => { if (!item || typeof item !== 'object') return false const tab = item as Partial return typeof tab.label === 'string' && typeof tab.path === 'string' && tab.path.startsWith('/') }).slice(-MAX_OPEN_TABS) } catch { return [] } } function saveOpenedTabs(tabs: RouteNavigationTab[]) { window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(tabs)) } function addOpenedTab(tabs: RouteNavigationTab[], tab: RouteNavigationTab) { const existing = tabs.findIndex((item) => item.path === tab.path) if (existing >= 0) { return tabs.map((item, index) => index === existing ? { ...item, label: tab.label } : item) } return [...tabs, tab].slice(-MAX_OPEN_TABS) } export function AppRouteTabs() { const { route, meta } = useCurrentRoute() const [openedTabs, setOpenedTabs] = React.useState(readOpenedTabs) const scrollAreaRef = React.useRef(null) const tabRefs = React.useRef(new Map()) const [scrollState, setScrollState] = React.useState({ overflow: false, canScrollLeft: false, canScrollRight: false }) const activeTabIndex = openedTabs.findIndex((tab) => tab.path === route) const syncScrollState = React.useCallback(() => { const element = scrollAreaRef.current if (!element) return const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth) setScrollState({ overflow: maxScrollLeft > SCROLL_EPSILON, canScrollLeft: element.scrollLeft > SCROLL_EPSILON, canScrollRight: element.scrollLeft < maxScrollLeft - SCROLL_EPSILON }) }, []) React.useEffect(() => { setOpenedTabs((current) => { const next = addOpenedTab(current, { label: meta.title, path: route }) saveOpenedTabs(next) return next }) }, [meta.title, route]) React.useLayoutEffect(() => { const element = scrollAreaRef.current if (!element) return syncScrollState() element.addEventListener('scroll', syncScrollState, { passive: true }) window.addEventListener('resize', syncScrollState) const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(syncScrollState) resizeObserver?.observe(element) return () => { element.removeEventListener('scroll', syncScrollState) window.removeEventListener('resize', syncScrollState) resizeObserver?.disconnect() } }, [openedTabs.length, syncScrollState]) React.useLayoutEffect(() => { const frame = window.requestAnimationFrame(() => { const element = scrollAreaRef.current const activeTab = tabRefs.current.get(route) if (!element || !activeTab) return const tabStart = activeTab.offsetLeft const tabEnd = tabStart + activeTab.offsetWidth const viewportStart = element.scrollLeft const viewportEnd = viewportStart + element.clientWidth let nextScrollLeft = viewportStart if (tabStart < viewportStart) nextScrollLeft = Math.max(0, tabStart - 16) if (tabEnd > viewportEnd) nextScrollLeft = tabEnd - element.clientWidth + 16 if (nextScrollLeft !== viewportStart) { if (typeof element.scrollTo === 'function') element.scrollTo({ left: nextScrollLeft, behavior: 'smooth' }) else element.scrollLeft = nextScrollLeft } syncScrollState() }) return () => window.cancelAnimationFrame(frame) }, [openedTabs, route, scrollState.overflow, syncScrollState]) const closeTab = (path: string) => { const closedIndex = openedTabs.findIndex((tab) => tab.path === path) const remaining = openedTabs.filter((tab) => tab.path !== path) const next = remaining.length ? remaining : [WORKBENCH_TAB] saveOpenedTabs(next) setOpenedTabs(next) if (path !== route) return const fallback = remaining[Math.max(0, closedIndex - 1)] ?? remaining[0] ?? WORKBENCH_TAB window.location.hash = fallback.path } const closeTabs = (anchorPath: string, scope: CloseScope) => { const anchorIndex = openedTabs.findIndex((tab) => tab.path === anchorPath) if (anchorIndex < 0 && scope !== 'all') return let remaining: RouteNavigationTab[] if (scope === 'left') remaining = openedTabs.slice(anchorIndex) else if (scope === 'right') remaining = openedTabs.slice(0, anchorIndex + 1) else if (scope === 'others') remaining = [openedTabs[anchorIndex]] else remaining = [] const next = remaining.length ? remaining : [WORKBENCH_TAB] saveOpenedTabs(next) setOpenedTabs(next) if (next.some((tab) => tab.path === route)) return const anchor = next.find((tab) => tab.path === anchorPath) window.location.hash = anchor?.path ?? WORKBENCH_TAB.path } const scrollTabs = (direction: -1 | 1) => { const element = scrollAreaRef.current if (!element) return const distance = Math.max(240, element.clientWidth * 0.65) * direction if (typeof element.scrollBy === 'function') element.scrollBy({ left: distance, behavior: 'smooth' }) else element.scrollLeft += distance } return ( ) }