2026-03-08 01:34:54 +08:00
|
|
|
import { FileItem } from '@/lib/api'
|
|
|
|
|
import { X } from 'lucide-react'
|
|
|
|
|
import { clsx } from 'clsx'
|
|
|
|
|
import { useWallpaper } from '@/stores'
|
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
|
import { ContextMenu } from '@/components/common/ContextMenu'
|
|
|
|
|
import { getModuleTabId } from '@/lib/module-registry'
|
|
|
|
|
|
|
|
|
|
const HOME_TAB_ID = getModuleTabId('home') ?? 'home-tab'
|
|
|
|
|
|
|
|
|
|
export interface TabBarProps {
|
|
|
|
|
openFiles: FileItem[]
|
|
|
|
|
activeFile: FileItem | null
|
|
|
|
|
onTabClick: (file: FileItem) => void
|
|
|
|
|
onTabClose: (file: FileItem, e: React.MouseEvent) => void
|
|
|
|
|
onCloseOther?: (file: FileItem) => void
|
|
|
|
|
onCloseAll?: () => void
|
2026-03-21 23:42:48 +08:00
|
|
|
onPopOut?: (file: FileItem) => void
|
2026-03-08 01:34:54 +08:00
|
|
|
className?: string
|
|
|
|
|
style?: React.CSSProperties
|
|
|
|
|
variant?: 'default' | 'titlebar'
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 23:42:48 +08:00
|
|
|
export const TabBar = ({ openFiles, activeFile, onTabClick, onTabClose, onCloseOther, onCloseAll, onPopOut, className, style, variant = 'default' }: TabBarProps) => {
|
2026-03-08 01:34:54 +08:00
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const { opacity } = useWallpaper()
|
|
|
|
|
const [contextMenu, setContextMenu] = useState<{
|
|
|
|
|
isOpen: boolean
|
|
|
|
|
position: { x: number; y: number }
|
|
|
|
|
file: FileItem | null
|
|
|
|
|
}>({
|
|
|
|
|
isOpen: false,
|
|
|
|
|
position: { x: 0, y: 0 },
|
|
|
|
|
file: null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleContextMenu = (e: React.MouseEvent, file: FileItem) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
setContextMenu({
|
|
|
|
|
isOpen: true,
|
|
|
|
|
position: { x: e.clientX, y: e.clientY },
|
|
|
|
|
file
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCloseContextMenu = () => {
|
|
|
|
|
setContextMenu(prev => ({ ...prev, isOpen: false }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCloseOther = () => {
|
|
|
|
|
if (contextMenu.file && onCloseOther) {
|
|
|
|
|
onCloseOther(contextMenu.file)
|
|
|
|
|
}
|
|
|
|
|
handleCloseContextMenu()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCloseAll = () => {
|
|
|
|
|
if (onCloseAll) {
|
|
|
|
|
onCloseAll()
|
|
|
|
|
}
|
|
|
|
|
handleCloseContextMenu()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 23:42:48 +08:00
|
|
|
const handlePopOut = () => {
|
|
|
|
|
if (contextMenu.file && onPopOut) {
|
|
|
|
|
onPopOut(contextMenu.file)
|
|
|
|
|
}
|
|
|
|
|
handleCloseContextMenu()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 00:00:34 +08:00
|
|
|
const isHomeTab = contextMenu.file?.path === HOME_TAB_ID
|
2026-03-08 01:34:54 +08:00
|
|
|
const contextMenuItems = [
|
2026-03-22 00:00:34 +08:00
|
|
|
...(!isHomeTab ? [{ label: '在新窗口中打开', onClick: handlePopOut }] : []),
|
2026-03-08 01:34:54 +08:00
|
|
|
{ label: '关闭其他标签页', onClick: handleCloseOther },
|
|
|
|
|
{ label: '关闭所有标签页', onClick: handleCloseAll }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (activeFile && scrollContainerRef.current) {
|
|
|
|
|
const activeTab = scrollContainerRef.current.querySelector('[data-active="true"]')
|
|
|
|
|
if (activeTab) {
|
|
|
|
|
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [activeFile?.path, openFiles.length])
|
|
|
|
|
|
|
|
|
|
if (openFiles.length === 0) return null
|
|
|
|
|
|
|
|
|
|
const isTitlebar = variant === 'titlebar'
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={scrollContainerRef}
|
|
|
|
|
className={clsx(
|
|
|
|
|
isTitlebar
|
|
|
|
|
? "flex overflow-hidden"
|
|
|
|
|
: "flex backdrop-blur-sm border-b border-gray-200 dark:border-gray-700/60 overflow-hidden",
|
|
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: isTitlebar ? 'transparent' : `rgba(var(--app-tabbar-bg-rgb), ${opacity})`,
|
|
|
|
|
'--tab-opacity': opacity,
|
|
|
|
|
...style
|
|
|
|
|
} as React.CSSProperties}
|
|
|
|
|
>
|
|
|
|
|
{openFiles.map((file) => {
|
|
|
|
|
const isActive = activeFile?.path === file.path
|
|
|
|
|
const isOnlyHomeTab = openFiles.length === 1 && file.path === HOME_TAB_ID
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={file.path}
|
|
|
|
|
data-active={isActive}
|
|
|
|
|
onClick={() => onTabClick(file)}
|
|
|
|
|
onContextMenu={(e) => handleContextMenu(e, file)}
|
|
|
|
|
className={clsx(
|
|
|
|
|
"group relative flex items-center px-3 min-w-[80px] max-w-[240px] text-sm cursor-pointer border-r border-gray-200 dark:border-gray-700/60 select-none rounded-t-md flex-1 titlebar-no-drag",
|
|
|
|
|
isTitlebar ? "h-full" : "h-10",
|
|
|
|
|
isActive
|
|
|
|
|
? "backdrop-blur-md text-gray-800 dark:text-gray-200 font-medium border-t-2 border-t-gray-400 dark:border-t-gray-500"
|
|
|
|
|
: "text-gray-600 dark:text-gray-300 hover:bg-gray-100/[var(--tab-opacity)] dark:hover:bg-gray-700/[var(--tab-opacity)] border-t-2 border-t-transparent"
|
|
|
|
|
)}
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: isActive
|
|
|
|
|
? `rgba(var(--app-tab-active-bg-rgb), ${opacity * 0.55})`
|
|
|
|
|
: `rgba(var(--app-tab-inactive-bg-rgb), ${opacity * 0.65})`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span className={clsx(
|
|
|
|
|
"w-full overflow-hidden whitespace-nowrap",
|
|
|
|
|
!isOnlyHomeTab && "pr-4"
|
|
|
|
|
)} style={{
|
|
|
|
|
maskImage: 'linear-gradient(to right, black calc(100% - 20px), transparent 100%)',
|
|
|
|
|
WebkitMaskImage: 'linear-gradient(to right, black calc(100% - 20px), transparent 100%)'
|
|
|
|
|
}}>{file.name}</span>
|
|
|
|
|
{!isOnlyHomeTab && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => onTabClose(file, e)}
|
|
|
|
|
className={clsx(
|
|
|
|
|
"absolute right-2 p-0.5 rounded-sm opacity-0 group-hover:opacity-100 transition-opacity bg-inherit",
|
|
|
|
|
isActive
|
|
|
|
|
? "opacity-100 hover:bg-gray-100/[var(--tab-opacity)] dark:hover:bg-gray-700/[var(--tab-opacity)] text-gray-500 dark:text-gray-300"
|
|
|
|
|
: "hover:bg-gray-200/[var(--tab-opacity)] dark:hover:bg-gray-600/[var(--tab-opacity)] text-gray-400 dark:text-gray-400"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
<ContextMenu
|
|
|
|
|
items={contextMenuItems}
|
|
|
|
|
isOpen={contextMenu.isOpen}
|
|
|
|
|
position={contextMenu.position}
|
|
|
|
|
onClose={handleCloseContextMenu}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|