mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
feat: add tab drag-to-reorder and complete context menu actions
- Tab drag-and-drop reordering with visual drop indicator - Add moveTab action to tabStore with localStorage persistence - Context menu: add "Close Left" and "Close All" options - Add i18n translations for new menu items (en/zh) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c4a1e8994
commit
2c19bb77ac
@ -12,11 +12,15 @@ export function TabBar() {
|
||||
const closeTab = useTabStore((s) => s.closeTab)
|
||||
const disconnectSession = useChatStore((s) => s.disconnectSession)
|
||||
|
||||
const moveTab = useTabStore((s) => s.moveTab)
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
||||
const [canScrollRight, setCanScrollRight] = useState(false)
|
||||
const [contextMenu, setContextMenu] = useState<{ sessionId: string; x: number; y: number } | null>(null)
|
||||
const [closingTabId, setClosingTabId] = useState<string | null>(null)
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
|
||||
const dragIndexRef = useRef<number | null>(null)
|
||||
const t = useTranslation()
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
@ -86,6 +90,16 @@ export function TabBar() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseLeft = (sessionId: string) => {
|
||||
setContextMenu(null)
|
||||
const idx = tabs.findIndex((t) => t.sessionId === sessionId)
|
||||
const leftIds = tabs.slice(0, idx).map((t) => t.sessionId)
|
||||
for (const id of leftIds) {
|
||||
disconnectSession(id)
|
||||
closeTab(id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseRight = (sessionId: string) => {
|
||||
setContextMenu(null)
|
||||
const idx = tabs.findIndex((t) => t.sessionId === sessionId)
|
||||
@ -96,6 +110,41 @@ export function TabBar() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseAll = () => {
|
||||
setContextMenu(null)
|
||||
const allIds = tabs.map((t) => t.sessionId)
|
||||
for (const id of allIds) {
|
||||
disconnectSession(id)
|
||||
closeTab(id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragStart = (index: number) => {
|
||||
dragIndexRef.current = index
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||
e.preventDefault()
|
||||
if (dragIndexRef.current === null || dragIndexRef.current === index) {
|
||||
setDragOverIndex(null)
|
||||
return
|
||||
}
|
||||
setDragOverIndex(index)
|
||||
}
|
||||
|
||||
const handleDrop = (index: number) => {
|
||||
if (dragIndexRef.current !== null && dragIndexRef.current !== index) {
|
||||
moveTab(dragIndexRef.current, index)
|
||||
}
|
||||
dragIndexRef.current = null
|
||||
setDragOverIndex(null)
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
dragIndexRef.current = null
|
||||
setDragOverIndex(null)
|
||||
}
|
||||
|
||||
if (tabs.length === 0) return null
|
||||
|
||||
return (
|
||||
@ -107,15 +156,20 @@ export function TabBar() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div ref={scrollRef} className="flex-1 flex items-center overflow-x-hidden" data-tauri-drag-region>
|
||||
{tabs.map((tab) => (
|
||||
<div ref={scrollRef} className="flex-1 flex items-center overflow-x-hidden" data-tauri-drag-region onDragOver={(e) => e.preventDefault()}>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabItem
|
||||
key={tab.sessionId}
|
||||
tab={tab}
|
||||
isActive={tab.sessionId === activeTabId}
|
||||
isDragOver={dragOverIndex === index}
|
||||
onClick={() => setActiveTab(tab.sessionId)}
|
||||
onClose={() => handleClose(tab.sessionId)}
|
||||
onContextMenu={(e) => handleContextMenu(e, tab.sessionId)}
|
||||
onDragStart={() => handleDragStart(index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={() => handleDrop(index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -143,12 +197,25 @@ export function TabBar() {
|
||||
>
|
||||
{t('tabs.closeOthers')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCloseLeft(contextMenu.sessionId)}
|
||||
className="w-full px-3 py-1.5 text-xs text-left text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
{t('tabs.closeLeft')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCloseRight(contextMenu.sessionId)}
|
||||
className="w-full px-3 py-1.5 text-xs text-left text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
{t('tabs.closeRight')}
|
||||
</button>
|
||||
<div className="my-1 border-t border-[var(--color-border)]" />
|
||||
<button
|
||||
onClick={handleCloseAll}
|
||||
className="w-full px-3 py-1.5 text-xs text-left text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
{t('tabs.closeAll')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -186,23 +253,34 @@ export function TabBar() {
|
||||
)
|
||||
}
|
||||
|
||||
function TabItem({ tab, isActive, onClick, onClose, onContextMenu }: {
|
||||
function TabItem({ tab, isActive, isDragOver, onClick, onClose, onContextMenu, onDragStart, onDragOver, onDrop, onDragEnd }: {
|
||||
tab: Tab
|
||||
isActive: boolean
|
||||
isDragOver: boolean
|
||||
onClick: () => void
|
||||
onClose: () => void
|
||||
onContextMenu: (e: React.MouseEvent) => void
|
||||
onDragStart: () => void
|
||||
onDragOver: (e: React.DragEvent) => void
|
||||
onDrop: () => void
|
||||
onDragEnd: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onDragEnd={onDragEnd}
|
||||
className={`
|
||||
flex-shrink-0 flex items-center gap-1.5 px-3 cursor-pointer group transition-colors
|
||||
flex-shrink-0 flex items-center gap-1.5 px-3 cursor-pointer group transition-colors relative
|
||||
${isActive
|
||||
? 'h-[37px] bg-[var(--color-surface)] border-t-2 border-t-[var(--color-brand)]'
|
||||
: 'h-[37px] bg-transparent hover:bg-[var(--color-surface-hover)]'
|
||||
}
|
||||
${isDragOver ? 'before:absolute before:left-0 before:top-[6px] before:bottom-[6px] before:w-[2px] before:bg-[var(--color-brand)] before:rounded-full' : ''}
|
||||
`}
|
||||
style={{ width: TAB_WIDTH, maxWidth: TAB_WIDTH }}
|
||||
>
|
||||
|
||||
@ -438,7 +438,9 @@ export const en = {
|
||||
// ─── Tabs ──────────────────────────────────────
|
||||
'tabs.close': 'Close',
|
||||
'tabs.closeOthers': 'Close Others',
|
||||
'tabs.closeLeft': 'Close to the Left',
|
||||
'tabs.closeRight': 'Close to the Right',
|
||||
'tabs.closeAll': 'Close All',
|
||||
'tabs.closeConfirmTitle': 'Session Running',
|
||||
'tabs.closeConfirmMessage': 'This session is still running. What would you like to do?',
|
||||
'tabs.closeConfirmKeep': 'Keep Running',
|
||||
|
||||
@ -440,7 +440,9 @@ export const zh: Record<TranslationKey, string> = {
|
||||
// ─── Tabs ──────────────────────────────────────
|
||||
'tabs.close': '关闭',
|
||||
'tabs.closeOthers': '关闭其他',
|
||||
'tabs.closeLeft': '关闭左侧',
|
||||
'tabs.closeRight': '关闭右侧',
|
||||
'tabs.closeAll': '关闭所有',
|
||||
'tabs.closeConfirmTitle': '会话运行中',
|
||||
'tabs.closeConfirmMessage': '此会话仍在运行,你想要怎么做?',
|
||||
'tabs.closeConfirmKeep': '保持运行',
|
||||
|
||||
@ -29,6 +29,7 @@ type TabStore = {
|
||||
setActiveTab: (sessionId: string) => void
|
||||
updateTabTitle: (sessionId: string, title: string) => void
|
||||
updateTabStatus: (sessionId: string, status: Tab['status']) => void
|
||||
moveTab: (fromIndex: number, toIndex: number) => void
|
||||
|
||||
saveTabs: () => void
|
||||
restoreTabs: () => Promise<void>
|
||||
@ -92,6 +93,17 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
}))
|
||||
},
|
||||
|
||||
moveTab: (fromIndex, toIndex) => {
|
||||
if (fromIndex === toIndex) return
|
||||
const { tabs } = get()
|
||||
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length) return
|
||||
const newTabs = [...tabs]
|
||||
const [moved] = newTabs.splice(fromIndex, 1)
|
||||
newTabs.splice(toIndex, 0, moved!)
|
||||
set({ tabs: newTabs })
|
||||
get().saveTabs()
|
||||
},
|
||||
|
||||
saveTabs: () => {
|
||||
const { tabs, activeTabId } = get()
|
||||
const data: TabPersistence = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user