feat: 添加导航功能,支持快捷键和鼠标手势操作

This commit is contained in:
kuaifan 2025-12-16 18:36:11 +08:00
parent a6385b699e
commit fee1c12357
2 changed files with 255 additions and 0 deletions

20
electron/electron.js vendored
View File

@ -43,6 +43,7 @@ const PDFDocument = require('pdf-lib').PDFDocument;
// 本地模块和配置
const utils = require('./lib/utils');
const navigation = require('./lib/navigation');
const config = require('./package.json');
const electronDown = require("./electron-down");
const electronMenu = require("./electron-menu");
@ -367,6 +368,9 @@ function createMainWindow() {
// 设置右键菜单
electronMenu.webContentsMenu(mainWindow.webContents)
// 设置导航快捷键(返回/前进)
navigation.setup(mainWindow)
// 加载地址
utils.loadUrl(mainWindow, serverUrl)
}
@ -623,6 +627,9 @@ function createChildWindow(args) {
// 设置右键菜单
electronMenu.webContentsMenu(browser.webContents)
// 设置导航快捷键(返回/前进)
navigation.setup(browser)
// 加载地址
const hash = `${args.hash || args.path}`;
if (/^https?:/i.test(hash)) {
@ -848,9 +855,20 @@ function createWebTabWindow(args) {
webTabClosedByShortcut = true
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
devToolsWebTab(0)
} else {
const item = currentWebTab()
if (item) {
navigation.handleInput(event, input, item.view.webContents)
}
}
})
// 设置鼠标侧键和触控板手势导航
navigation.setupWindowEvents(webTabWindow, () => {
const item = currentWebTab()
return item ? item.view.webContents : null
})
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => { }).catch(_ => { })
}
if (webTabWindow.isMinimized()) {
@ -962,6 +980,8 @@ function createWebTabWindow(args) {
webTabClosedByShortcut = true
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
browserView.webContents.toggleDevTools()
} else {
navigation.handleInput(event, input, browserView.webContents)
}
})

235
electron/lib/navigation.js vendored Normal file
View File

