feat: 添加内置浏览器导航功能

This commit is contained in:
kuaifan 2025-08-19 13:51:51 +08:00
parent d048aa33f7
commit bb83875c99
3 changed files with 294 additions and 32 deletions

94
electron/electron.js vendored
View File

@ -910,6 +910,7 @@ function createWebTabWindow(args) {
event: 'stop-loading', event: 'stop-loading',
id: browserView.webContents.id, id: browserView.webContents.id,
}).then(_ => { }) }).then(_ => { })
// 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透 // 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透
if (nativeTheme.shouldUseDarkColors) { if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#FFFFFF') browserView.setBackgroundColor('#FFFFFF')
@ -1320,6 +1321,99 @@ ipcMain.on('webTabDestroyAll', (event) => {
event.returnValue = "ok" event.returnValue = "ok"
}) })
/**
* 内置浏览器 - 后退
*/
ipcMain.on('webTabGoBack', (event) => {
const item = currentWebTab()
if (!item) {
return
}
if (item.view.webContents.canGoBack()) {
item.view.webContents.goBack()
// 导航后更新状态
setTimeout(() => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack: item.view.webContents.canGoBack(),
canGoForward: item.view.webContents.canGoForward()
}).then(_ => { })
}, 100)
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 前进
*/
ipcMain.on('webTabGoForward', (event) => {
const item = currentWebTab()
if (!item) {
return
}
if (item.view.webContents.canGoForward()) {
item.view.webContents.goForward()
// 导航后更新状态
setTimeout(() => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack: item.view.webContents.canGoBack(),
canGoForward: item.view.webContents.canGoForward()
}).then(_ => { })
}, 100)
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 刷新
*/
ipcMain.on('webTabReload', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.reload()
// 刷新完成后会触发 did-stop-loading 事件,在那里会更新导航状态
event.returnValue = "ok"
})
/**
* 内置浏览器 - 停止加载
*/
ipcMain.on('webTabStop', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.stop()
event.returnValue = "ok"
})
/**
* 内置浏览器 - 获取导航状态
*/
ipcMain.on('webTabGetNavigationState', (event) => {
const item = currentWebTab()
if (!item) {
return
}
const canGoBack = item.view.webContents.canGoBack()
const canGoForward = item.view.webContents.canGoForward()
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack,
canGoForward
}).then(_ => { })
event.returnValue = "ok"
})
/** /**
* 隐藏窗口macwin隐藏其他关闭 * 隐藏窗口macwin隐藏其他关闭
*/ */

View File

