Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/main/presenter/tabPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,41 @@ export class TabPresenter implements ITabPresenter {
this.webContentsToTabId.clear()
}

/**
* 重排序窗口内的标签页
*/
async reorderTabs(windowId: number, tabIds: number[]): Promise<boolean> {
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<boolean> {
const tabInfo = this.tabState.get(tabId)
Expand Down
214 changes: 208 additions & 6 deletions src/renderer/shell/components/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
class="h-full flex flex-row items-center justify-start overflow-y-hidden overflow-x-auto scrollbar-hide"
@scroll="onTabContainerWrapperScroll"
>
<div ref="tabContainer" class="h-full flex flex-row items-center justify-start gap-1">
<div
ref="tabContainer"
class="h-full flex flex-row items-center justify-start gap-1 relative"
@dragover="onTabContainerDragOver"
@drop="onTabContainerDrop"
>
<AppBarTabItem
v-for="(tab, idx) in tabStore.tabs"
:key="tab.id"
Expand All @@ -37,10 +42,17 @@
@click="tabStore.setCurrentTabId(tab.id)"
@close="tabStore.removeTab(tab.id)"
@dragstart="onTabDragStart(tab.id, $event)"
@dragover="onTabItemDragOver(idx, $event)"
>
<img src="@/assets/logo.png" class="w-4 h-4 mr-2 rounded-sm" />
<span class="truncate">{{ tab.title ?? 'DeepChat' }}</span>
</AppBarTabItem>
<!-- 拖拽插入指示器 -->
<div
v-if="dragInsertIndex !== -1"
class="absolute top-0 bottom-0 w-0.5 bg-blue-500 z-10 pointer-events-none"
:style="{ left: dragInsertPosition + 'px' }"
></div>
<div ref="endOfTabs" class="w-0 flex-shrink-0 h-full"></div>
</div>
</div>
Expand Down Expand Up @@ -134,6 +146,8 @@ const tabContainerWrapper = ref<HTMLElement | null>(null)
const tabContainer = ref<HTMLElement | null>(null)

let draggedTabId: number | null = null
const dragInsertIndex = ref(-1)
const dragInsertPosition = ref(0)

const tabContainerWrapperSize = useElementSize(tabContainerWrapper)
const tabContainerSize = useElementSize(tabContainer)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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'
}
}
}
}
Comment on lines +369 to 395
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for getBoundingClientRect in handleDragOver.

Similar to the onTabItemDragOver function, this should handle potential DOM operation errors.

 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'
-      }
-    }
-  }
+  try {
+    // 检查鼠标是否在标签页容器区域内
+    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'
+        }
+      }
+    }
+  } catch (error) {
+    console.error('Error in handleDragOver:', error)
+    // Set safe default drop effect
+    if (event.dataTransfer) {
+      event.dataTransfer.dropEffect = 'none'
+    }
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 handleDragOver = (event: DragEvent) => {
// 只处理当前窗口的标签页拖拽
if (!draggedTabId) return
try {
// 检查鼠标是否在标签页容器区域内
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'
}
}
}
} catch (error) {
console.error('Error in handleDragOver:', error)
// Set safe default drop effect
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'none'
}
}
}
🤖 Prompt for AI Agents
In src/renderer/shell/components/AppBar.vue around lines 369 to 395, the
handleDragOver function calls getBoundingClientRect on tabContainer.value
without error handling, which may cause runtime errors if the DOM element is
unavailable or detached. Wrap the call to getBoundingClientRect in a try-catch
block to safely handle any exceptions, and ensure the function gracefully
handles errors by skipping the drag over logic or logging the error as
appropriate.


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 ||
Expand Down
29 changes: 28 additions & 1 deletion src/renderer/shell/stores/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,6 +137,7 @@ export const useTabStore = defineStore('tab', () => {
currentTabId,
addTab,
removeTab,
setCurrentTabId
setCurrentTabId,
reorderTabs
}
})
1 change: 1 addition & 0 deletions src/shared/presenter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export interface ITabPresenter {
getActiveTabId(windowId: number): Promise<number | undefined>
getTabIdByWebContentsId(webContentsId: number): number | undefined
getWindowIdByWebContentsId(webContentsId: number): number | undefined
reorderTabs(windowId: number, tabIds: number[]): Promise<boolean>
moveTabToNewWindow(tabId: number, screenX?: number, screenY?: number): Promise<boolean>
captureTabArea(
tabId: number,
Expand Down