feat: add enterprise identity and channel integrations
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import { Check, ChevronLeft, ChevronRight, Ellipsis, X } from 'lucide-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'
|
||||
@@ -9,14 +9,26 @@ import {
|
||||
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
|
||||
@@ -60,6 +72,7 @@ export function AppRouteTabs() {
|
||||
canScrollLeft: false,
|
||||
canScrollRight: false
|
||||
})
|
||||
const activeTabIndex = openedTabs.findIndex((tab) => tab.path === route)
|
||||
|
||||
const syncScrollState = React.useCallback(() => {
|
||||
const element = scrollAreaRef.current
|
||||
@@ -124,13 +137,33 @@ export function AppRouteTabs() {
|
||||
|
||||
const closeTab = (path: string) => {
|
||||
const closedIndex = openedTabs.findIndex((tab) => tab.path === path)
|
||||
const next = openedTabs.filter((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 = next[Math.max(0, closedIndex - 1)] ?? next[0]
|
||||
window.location.hash = fallback?.path ?? '/'
|
||||
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) => {
|
||||
@@ -175,39 +208,53 @@ export function AppRouteTabs() {
|
||||
event.currentTarget.scrollLeft += event.deltaY
|
||||
}}
|
||||
>
|
||||
{openedTabs.map((tab) => (
|
||||
<div
|
||||
key={tab.path}
|
||||
ref={(node) => {
|
||||
if (node) tabRefs.current.set(tab.path, node)
|
||||
else tabRefs.current.delete(tab.path)
|
||||
}}
|
||||
className="group/tab relative flex flex-none items-center"
|
||||
>
|
||||
<TabsTrigger value={tab.path} className="max-w-52 flex-none px-3.5 pr-8">
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</TabsTrigger>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="absolute right-1 opacity-60 group-hover/tab:opacity-100 focus-visible:opacity-100"
|
||||
aria-label={`关闭${tab.label}(${tab.path})`}
|
||||
title={`关闭${tab.label}`}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
closeTab(tab.path)
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
{openedTabs.map((tab, tabIndex) => (
|
||||
<ContextMenu key={tab.path}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
ref={(node) => {
|
||||
if (node) tabRefs.current.set(tab.path, node)
|
||||
else tabRefs.current.delete(tab.path)
|
||||
}}
|
||||
className="group/tab relative flex flex-none items-center"
|
||||
>
|
||||
<TabsTrigger value={tab.path} className="max-w-52 flex-none px-3.5 pr-8">
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</TabsTrigger>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="absolute right-1 opacity-60 group-hover/tab:opacity-100 focus-visible:opacity-100"
|
||||
aria-label={`关闭${tab.label}(${tab.path})`}
|
||||
title={`关闭${tab.label}`}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
closeTab(tab.path)
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-44">
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem onSelect={() => closeTab(tab.path)}><X />关闭当前标签</ContextMenuItem>
|
||||
<ContextMenuItem disabled={tabIndex === 0} onSelect={() => closeTabs(tab.path, 'left')}><PanelLeftClose />关闭左侧标签</ContextMenuItem>
|
||||
<ContextMenuItem disabled={tabIndex === openedTabs.length - 1} onSelect={() => closeTabs(tab.path, 'right')}><PanelRightClose />关闭右侧标签</ContextMenuItem>
|
||||
<ContextMenuItem disabled={openedTabs.length <= 1} onSelect={() => closeTabs(tab.path, 'others')}><Layers2 />关闭其他标签</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem onSelect={() => closeTabs(tab.path, 'all')}><Trash2 />关闭全部标签</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{scrollState.overflow ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -218,33 +265,40 @@ export function AppRouteTabs() {
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="ghost" size="icon-sm" aria-label={`查看全部已打开页面,共 ${openedTabs.length} 个`}>
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="scrollbar-panel max-h-80 w-64">
|
||||
<DropdownMenuLabel>已打开页面 · {openedTabs.length}</DropdownMenuLabel>
|
||||
<DropdownMenuGroup>
|
||||
{openedTabs.map((tab) => (
|
||||
<DropdownMenuItem
|
||||
key={tab.path}
|
||||
aria-current={tab.path === route ? 'page' : undefined}
|
||||
onSelect={() => {
|
||||
if (tab.path !== route) window.location.hash = tab.path
|
||||
}}
|
||||
>
|
||||
{tab.path === route ? <Check /> : <span aria-hidden="true" className="size-4" />}
|
||||
<span className="truncate">{tab.label}</span>
|
||||
{tab.path === route ? <DropdownMenuShortcut>当前</DropdownMenuShortcut> : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : null}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="ghost" size="icon-sm" aria-label={`查看全部已打开页面,共 ${openedTabs.length} 个`} title="管理已打开页面">
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="scrollbar-panel max-h-80 w-64">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>已打开页面 · {openedTabs.length}</DropdownMenuLabel>
|
||||
{openedTabs.map((tab) => (
|
||||
<DropdownMenuItem
|
||||
key={tab.path}
|
||||
aria-current={tab.path === route ? 'page' : undefined}
|
||||
onSelect={() => {
|
||||
if (tab.path !== route) window.location.hash = tab.path
|
||||
}}
|
||||
>
|
||||
{tab.path === route ? <Check /> : <span aria-hidden="true" className="size-4" />}
|
||||
<span className="truncate">{tab.label}</span>
|
||||
{tab.path === route ? <DropdownMenuShortcut>当前</DropdownMenuShortcut> : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>批量关闭</DropdownMenuLabel>
|
||||
<DropdownMenuItem disabled={activeTabIndex <= 0} onSelect={() => closeTabs(route, 'left')}><PanelLeftClose />关闭左侧标签</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={activeTabIndex < 0 || activeTabIndex === openedTabs.length - 1} onSelect={() => closeTabs(route, 'right')}><PanelRightClose />关闭右侧标签</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={openedTabs.length <= 1} onSelect={() => closeTabs(route, 'others')}><Layers2 />关闭其他标签</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => closeTabs(route, 'all')}><Trash2 />关闭全部标签</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user