feat: add enterprise identity and channel integrations

This commit is contained in:
hailin
2026-07-27 19:03:42 +08:00
parent d93cb75c10
commit e69368a2f3
44 changed files with 5045 additions and 69 deletions

View File

@@ -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>
)
}