diff --git a/src/main/presenter/tabPresenter.ts b/src/main/presenter/tabPresenter.ts index 12571337d..158d1c120 100644 --- a/src/main/presenter/tabPresenter.ts +++ b/src/main/presenter/tabPresenter.ts @@ -688,6 +688,41 @@ export class TabPresenter implements ITabPresenter { this.webContentsToTabId.clear() } + /** + * 重排序窗口内的标签页 + */ + async reorderTabs(windowId: number, tabIds: number[]): Promise { + console.log('reorderTabs', windowId, tabIds) + + const windowTabs = this.windowTabs.get(windowId) + if (!windowTabs) return false + + for (const tabId of tabIds) { + if (!windowTabs.includes(tabId)) { + console.warn(`Tab ${tabId} does not belong to window ${windowId}`) + return false + } + } + + if (tabIds.length !== windowTabs.length) { + console.warn('Tab count mismatch in reorder operation') + return false + } + + this.windowTabs.set(windowId, [...tabIds]) + + tabIds.forEach((tabId, index) => { + const tabState = this.tabState.get(tabId) + if (tabState) { + tabState.position = index + } + }) + + await this.notifyWindowTabsUpdate(windowId) + + return true + } + // 将标签页移动到新窗口 async moveTabToNewWindow(tabId: number, screenX?: number, screenY?: number): Promise { const tabInfo = this.tabState.get(tabId) diff --git a/src/renderer/shell/components/AppBar.vue b/src/renderer/shell/components/AppBar.vue index 38310e27a..f41247998 100644 --- a/src/renderer/shell/components/AppBar.vue +++ b/src/renderer/shell/components/AppBar.vue @@ -26,7 +26,12 @@ class="h-full flex flex-row items-center justify-start overflow-y-hidden overflow-x-auto scrollbar-hide" @scroll="onTabContainerWrapperScroll" > -
+
{{ tab.title ?? 'DeepChat' }} + +
@@ -134,6 +146,8 @@ const tabContainerWrapper = ref(null) const tabContainer = ref(null) let draggedTabId: number | null = null +const dragInsertIndex = ref(-1) +const dragInsertPosition = ref(0) const tabContainerWrapperSize = useElementSize(tabContainerWrapper) const tabContainerSize = useElementSize(tabContainer) @@ -169,7 +183,7 @@ const onTabDragStart = (tabId: number, event: DragEvent) => { if (event.dataTransfer) { event.dataTransfer.setData('text/plain', tabId.toString()) - event.dataTransfer.effectAllowed = 'none' + event.dataTransfer.effectAllowed = 'all' // 允许所有拖拽效果,动态判断 draggedTabId = tabId console.log('onTabDragStart - Tab ID:', tabId, 'Name:', tab.title) @@ -203,19 +217,207 @@ const onTabDragStart = (tabId: number, event: DragEvent) => { } } -const handleDragOver = (event: DragEvent) => { +// 标签页项目拖拽悬停处理(窗口内重排序) +const onTabItemDragOver = (index: number, event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + // 检查是否是当前窗口的标签页拖拽 + const isCurrentWindowDrag = draggedTabId !== null + // 检查是否是外部拖拽(跨窗口) + const isExternalDrag = !isCurrentWindowDrag && event.dataTransfer?.types.includes('text/plain') + + if (!isCurrentWindowDrag && !isExternalDrag) return + + // 窗口内拖拽使用 move + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move' + } + + // 计算插入位置 + const tabElement = event.currentTarget as HTMLElement + const rect = tabElement.getBoundingClientRect() + const containerRect = tabContainer.value?.getBoundingClientRect() + + if (containerRect) { + const mouseX = event.clientX + const tabCenterX = rect.left + rect.width / 2 + + // 判断插入到左侧还是右侧 + if (mouseX < tabCenterX) { + dragInsertIndex.value = index + dragInsertPosition.value = rect.left - containerRect.left + } else { + dragInsertIndex.value = index + 1 + dragInsertPosition.value = rect.right - containerRect.left + } + } +} + +// 标签页容器拖拽悬停处理 +const onTabContainerDragOver = (event: DragEvent) => { + // 检查是否是当前窗口的标签页拖拽或外部拖拽 + const isCurrentWindowDrag = draggedTabId !== null + const isExternalDrag = !isCurrentWindowDrag && event.dataTransfer?.types.includes('text/plain') + + if (!isCurrentWindowDrag && !isExternalDrag) return + event.preventDefault() + // 设置正确的 dropEffect 以支持窗口内拖拽 + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move' + } +} + +// 标签页容器放置处理(窗口内重排序和跨窗口拖拽) +const onTabContainerDrop = async (event: DragEvent) => { + event.preventDefault() + + // Helper to reset drag state + const resetDragState = () => { + dragInsertIndex.value = -1 + dragInsertPosition.value = 0 + } + + if (dragInsertIndex.value === -1) { + resetDragState() + return + } + + // 获取拖拽的标签页ID + const draggedTabIdFromEvent = event.dataTransfer?.getData('text/plain') + const finalDraggedTabId = + draggedTabId || (draggedTabIdFromEvent ? parseInt(draggedTabIdFromEvent) : null) + + if (!finalDraggedTabId) { + resetDragState() + return + } + + const currentWindowId = window.api.getWindowId() + if (!currentWindowId) { + resetDragState() + return + } + + try { + // 检查是否是当前窗口的标签页 + const isFromCurrentWindow = tabStore.tabs.some((tab) => tab.id === finalDraggedTabId) + + if (isFromCurrentWindow) { + // 窗口内重排序 + const draggedTabIndex = tabStore.tabs.findIndex((tab) => tab.id === finalDraggedTabId) + if (draggedTabIndex === -1) { + resetDragState() + return + } + + let targetIndex = dragInsertIndex.value + + // 如果拖拽到原位置,不需要重排序 + if (targetIndex === draggedTabIndex || targetIndex === draggedTabIndex + 1) { + resetDragState() + return + } + + // 调整目标索引(如果拖拽到后面的位置,需要减1) + if (targetIndex > draggedTabIndex) { + targetIndex -= 1 + } + + // 创建新的标签页顺序 + const newTabs = [...tabStore.tabs] + const [draggedTab] = newTabs.splice(draggedTabIndex, 1) + newTabs.splice(targetIndex, 0, draggedTab) + + // 调用后端重排序方法,同步到主进程 + const newTabIds = newTabs.map((tab) => tab.id) + const success = await tabStore.reorderTabs(newTabIds) + if (!success) { + console.error('Failed to reorder tabs') + } + } else { + // 跨窗口拖拽 + console.log( + 'Cross-window drag detected:', + finalDraggedTabId, + 'to window:', + currentWindowId, + 'at index:', + dragInsertIndex.value + ) + + // 调用主进程的 moveTab 方法 + const success = await tabPresenter.moveTab( + finalDraggedTabId, + currentWindowId, + dragInsertIndex.value + ) + if (success) { + console.log('Tab moved successfully') + } else { + console.error('Failed to move tab') + } + } + } catch (error) { + console.error('Error during tab drop operation:', error) + } finally { + resetDragState() + } +} + +const handleDragOver = (event: DragEvent) => { + // 只处理当前窗口的标签页拖拽 + if (!draggedTabId) return + + // 检查鼠标是否在标签页容器区域内 + const containerRect = tabContainer.value?.getBoundingClientRect() + if (containerRect) { + const isOverTabContainer = + event.clientX >= containerRect.left && + event.clientX <= containerRect.right && + event.clientY >= containerRect.top && + event.clientY <= containerRect.bottom + + if (isOverTabContainer) { + // 在标签页区域内,允许拖拽 + event.preventDefault() + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move' + } + } else { + // 在标签页区域外,设置为 none 以支持拖拽到窗口外 + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'none' + } + } + } } const handleDragEnd = async (event: DragEvent) => { - console.log('handleDragEnd', event.clientX, event.clientY, window.innerWidth, window.innerHeight) + console.log( + 'handleDragEnd', + event.clientX, + event.clientY, + window.innerWidth, + window.innerHeight, + 'dropEffect:', + event.dataTransfer?.dropEffect + ) + + // 清理拖拽状态 + dragInsertIndex.value = -1 + if (tabStore.tabs.length <= 1) { event.preventDefault() + draggedTabId = null return } + + // 检查是否拖拽到窗口外创建新窗口 + // 当 dropEffect 为 'none' 时,说明没有有效的放置目标 if (draggedTabId && event.dataTransfer?.dropEffect === 'none') { - // Check if the mouse is outside the window bounds - // This is a simplified check; more robust checking might be needed + // Check if the mouse is outside the window bounds or in non-droppable area const isOutsideWindow = event.clientX <= 0 || event.clientY <= 0 || diff --git a/src/renderer/shell/stores/tab.ts b/src/renderer/shell/stores/tab.ts index e5e5f0cff..cd08eee7a 100644 --- a/src/renderer/shell/stores/tab.ts +++ b/src/renderer/shell/stores/tab.ts @@ -50,6 +50,32 @@ export const useTabStore = defineStore('tab', () => { currentTabId.value = id } + const reorderTabs = async (newTabIds: number[]) => { + const windowId = window.api.getWindowId() + if (!windowId) return false + + try { + const success = await tabPresenter.reorderTabs(windowId, newTabIds) + if (success) { + const reorderedTabs = newTabIds + .map((id) => tabs.value.find((tab) => tab.id === id)) + .filter(Boolean) as TabData[] + + // Validate that all tabs were found + if (reorderedTabs.length !== newTabIds.length) { + console.warn('Some tab IDs were not found during reorder operation') + return false + } + + tabs.value.splice(0, tabs.value.length, ...reorderedTabs) + } + return success + } catch (error) { + console.error('Failed to reorder tabs:', error) + return false + } + } + const updateWindowTabs = (windowId: number, tabsData: TabData[]) => { console.log('updateWindowTabs', windowId, tabsData) tabs.value = tabsData @@ -111,6 +137,7 @@ export const useTabStore = defineStore('tab', () => { currentTabId, addTab, removeTab, - setCurrentTabId + setCurrentTabId, + reorderTabs } }) diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index 8f80dca69..37229c7ec 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -188,6 +188,7 @@ export interface ITabPresenter { getActiveTabId(windowId: number): Promise getTabIdByWebContentsId(webContentsId: number): number | undefined getWindowIdByWebContentsId(webContentsId: number): number | undefined + reorderTabs(windowId: number, tabIds: number[]): Promise moveTabToNewWindow(tabId: number, screenX?: number, screenY?: number): Promise captureTabArea( tabId: number,