From aec704280a2f3829be06e21b11bbcfb4935383c4 Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Mon, 28 Jul 2025 23:07:46 +0800 Subject: [PATCH 1/2] feat: implement comprehensive tab drag and drop functionality --- src/main/presenter/tabPresenter.ts | 35 ++++ src/renderer/shell/components/AppBar.vue | 202 ++++++++++++++++++++++- src/renderer/shell/stores/tab.ts | 17 +- src/shared/presenter.d.ts | 1 + 4 files changed, 248 insertions(+), 7 deletions(-) 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..3d9a6abf7 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,195 @@ 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() + + if (dragInsertIndex.value === -1) { + dragInsertIndex.value = -1 + return + } + + // 获取拖拽的标签页ID + const draggedTabIdFromEvent = event.dataTransfer?.getData('text/plain') + const finalDraggedTabId = + draggedTabId || (draggedTabIdFromEvent ? parseInt(draggedTabIdFromEvent) : null) + + if (!finalDraggedTabId) { + dragInsertIndex.value = -1 + return + } + + const currentWindowId = window.api.getWindowId() + if (!currentWindowId) { + dragInsertIndex.value = -1 + return + } + + // 检查是否是当前窗口的标签页 + const isFromCurrentWindow = tabStore.tabs.some((tab) => tab.id === finalDraggedTabId) + + if (isFromCurrentWindow) { + // 窗口内重排序 + const draggedTabIndex = tabStore.tabs.findIndex((tab) => tab.id === finalDraggedTabId) + if (draggedTabIndex === -1) { + dragInsertIndex.value = -1 + return + } + + let targetIndex = dragInsertIndex.value + + // 如果拖拽到原位置,不需要重排序 + if (targetIndex === draggedTabIndex || targetIndex === draggedTabIndex + 1) { + dragInsertIndex.value = -1 + 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) + await tabStore.reorderTabs(newTabIds) + } 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') + } + } + + // 清理拖拽状态 + dragInsertIndex.value = -1 +} + +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..8e817bbcf 100644 --- a/src/renderer/shell/stores/tab.ts +++ b/src/renderer/shell/stores/tab.ts @@ -50,6 +50,20 @@ export const useTabStore = defineStore('tab', () => { currentTabId.value = id } + const reorderTabs = async (newTabIds: number[]) => { + const windowId = window.api.getWindowId() + if (!windowId) return false + + 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[] + tabs.value.splice(0, tabs.value.length, ...reorderedTabs) + } + return success + } + const updateWindowTabs = (windowId: number, tabsData: TabData[]) => { console.log('updateWindowTabs', windowId, tabsData) tabs.value = tabsData @@ -111,6 +125,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, From a6f0d272547777533c5cdad976d788eb587b54e4 Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Mon, 28 Jul 2025 23:22:56 +0800 Subject: [PATCH 2/2] fix: improve error handling and state management in tab drag and drop --- src/renderer/shell/components/AppBar.vue | 116 +++++++++++++---------- src/renderer/shell/stores/tab.ts | 26 +++-- 2 files changed, 83 insertions(+), 59 deletions(-) diff --git a/src/renderer/shell/components/AppBar.vue b/src/renderer/shell/components/AppBar.vue index 3d9a6abf7..f41247998 100644 --- a/src/renderer/shell/components/AppBar.vue +++ b/src/renderer/shell/components/AppBar.vue @@ -273,8 +273,14 @@ const onTabContainerDragOver = (event: DragEvent) => { const onTabContainerDrop = async (event: DragEvent) => { event.preventDefault() - if (dragInsertIndex.value === -1) { + // Helper to reset drag state + const resetDragState = () => { dragInsertIndex.value = -1 + dragInsertPosition.value = 0 + } + + if (dragInsertIndex.value === -1) { + resetDragState() return } @@ -284,74 +290,80 @@ const onTabContainerDrop = async (event: DragEvent) => { draggedTabId || (draggedTabIdFromEvent ? parseInt(draggedTabIdFromEvent) : null) if (!finalDraggedTabId) { - dragInsertIndex.value = -1 + resetDragState() return } const currentWindowId = window.api.getWindowId() if (!currentWindowId) { - dragInsertIndex.value = -1 + resetDragState() return } - // 检查是否是当前窗口的标签页 - const isFromCurrentWindow = tabStore.tabs.some((tab) => tab.id === finalDraggedTabId) + try { + // 检查是否是当前窗口的标签页 + const isFromCurrentWindow = tabStore.tabs.some((tab) => tab.id === finalDraggedTabId) - if (isFromCurrentWindow) { - // 窗口内重排序 - const draggedTabIndex = tabStore.tabs.findIndex((tab) => tab.id === finalDraggedTabId) - if (draggedTabIndex === -1) { - dragInsertIndex.value = -1 - return - } + if (isFromCurrentWindow) { + // 窗口内重排序 + const draggedTabIndex = tabStore.tabs.findIndex((tab) => tab.id === finalDraggedTabId) + if (draggedTabIndex === -1) { + resetDragState() + return + } - let targetIndex = dragInsertIndex.value + let targetIndex = dragInsertIndex.value - // 如果拖拽到原位置,不需要重排序 - if (targetIndex === draggedTabIndex || targetIndex === draggedTabIndex + 1) { - dragInsertIndex.value = -1 - return - } + // 如果拖拽到原位置,不需要重排序 + if (targetIndex === draggedTabIndex || targetIndex === draggedTabIndex + 1) { + resetDragState() + return + } - // 调整目标索引(如果拖拽到后面的位置,需要减1) - if (targetIndex > draggedTabIndex) { - targetIndex -= 1 - } + // 调整目标索引(如果拖拽到后面的位置,需要减1) + if (targetIndex > draggedTabIndex) { + targetIndex -= 1 + } - // 创建新的标签页顺序 - const newTabs = [...tabStore.tabs] - const [draggedTab] = newTabs.splice(draggedTabIndex, 1) - newTabs.splice(targetIndex, 0, draggedTab) + // 创建新的标签页顺序 + const newTabs = [...tabStore.tabs] + const [draggedTab] = newTabs.splice(draggedTabIndex, 1) + newTabs.splice(targetIndex, 0, draggedTab) - // 调用后端重排序方法,同步到主进程 - const newTabIds = newTabs.map((tab) => tab.id) - await tabStore.reorderTabs(newTabIds) - } 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') + // 调用后端重排序方法,同步到主进程 + const newTabIds = newTabs.map((tab) => tab.id) + const success = await tabStore.reorderTabs(newTabIds) + if (!success) { + console.error('Failed to reorder tabs') + } } else { - console.error('Failed to move tab') + // 跨窗口拖拽 + 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() } - - // 清理拖拽状态 - dragInsertIndex.value = -1 } const handleDragOver = (event: DragEvent) => { diff --git a/src/renderer/shell/stores/tab.ts b/src/renderer/shell/stores/tab.ts index 8e817bbcf..cd08eee7a 100644 --- a/src/renderer/shell/stores/tab.ts +++ b/src/renderer/shell/stores/tab.ts @@ -54,14 +54,26 @@ export const useTabStore = defineStore('tab', () => { const windowId = window.api.getWindowId() if (!windowId) return false - 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[] - tabs.value.splice(0, tabs.value.length, ...reorderedTabs) + 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 } - return success } const updateWindowTabs = (windowId: number, tabsData: TabData[]) => {