@ -2,7 +2,7 @@
--tab-font-family: -apple-system, 'Segoe UI', roboto, oxygen-sans, ubuntu, cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; --tab-font-family: -apple-system, 'Segoe UI', roboto, oxygen-sans, ubuntu, cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--tab-font-size: 12px; --tab-font-size: 12px;
--tab-transition: background-color 200ms ease-out, color 200ms ease-out; --tab-transition: background-color 200ms ease-out, color 200ms ease-out;
--tab-cursor: pointer; /* 设置鼠标指针为手型 */ --tab-cursor: pointer;
--tab-color: #7f8792; --tab-color: #7f8792;
--tab-background: #EFF0F4; --tab-background: #EFF0F4;
--tab-active-color: #222529; --tab-active-color: #222529;
@ -15,7 +15,8 @@
padding: 0; padding: 0;
} }
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
@ -39,8 +40,43 @@ html, body {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.nav ul { /* 导航按钮 */
.nav-controls {
display: flex; display: flex;
align-items: center;
margin-right: 12px;
-webkit-app-region: none;
}
.nav-controls div {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
cursor: pointer;
}
.nav-controls svg {
width: 16px;
height: 16px;
color: var(--tab-active-color);
}
.nav-controls .disabled {
cursor: not-allowed !important;
}
.nav-controls .disabled svg {
opacity: 0.3;
}
/* 标签 */
.nav-tabs {
min-width: 0;
flex: 1;
display: flex;
gap: 8px;
height: 35px; height: 35px;
margin-top: 5px; margin-top: 5px;
user-select: none; user-select: none;
@ -48,18 +84,17 @@ html, body {
overflow-y: hidden; overflow-y: hidden;
} }
.nav ul::-webkit-scrollbar { .nav-tabs::-webkit-scrollbar {
display: none; display: none;
} }
.nav ul li { .nav-tabs li {
display: inline-flex; display: inline-flex;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
height: calc(100% - 5px); height: calc(100% - 5px);
padding: 7px 8px; padding: 7px 8px;
margin: 0 8px 0 0;
min-width: 100px; min-width: 100px;
max-width: 240px; max-width: 240px;
scroll-margin: 12px; scroll-margin: 12px;
@ -70,24 +105,23 @@ html, body {
-webkit-app-region: none; -webkit-app-region: none;
} }
.nav ul li:first-child { .nav-tabs li:first-child {
margin-left: 8px;
border-left: none; border-left: none;
} }
.nav ul li.active { .nav-tabs li.active {
color: var(--tab-active-color); color: var(--tab-active-color);
background: var(--tab-active-background); background: var(--tab-active-background);
border-radius: 4px; border-radius: 4px;
} }
.nav ul li.active .tab-icon.background { .nav-tabs li.active .tab-icon.background {
background-image: url(../image/link_normal_selected_icon.png); background-image: url(../image/link_normal_selected_icon.png);
} }
.nav ul li:not(.active)::after { .nav-tabs li:not(.active)::after {
position: absolute; position: absolute;
right: 0; right: 0;
width: 1px; width: 1px;
@ -96,22 +130,24 @@ html, body {
content: ''; content: '';
} }
.nav ul li:not(.active):last-child::after { .nav-tabs li:not(.active):last-child::after {
content: none; content: none;
} }
/* 浏览器打开 */ /* 浏览器打开 */
.browser { .nav-browser {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
height: 40px; height: 40px;
padding: 0 14px; padding: 0 14px;
margin: 0 2px;
cursor: pointer; cursor: pointer;
background-color: var(--tab-background); background-color: var(--tab-background);
-webkit-app-region: none; -webkit-app-region: none;
} }
.browser span {
.nav-browser span {
display: inline-block; display: inline-block;
width: 18px; width: 18px;
height: 18px; height: 18px;
@ -161,6 +197,7 @@ html, body {
0% { 0% {
transform: scale(0.8) rotate(0deg); transform: scale(0.8) rotate(0deg);
} }
100% { 100% {
transform: scale(0.8) rotate(360deg); transform: scale(0.8) rotate(360deg);
} }
@ -205,18 +242,17 @@ html, body {
} }
/* 不同平台样式 */ /* 不同平台样式 */
body.win32 .nav ul { body.win32 .nav {
margin-left: 8px; padding-left: 8px;
margin-right: 186px; padding-right: 186px;
} }
body.win32 .browser {
right: 140px; body.darwin .nav {
padding-left: 76px;
} }
body.darwin .nav ul {
margin-left: 76px; body.darwin.full-screen .nav {
} padding-left: 8px;
body.darwin.full-screen .nav ul {
margin-left: 8px;
} }
/* 暗黑模式 */ /* 暗黑模式 */
@ -229,15 +265,15 @@ body.darwin.full-screen .nav ul {
--tab-close-color: #E3E3E3; --tab-close-color: #E3E3E3;
} }
.nav ul li.active .tab-icon.background { .nav-tabs li.active .tab-icon.background {
background-image: url(../image/dark/link_normal_selected_icon.png); background-image: url(../image/dark/link_normal_selected_icon.png);
} }
.browser span { .nav-browser span {
background-image: url(../image/dark/link_normal_selected_icon.png); background-image: url(../image/dark/link_normal_selected_icon.png);
} }
.tab-icon.background { .tab-icon.background {
background-image: url(../image/dark/link_normal_icon.png); background-image: url(../image/dark/link_normal_icon.png);
} }
} }

View File

@ -9,7 +9,19 @@
<body> <body>
<div id="app" class="app"> <div id="app" class="app">
<div class="nav"> <div class="nav">
<ul> <div class="nav-controls">
<div class="nav-back" :class="{disabled: !canGoBack}" @click="goBack">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</div>
<div class="nav-forward" :class="{disabled: !canGoForward}" @click="goForward">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
</div>
<div class="nav-refresh" @click="loadingState ? stop() : refresh()">
<svg v-if="loadingState" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" style="transform:scale(0.99)" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
</div>
</div>
<ul class="nav-tabs">
<li v-for="item in tabs" :data-id="item.id" :class="{active: activeId === item.id}" @click="onSwitch(item)"> <li v-for="item in tabs" :data-id="item.id" :class="{active: activeId === item.id}" @click="onSwitch(item)">
<div v-if="item.state === 'loading'" class="tab-icon loading"> <div v-if="item.state === 'loading'" class="tab-icon loading">
<div class="tab-icon-loading"></div> <div class="tab-icon-loading"></div>
@ -19,9 +31,9 @@
<div class="tab-close" @click.stop="onClose(item)"></div> <div class="tab-close" @click.stop="onClose(item)"></div>
</li> </li>
</ul> </ul>
</div> <div v-if="canBrowser" class="nav-browser" @click="onBrowser">
<div v-if="canBrowser" class="browser" @click="onBrowser"> <span></span>
<span></span> </div>
</div> </div>
</div> </div>
@ -29,10 +41,20 @@
const App = { const App = {
data() { data() {
return { return {
// 当前激活的标签页ID
activeId: 0, activeId: 0,
// 标签页列表
tabs: [], tabs: [],
// 停止定时器
stopTimer: null, stopTimer: null,
// 是否可以后退
canGoBack: false,
// 是否可以前进
canGoForward: false,
} }
}, },
beforeCreate() { beforeCreate() {
@ -42,6 +64,7 @@
window.__onDispatchEvent = (detail) => { window.__onDispatchEvent = (detail) => {
const {id, event} = detail const {id, event} = detail
switch (event) { switch (event) {
// 创建标签页
case 'create': case 'create':
this.tabs.push(Object.assign({ this.tabs.push(Object.assign({
id, id,
@ -52,6 +75,7 @@
}, detail)) }, detail))
break break
// 关闭标签页
case 'close': case 'close':
const closeIndex = this.tabs.findIndex(item => item.id === id) const closeIndex = this.tabs.findIndex(item => item.id === id)
if (closeIndex > -1) { if (closeIndex > -1) {
@ -59,11 +83,14 @@
} }
break break
// 切换标签页
case 'switch': case 'switch':
this.activeId = id this.activeId = id
this.scrollTabActive() this.scrollTabActive()
this.updateNavigationState()
break break
// 页面标题
case 'title': case 'title':
if (["HitoseaTask", "DooTask", "about:blank"].includes(detail.title)) { if (["HitoseaTask", "DooTask", "about:blank"].includes(detail.title)) {
return return
@ -75,6 +102,7 @@
} }
break break
// 页面图标
case 'favicon': case 'favicon':
const faviconItem = this.tabs.find(item => item.id === id) const faviconItem = this.tabs.find(item => item.id === id)
if (faviconItem) { if (faviconItem) {
@ -88,6 +116,7 @@
} }
break break
// 开始加载
case 'start-loading': case 'start-loading':
const startItem = this.tabs.find(item => item.id === id) const startItem = this.tabs.find(item => item.id === id)
if (startItem) { if (startItem) {
@ -96,19 +125,33 @@
} }
break break
// 停止加载
case 'stop-loading': case 'stop-loading':
this.stopTimer = setTimeout(_ => { this.stopTimer = setTimeout(_ => {
const stopItem = this.tabs.find(item => item.id === id) const stopItem = this.tabs.find(item => item.id === id)
if (stopItem) { if (stopItem) {
stopItem.state = 'loaded' stopItem.state = 'loaded'
} }
if (id === this.activeId) {
this.updateNavigationState()
}
}, 300) }, 300)
break break
// 导航状态
case 'navigation-state':
if (id === this.activeId) {
this.canGoBack = detail.canGoBack
this.canGoForward = detail.canGoForward
}
break
// 进入全屏
case 'enter-full-screen': case 'enter-full-screen':
document.body.classList.add('full-screen') document.body.classList.add('full-screen')
break break
// 离开全屏
case 'leave-full-screen': case 'leave-full-screen':
document.body.classList.remove('full-screen') document.body.classList.remove('full-screen')
break break
@ -119,41 +162,85 @@
} }
}, },
computed: { computed: {
/**
* 获取当前激活的标签页
* @returns {object|null}
*/
activeItem() { activeItem() {
if (this.tabs.length === 0) { if (this.tabs.length === 0) {
return null return null
} }
return this.tabs.find(item => item.id === this.activeId) return this.tabs.find(item => item.id === this.activeId)
}, },
/**
* 获取页面标题
* @returns {string}
*/
pageTitle() { pageTitle() {
return this.activeItem ? this.activeItem.title : 'Untitled' return this.activeItem ? this.activeItem.title : 'Untitled'
}, },
/**
* 是否可以打开浏览器
* @returns {boolean}
*/
canBrowser() { canBrowser() {
return !(this.activeItem && this.isLocalHost(this.activeItem.url)) return !(this.activeItem && this.isLocalHost(this.activeItem.url))
},
/**
* 获取加载状态
* @returns {boolean}
*/
loadingState() {
return this.activeItem ? this.activeItem.state === 'loading' : false
} }
}, },
watch: { watch: {
/**
* 监听页面标题
* @param title
*/
pageTitle(title) { pageTitle(title) {
document.title = title; document.title = title;
}, },
}, },
methods: { methods: {
/**
* 切换标签页
* @param item
*/
onSwitch(item) { onSwitch(item) {
this.sendMessage('webTabActivate', item.id) this.sendMessage('webTabActivate', item.id)
}, },
/**
* 关闭标签页
* @param item
*/
onClose(item) { onClose(item) {
this.sendMessage('webTabClose', item.id); this.sendMessage('webTabClose', item.id);
}, },
/**
* 打开浏览器
*/
onBrowser() { onBrowser() {
this.sendMessage('webTabExternal') this.sendMessage('webTabExternal')
}, },
/**
* 获取标签页图标样式
* @param item
* @returns {string}
*/
iconStyle(item) { iconStyle(item) {
return item.icon ? `background-image: url(${item.icon})` : '' return item.icon ? `background-image: url(${item.icon})` : ''
}, },
/**
* 获取标签页标题
* @param item
* @returns {string}
*/
tabTitle(item) { tabTitle(item) {
if (item.title) { if (item.title) {
return item.title return item.title
@ -166,10 +253,13 @@
} }
}, },
/**
* 滚动到当前激活的标签页
*/
scrollTabActive() { scrollTabActive() {
setTimeout(() => { setTimeout(() => {
try { try {
const child = document.querySelector(`.nav ul li[data-id=${this.activeId}]`) const child = document.querySelector(`.nav-tabs li[data-id="${this.activeId}"]`)
if (child) { if (child) {
child.scrollIntoView({behavior: 'smooth', block: 'nearest'}) child.scrollIntoView({behavior: 'smooth', block: 'nearest'})
} }
@ -179,10 +269,52 @@
}, 0) }, 0)
}, },
/**
* 发送消息
* @param event
* @param args
*/
sendMessage(event, args) { sendMessage(event, args) {
electron?.sendMessage(event, args) electron?.sendMessage(event, args)
}, },
/**
* 后退
*/
goBack() {
if (!this.canGoBack) return
this.sendMessage('webTabGoBack')
},
/**
* 前进
*/
goForward() {
if (!this.canGoForward) return
this.sendMessage('webTabGoForward')
},
/**
* 停止
*/
stop() {
this.sendMessage('webTabStop')
},
/**
* 刷新
*/
refresh() {
this.sendMessage('webTabReload')
},
/**
* 更新导航状态
*/
updateNavigationState() {
this.sendMessage('webTabGetNavigationState')
},
/** /**
* 判断是否是本地URL * 判断是否是本地URL
* @param url * @param url