mirror of
https://github.com/kuaifan/dootask.git
synced 2026-03-17 19:23:26 +00:00
feat: 支持 Tab 拖拽排序和拖出窗口创建独立窗口
实现类似 Chrome 的 Tab 管理功能: - 添加多窗口 Tab 管理器 (webTabWindows Map) - 支持 Tab 拖拽排序(使用 SortableJS) - 支持将 Tab 拖出窗口边界创建独立窗口 - 添加 Tab 转移函数 transferTab 实现跨窗口 Tab 迁移 - 前端添加拖拽检测和分离逻辑 - 添加拖拽排序相关 CSS 样式
This commit is contained in:
parent
5a4e51d1e0
commit
fbbaff55f3
485
electron/electron.js
vendored
485
electron/electron.js
vendored
@ -85,6 +85,9 @@ let mediaType = null,
|
||||
webTabHeight = 40,
|
||||
webTabClosedByShortcut = false;
|
||||
|
||||
// 多窗口 Tab 管理器
|
||||
let webTabWindows = new Map(); // windowId -> { id, window, tabs: [{id, view, url, title, icon}], activeTabId }
|
||||
|
||||
// 开发模式路径
|
||||
let devloadPath = path.resolve(__dirname, ".devload");
|
||||
|
||||
@ -735,6 +738,412 @@ function createMediaWindow(args, type = 'image') {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 webContents 获取所属的 webTabWindow ID
|
||||
* @param {WebContents} sender - 发送者的 webContents
|
||||
* @returns {number|null} 窗口 ID 或 null
|
||||
*/
|
||||
function getWebTabWindowId(sender) {
|
||||
const win = BrowserWindow.fromWebContents(sender)
|
||||
if (win && webTabWindows.has(win.id)) {
|
||||
return win.id
|
||||
}
|
||||
for (const [id, data] of webTabWindows) {
|
||||
if (data.window.webContents === sender) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 webTabWindow 数据
|
||||
* @param {number} windowId - 窗口 ID
|
||||
* @returns {object|null} 窗口数据或 null
|
||||
*/
|
||||
function getWebTabWindowData(windowId) {
|
||||
return webTabWindows.get(windowId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定窗口中查找 Tab
|
||||
* @param {number} windowId - 窗口 ID
|
||||
* @param {number} tabId - Tab ID
|
||||
* @returns {object|null} Tab 数据或 null
|
||||
*/
|
||||
function findTabInWindow(windowId, tabId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (!windowData) return null
|
||||
return windowData.tabs.find(t => t.id === tabId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定位置创建新的 Tab 窗口(用于 Tab 分离)
|
||||
* @param {object} position - {x, y} 屏幕坐标
|
||||
* @param {object} tabData - 初始 Tab 数据
|
||||
* @returns {object} 新窗口数据
|
||||
*/
|
||||
function createWebTabWindowAt(position, tabData) {
|
||||
const titleBarOverlay = {
|
||||
height: webTabHeight
|
||||
}
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
titleBarOverlay.color = '#3B3B3D'
|
||||
titleBarOverlay.symbolColor = '#C5C5C5'
|
||||
}
|
||||
|
||||
const newWindow = new BrowserWindow({
|
||||
x: Math.max(0, position.x - 640),
|
||||
y: Math.max(0, position.y - 30),
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
}
|
||||
})
|
||||
|
||||
const windowId = newWindow.id
|
||||
|
||||
// 复用窗口关闭快捷键处理
|
||||
const originalClose = newWindow.close
|
||||
newWindow.close = function() {
|
||||
webTabClosedByShortcut = true
|
||||
return originalClose.apply(this, arguments)
|
||||
}
|
||||
|
||||
newWindow.on('resize', () => {
|
||||
resizeWebTabInWindow(windowId, 0)
|
||||
})
|
||||
|
||||
newWindow.on('enter-full-screen', () => {
|
||||
utils.onDispatchEvent(newWindow.webContents, {
|
||||
event: 'enter-full-screen',
|
||||
}).then(_ => { })
|
||||
})
|
||||
|
||||
newWindow.on('leave-full-screen', () => {
|
||||
utils.onDispatchEvent(newWindow.webContents, {
|
||||
event: 'leave-full-screen',
|
||||
}).then(_ => { })
|
||||
})
|
||||
|
||||
newWindow.on('close', event => {
|
||||
if (webTabClosedByShortcut) {
|
||||
webTabClosedByShortcut = false
|
||||
if (!willQuitApp) {
|
||||
closeWebTabInWindow(windowId, 0)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
userConf.set(`webTabWindow_${windowId}`, newWindow.getBounds())
|
||||
})
|
||||
|
||||
newWindow.on('closed', () => {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (windowData) {
|
||||
windowData.tabs.forEach(({view}) => {
|
||||
try {
|
||||
view.webContents.close()
|
||||
} catch (e) {}
|
||||
})
|
||||
}
|
||||
webTabWindows.delete(windowId)
|
||||
|
||||
// 同步更新旧的 webTabView 数组
|
||||
if (windowData) {
|
||||
windowData.tabs.forEach(tab => {
|
||||
const idx = webTabView.findIndex(t => t.id === tab.id)
|
||||
if (idx > -1) webTabView.splice(idx, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果关闭的是当前活跃窗口,切换到其他窗口
|
||||
if (webTabWindow === newWindow) {
|
||||
webTabWindow = null
|
||||
for (const [, data] of webTabWindows) {
|
||||
webTabWindow = data.window
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
newWindow.once('ready-to-show', () => {
|
||||
onShowWindow(newWindow)
|
||||
})
|
||||
|
||||
newWindow.webContents.once('dom-ready', () => {
|
||||
onShowWindow(newWindow)
|
||||
})
|
||||
|
||||
newWindow.webContents.on('before-input-event', (event, input) => {
|
||||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||||
reloadWebTabInWindow(windowId, 0)
|
||||
event.preventDefault()
|
||||
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
|
||||
webTabClosedByShortcut = true
|
||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||
devToolsWebTabInWindow(windowId, 0)
|
||||
} else {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (windowData && windowData.activeTabId) {
|
||||
const tab = windowData.tabs.find(t => t.id === windowData.activeTabId)
|
||||
if (tab) {
|
||||
navigation.handleInput(event, input, tab.view.webContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
navigation.setupWindowEvents(newWindow, () => {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (windowData && windowData.activeTabId) {
|
||||
const tab = windowData.tabs.find(t => t.id === windowData.activeTabId)
|
||||
return tab ? tab.view.webContents : null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
newWindow.loadFile('./render/tabs/index.html')
|
||||
|
||||
// 注册到多窗口管理器
|
||||
const windowData = {
|
||||
id: windowId,
|
||||
window: newWindow,
|
||||
tabs: [],
|
||||
activeTabId: null
|
||||
}
|
||||
webTabWindows.set(windowId, windowData)
|
||||
|
||||
return windowData
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Tab 从源窗口转移到目标窗口
|
||||
* @param {number} tabId - Tab 的 webContents.id
|
||||
* @param {number} fromWindowId - 源窗口 ID
|
||||
* @param {number|null} toWindowId - 目标窗口 ID(null 表示创建新窗口)
|
||||
* @param {object} position - 新窗口位置 {x, y}(屏幕坐标)
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
function transferTab(tabId, fromWindowId, toWindowId, position) {
|
||||
const sourceData = webTabWindows.get(fromWindowId)
|
||||
if (!sourceData) return false
|
||||
|
||||
const tabIndex = sourceData.tabs.findIndex(t => t.id === tabId)
|
||||
if (tabIndex === -1) return false
|
||||
|
||||
const tabData = sourceData.tabs[tabIndex]
|
||||
const view = tabData.view
|
||||
|
||||
// 1. 从源窗口移除 view
|
||||
sourceData.window.contentView.removeChildView(view)
|
||||
sourceData.tabs.splice(tabIndex, 1)
|
||||
|
||||
// 从旧的 webTabView 数组中移除
|
||||
const oldIdx = webTabView.findIndex(t => t.id === tabId)
|
||||
if (oldIdx > -1) webTabView.splice(oldIdx, 1)
|
||||
|
||||
// 2. 通知源窗口前端更新
|
||||
utils.onDispatchEvent(sourceData.window.webContents, {
|
||||
event: 'tab-removed',
|
||||
id: tabId
|
||||
}).then(_ => { })
|
||||
|
||||
// 3. 处理源窗口空 Tab 情况
|
||||
if (sourceData.tabs.length === 0) {
|
||||
userConf.set(`webTabWindow_${fromWindowId}`, sourceData.window.getBounds())
|
||||
sourceData.window.destroy()
|
||||
// webTabWindows.delete 会在 closed 事件中处理
|
||||
} else {
|
||||
// 激活源窗口的下一个 Tab
|
||||
const nextTab = sourceData.tabs[Math.min(tabIndex, sourceData.tabs.length - 1)]
|
||||
activateWebTabInWindow(fromWindowId, nextTab.id)
|
||||
}
|
||||
|
||||
// 4. 获取或创建目标窗口
|
||||
let targetData
|
||||
if (toWindowId && webTabWindows.has(toWindowId)) {
|
||||
targetData = webTabWindows.get(toWindowId)
|
||||
} else {
|
||||
targetData = createWebTabWindowAt(position, tabData)
|
||||
}
|
||||
|
||||
// 等待目标窗口 TabBar 加载完成后再添加 Tab
|
||||
const addTabToTarget = () => {
|
||||
// 5. 将 view 添加到目标窗口
|
||||
targetData.window.contentView.addChildView(view)
|
||||
targetData.tabs.push(tabData)
|
||||
|
||||
// 同步添加到旧的 webTabView 数组
|
||||
webTabView.push(tabData)
|
||||
|
||||
// 6. 调整 view 尺寸适应新窗口
|
||||
view.setBounds({
|
||||
x: 0,
|
||||
y: webTabHeight,
|
||||
width: targetData.window.getContentBounds().width || 1280,
|
||||
height: (targetData.window.getContentBounds().height || 800) - webTabHeight
|
||||
})
|
||||
view.setVisible(true)
|
||||
|
||||
// 7. 通知目标窗口前端更新
|
||||
utils.onDispatchEvent(targetData.window.webContents, {
|
||||
event: 'tab-added',
|
||||
id: tabId,
|
||||
title: tabData.title || '',
|
||||
url: tabData.url || '',
|
||||
icon: tabData.icon || ''
|
||||
}).then(_ => { })
|
||||
|
||||
// 8. 激活新添加的 Tab
|
||||
activateWebTabInWindow(targetData.id, tabId)
|
||||
|
||||
// 9. 聚焦目标窗口
|
||||
targetData.window.focus()
|
||||
}
|
||||
|
||||
// 如果是新创建的窗口,等待 dom-ready 后再添加 Tab
|
||||
if (!toWindowId) {
|
||||
targetData.window.webContents.once('dom-ready', () => {
|
||||
setTimeout(addTabToTarget, 100)
|
||||
})
|
||||
} else {
|
||||
addTabToTarget()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定窗口中调整 Tab 尺寸
|
||||
*/
|
||||
function resizeWebTabInWindow(windowId, tabId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (!windowData) return
|
||||
|
||||
const item = tabId === 0
|
||||
? windowData.tabs.find(t => t.id === windowData.activeTabId)
|
||||
: windowData.tabs.find(t => t.id === tabId)
|
||||
if (!item) return
|
||||
|
||||
item.view.setBounds({
|
||||
x: 0,
|
||||
y: webTabHeight,
|
||||
width: windowData.window.getContentBounds().width || 1280,
|
||||
height: (windowData.window.getContentBounds().height || 800) - webTabHeight
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定窗口中激活 Tab
|
||||
*/
|
||||
function activateWebTabInWindow(windowId, tabId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (!windowData) return
|
||||
|
||||
const item = tabId === 0
|
||||
? windowData.tabs[0]
|
||||
: windowData.tabs.find(t => t.id === tabId)
|
||||
if (!item) return
|
||||
|
||||
windowData.tabs.forEach(({id, view}) => {
|
||||
view.setVisible(id === item.id)
|
||||
})
|
||||
|
||||
resizeWebTabInWindow(windowId, item.id)
|
||||
item.view.webContents.focus()
|
||||
windowData.activeTabId = item.id
|
||||
|
||||
utils.onDispatchEvent(windowData.window.webContents, {
|
||||
event: 'switch',
|
||||
id: item.id
|
||||
}).then(_ => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定窗口中关闭 Tab
|
||||
*/
|
||||
function closeWebTabInWindow(windowId, tabId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (!windowData) return
|
||||
|
||||
const item = tabId === 0
|
||||
? windowData.tabs.find(t => t.id === windowData.activeTabId)
|
||||
: windowData.tabs.find(t => t.id === tabId)
|
||||
if (!item) return
|
||||
|
||||
if (windowData.tabs.length === 1) {
|
||||
windowData.window.hide()
|
||||
}
|
||||
|
||||
windowData.window.contentView.removeChildView(item.view)
|
||||
try {
|
||||
item.view.webContents.close()
|
||||
} catch (e) {}
|
||||
|
||||
const index = windowData.tabs.findIndex(t => t.id === item.id)
|
||||
if (index > -1) {
|
||||
windowData.tabs.splice(index, 1)
|
||||
}
|
||||
|
||||
// 从旧数组中移除
|
||||
const oldIdx = webTabView.findIndex(t => t.id === item.id)
|
||||
if (oldIdx > -1) webTabView.splice(oldIdx, 1)
|
||||
|
||||
utils.onDispatchEvent(windowData.window.webContents, {
|
||||
event: 'close',
|
||||
id: item.id
|
||||
}).then(_ => { })
|
||||
|
||||
if (windowData.tabs.length === 0) {
|
||||
userConf.set(`webTabWindow_${windowId}`, windowData.window.getBounds())
|
||||
windowData.window.destroy()
|
||||
} else {
|
||||
activateWebTabInWindow(windowId, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定窗口中刷新 Tab
|
||||
*/
|
||||
function reloadWebTabInWindow(windowId, tabId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (!windowData) return
|
||||
|
||||
const item = tabId === 0
|
||||
? windowData.tabs.find(t => t.id === windowData.activeTabId)
|
||||
: windowData.tabs.find(t => t.id === tabId)
|
||||
if (!item) return
|
||||
|
||||
item.view.webContents.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定窗口中打开 DevTools
|
||||
*/
|
||||
function devToolsWebTabInWindow(windowId, tabId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (!windowData) return
|
||||
|
||||
const item = tabId === 0
|
||||
? windowData.tabs.find(t => t.id === windowData.activeTabId)
|
||||
: windowData.tabs.find(t => t.id === tabId)
|
||||
if (!item) return
|
||||
|
||||
item.view.webContents.openDevTools()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内置浏览器
|
||||
* @param args {url, ?}
|
||||
@ -820,6 +1229,8 @@ function createWebTabWindow(args) {
|
||||
//
|
||||
}
|
||||
})
|
||||
// 从多窗口管理器中删除
|
||||
webTabWindows.delete(webTabWindow.id)
|
||||
webTabView = []
|
||||
webTabWindow = null
|
||||
})
|
||||
@ -855,6 +1266,14 @@ function createWebTabWindow(args) {
|
||||
})
|
||||
|
||||
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => { }).catch(_ => { })
|
||||
|
||||
// 注册到多窗口管理器
|
||||
webTabWindows.set(webTabWindow.id, {
|
||||
id: webTabWindow.id,
|
||||
window: webTabWindow,
|
||||
tabs: [],
|
||||
activeTabId: null
|
||||
})
|
||||
}
|
||||
if (webTabWindow.isMinimized()) {
|
||||
webTabWindow.restore()
|
||||
@ -984,10 +1403,24 @@ function createWebTabWindow(args) {
|
||||
browserView.setVisible(true)
|
||||
|
||||
webTabWindow.contentView.addChildView(browserView)
|
||||
webTabView.push({
|
||||
|
||||
// 创建 Tab 数据对象
|
||||
const tabData = {
|
||||
id: browserView.webContents.id,
|
||||
view: browserView
|
||||
})
|
||||
view: browserView,
|
||||
url: args.url,
|
||||
title: '',
|
||||
icon: ''
|
||||
}
|
||||
|
||||
// 添加到原有数组(兼容)
|
||||
webTabView.push(tabData)
|
||||
|
||||
// 添加到多窗口管理器
|
||||
const windowData = webTabWindows.get(webTabWindow.id)
|
||||
if (windowData) {
|
||||
windowData.tabs.push(tabData)
|
||||
}
|
||||
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'create',
|
||||
@ -1086,6 +1519,13 @@ function activateWebTab(id) {
|
||||
})
|
||||
resizeWebTab(item.id)
|
||||
item.view.webContents.focus()
|
||||
|
||||
// 更新多窗口管理器的 activeTabId
|
||||
const windowData = webTabWindows.get(webTabWindow.id)
|
||||
if (windowData) {
|
||||
windowData.activeTabId = item.id
|
||||
}
|
||||
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'switch',
|
||||
id: item.id,
|
||||
@ -1111,11 +1551,21 @@ function closeWebTab(id) {
|
||||
//
|
||||
}
|
||||
|
||||
// 从原有数组中删除
|
||||
const index = webTabView.findIndex(({id}) => item.id == id)
|
||||
if (index > -1) {
|
||||
webTabView.splice(index, 1)
|
||||
}
|
||||
|
||||
// 从多窗口管理器中删除
|
||||
const windowData = webTabWindows.get(webTabWindow.id)
|
||||
if (windowData) {
|
||||
const tabIndex = windowData.tabs.findIndex(t => t.id === item.id)
|
||||
if (tabIndex > -1) {
|
||||
windowData.tabs.splice(tabIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'close',
|
||||
id: item.id,
|
||||
@ -1359,6 +1809,35 @@ ipcMain.on('webTabClose', (event, id) => {
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - Tab 排序变更
|
||||
* @param tabIds - 排序后的 Tab ID 数组
|
||||
*/
|
||||
ipcMain.on('webTabReorder', (event, { tabIds }) => {
|
||||
const windowId = getWebTabWindowId(event.sender)
|
||||
if (windowId) {
|
||||
const windowData = webTabWindows.get(windowId)
|
||||
if (windowData && tabIds) {
|
||||
windowData.tabs.sort((a, b) => tabIds.indexOf(a.id) - tabIds.indexOf(b.id))
|
||||
}
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - Tab 分离到新窗口
|
||||
* @param tabId - 要分离的 Tab ID
|
||||
* @param screenX - 屏幕 X 坐标
|
||||
* @param screenY - 屏幕 Y 坐标
|
||||
*/
|
||||
ipcMain.on('webTabDetach', (event, { tabId, screenX, screenY }) => {
|
||||
const windowId = getWebTabWindowId(event.sender)
|
||||
if (windowId && tabId) {
|
||||
transferTab(tabId, windowId, null, { x: screenX, y: screenY })
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 在外部浏览器打开
|
||||
*/
|
||||
|
||||
23
electron/render/tabs/assets/css/style.css
vendored
23
electron/render/tabs/assets/css/style.css
vendored
@ -276,3 +276,26 @@ body.darwin.full-screen .nav {
|
||||
background-image: url(../image/dark/link_normal_icon.png);
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽排序样式 */
|
||||
body.dragging .nav-tabs li {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.nav-tabs li.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
background: var(--tab-active-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-tabs li.sortable-drag {
|
||||
opacity: 1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
background: var(--tab-active-background);
|
||||
}
|
||||
|
||||
.nav-tabs li.sortable-chosen {
|
||||
background: var(--tab-active-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
2
electron/render/tabs/assets/js/Sortable.min.js
vendored
Normal file
2
electron/render/tabs/assets/js/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -5,6 +5,7 @@
|
||||
<title>Untitled</title>
|
||||
<link rel="stylesheet" href="./assets/css/style.css">
|
||||
<script src="./assets/js/vue.global.min.js"></script>
|
||||
<script src="./assets/js/Sortable.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app">
|
||||
@ -49,12 +50,18 @@
|
||||
|
||||
// 停止定时器
|
||||
stopTimer: null,
|
||||
|
||||
|
||||
// 是否可以后退
|
||||
canGoBack: false,
|
||||
|
||||
// 是否可以前进
|
||||
canGoForward: false,
|
||||
|
||||
// 拖拽相关状态
|
||||
isDragging: false,
|
||||
dragTabId: null,
|
||||
sortableInstance: null,
|
||||
dragThreshold: 50,
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
@ -155,11 +162,43 @@
|
||||
case 'leave-full-screen':
|
||||
document.body.classList.remove('full-screen')
|
||||
break
|
||||
|
||||
// Tab 被移除(转移到其他窗口)
|
||||
case 'tab-removed':
|
||||
const removeIndex = this.tabs.findIndex(item => item.id === id)
|
||||
if (removeIndex > -1) {
|
||||
this.tabs.splice(removeIndex, 1)
|
||||
// 如果移除的是当前激活的 Tab,切换到第一个
|
||||
if (this.activeId === id && this.tabs.length > 0) {
|
||||
this.activeId = this.tabs[0].id
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
// Tab 被添加(从其他窗口转移来)
|
||||
case 'tab-added':
|
||||
this.tabs.push({
|
||||
id: detail.id,
|
||||
title: detail.title || '',
|
||||
url: detail.url || '',
|
||||
icon: detail.icon || '',
|
||||
state: 'loaded'
|
||||
})
|
||||
this.activeId = detail.id
|
||||
this.$nextTick(() => {
|
||||
this.scrollTabActive()
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
window.__openDevTools = () => {
|
||||
this.sendMessage('webTabOpenDevTools')
|
||||
}
|
||||
|
||||
// 初始化拖拽排序
|
||||
this.$nextTick(() => {
|
||||
this.initSortable()
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
@ -333,6 +372,87 @@
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化拖拽排序
|
||||
*/
|
||||
initSortable() {
|
||||
const tabList = document.querySelector('.nav-tabs')
|
||||
if (!tabList || this.sortableInstance) return
|
||||
|
||||
this.sortableInstance = new Sortable(tabList, {
|
||||
animation: 150,
|
||||
delay: 100,
|
||||
delayOnTouchOnly: true,
|
||||
direction: 'horizontal',
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.tab-close',
|
||||
|
||||
onStart: (evt) => {
|
||||
this.isDragging = true
|
||||
this.dragTabId = this.tabs[evt.oldIndex]?.id
|
||||
document.body.classList.add('dragging')
|
||||
},
|
||||
|
||||
onMove: (evt, originalEvent) => {
|
||||
// 检测是否拖出窗口边界
|
||||
const y = originalEvent.clientY
|
||||
if (y < -this.dragThreshold || y > window.innerHeight + this.dragThreshold) {
|
||||
this.detachTab(originalEvent.screenX, originalEvent.screenY)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
onEnd: (evt) => {
|
||||
document.body.classList.remove('dragging')
|
||||
|
||||
if (!this.isDragging) return
|
||||
this.isDragging = false
|
||||
|
||||
// 如果是正常排序结束(位置有变化)
|
||||
if (evt.oldIndex !== evt.newIndex) {
|
||||
const [item] = this.tabs.splice(evt.oldIndex, 1)
|
||||
this.tabs.splice(evt.newIndex, 0, item)
|
||||
this.sendMessage('webTabReorder', {
|
||||
tabIds: this.tabs.map(t => t.id)
|
||||
})
|
||||
}
|
||||
|
||||
this.dragTabId = null
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 分离 Tab 到新窗口
|
||||
* @param screenX
|
||||
* @param screenY
|
||||
*/
|
||||
detachTab(screenX, screenY) {
|
||||
if (!this.dragTabId) return
|
||||
|
||||
// 发送分离请求到主进程
|
||||
this.sendMessage('webTabDetach', {
|
||||
tabId: this.dragTabId,
|
||||
screenX,
|
||||
screenY
|
||||
})
|
||||
|
||||
// 重置拖拽状态
|
||||
this.isDragging = false
|
||||
this.dragTabId = null
|
||||
document.body.classList.remove('dragging')
|
||||
|
||||
// 临时禁用 Sortable 避免状态冲突
|
||||
if (this.sortableInstance) {
|
||||
this.sortableInstance.option('disabled', true)
|
||||
setTimeout(() => {
|
||||
this.sortableInstance.option('disabled', false)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user