@ -0,0 +1,235 @@
/**
* 窗口导航相关工具函数
*
* 规则
* - 顶层页面是 localhost 禁用顶层 goBack/goForward
* 避免影响应用主路由历史例如 Vue hash 路由
* - 若此时焦点在 iframe 允许 iframe 自己执行 history.back()/history.forward()不影响顶层路由
*/
const utils = require('./utils')
/**
* @typedef {'back'|'forward'} NavDirection
*/
function getWebContentsUrl(webContents) {
try {
return webContents?.getURL?.() || ''
} catch (e) {
return ''
}
}
function isBackKey(input) {
return (input.alt && input.key === 'ArrowLeft') || (input.meta && input.key === '[')
}
function isForwardKey(input) {
return (input.alt && input.key === 'ArrowRight') || (input.meta && input.key === ']')
}
/**
* 尝试从 Electron 提供的 focusedFrame 获取当前聚焦 frame兼容属性/方法两种形态
* @param webContents
* @returns {Electron.WebFrameMain|null}
*/
function getFocusedFrameDirect(webContents) {
const focused = webContents?.focusedFrame
if (!focused) {
return null
}
try {
return typeof focused === 'function' ? focused.call(webContents) : focused
} catch (e) {
return null
}
}
/**
* 获取当前聚焦的 Frame优先返回更深层的 iframe
* @param webContents
* @returns {Promise<Electron.WebFrameMain|null>}
*/
async function getFocusedFrame(webContents) {
const mainFrame = webContents?.mainFrame
if (!mainFrame) {
return null
}
const direct = getFocusedFrameDirect(webContents)
if (direct) {
return direct
}
const frames = Array.isArray(mainFrame.framesInSubtree) && mainFrame.framesInSubtree.length > 0
? mainFrame.framesInSubtree
: [mainFrame]
// document.hasFocus() 可能在主文档与子 frame 同时为 true因此取“最深”的那个
const focusedList = await Promise.all(frames.map((frame) => {
if (!frame?.executeJavaScript) {
return Promise.resolve(false)
}
return frame
.executeJavaScript('document.hasFocus && document.hasFocus()')
.then(Boolean)
.catch(() => false)
}))
for (let i = focusedList.length - 1; i >= 0; i--) {
if (focusedList[i]) {
return frames[i]
}
}
return mainFrame
}
/**
* 聚焦 iframe内执行前进/后退不影响顶层路由历史
* @param webContents
* @param {NavDirection} direction
* @returns {Promise<boolean>}
*/
async function navigateFocusedSubframe(webContents, direction) {
const mainFrame = webContents?.mainFrame
if (!mainFrame) {
return false
}
const frame = await getFocusedFrame(webContents)
if (!frame || frame === mainFrame) {
return false
}
const js = direction === 'forward' ? 'history.forward()' : 'history.back()'
try {
await frame.executeJavaScript(js)
return true
} catch (e) {
return false
}
}
/**
* 尝试在顶层 webContents 上执行前进/后退
* @param webContents
* @param {NavDirection} direction
* @returns {boolean}
*/
function navigateTopLevel(webContents, direction) {
if (!webContents) {
return false
}
if (direction === 'back') {
if (!webContents.canGoBack()) {
return false
}
webContents.goBack()
return true
}
if (direction === 'forward') {
if (!webContents.canGoForward()) {
return false
}
webContents.goForward()
return true
}
return false
}
/**
* 统一导航入口
* - 顶层 intranet阻止顶层导航尝试让聚焦 iframe 自己 history.back/forward
* - 外网正常顶层导航
*
* @param event
* @param webContents
* @param {NavDirection} direction
* @returns {boolean} 是否接管了这次操作intranet 总是接管
*/
function handleDirection(event, webContents, direction) {
const url = getWebContentsUrl(webContents)
const intranet = utils.isLocalHost(url)
if (intranet) {
event?.preventDefault?.()
navigateFocusedSubframe(webContents, direction)
.catch(() => {})
return true
}
const ok = navigateTopLevel(webContents, direction)
if (ok) {
event?.preventDefault?.()
}
return ok
}
function resolveDirectionFromInput(input) {
if (isBackKey(input)) return 'back'
if (isForwardKey(input)) return 'forward'
return null
}
function resolveDirectionFromAppCommand(cmd) {
if (cmd === 'browser-backward') return 'back'
if (cmd === 'browser-forward') return 'forward'
return null
}
function resolveDirectionFromSwipe(direction) {
// macOS swipe: left = back, right = forward与当前用户验证一致
if (direction === 'left') return 'back'
if (direction === 'right') return 'forward'
return null
}
function handleInput(event, input, webContents) {
const direction = resolveDirectionFromInput(input)
if (!direction) return false
return handleDirection(event, webContents, direction)
}
function setupWindowEvents(win, getWebContents) {
if (!win) return
win.on('app-command', (event, cmd) => {
const direction = resolveDirectionFromAppCommand(cmd)
if (!direction) return
const webContents = getWebContents?.()
if (!webContents) return
handleDirection(event, webContents, direction)
})
win.on('swipe', (event, direction) => {
const navDirection = resolveDirectionFromSwipe(direction)
if (!navDirection) return
const webContents = getWebContents?.()
if (!webContents) return
handleDirection(event, webContents, navDirection)
})
}
function setup(win) {
if (!win || !win.webContents) return
win.webContents.on('before-input-event', (event, input) => {
handleInput(event, input, win.webContents)
})
setupWindowEvents(win, () => win.webContents)
}
module.exports = {
isBackKey,
isForwardKey,
getWebContentsUrl,
getFocusedFrame,
navigateFocusedSubframe,
handleInput,
setupWindowEvents,
setup,
}