From bb83875c9981f44c74e2481aa16509216cc7e739 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 19 Aug 2025 13:51:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E5=AF=BC=E8=88=AA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/electron.js | 94 ++++++++++++++ electron/render/tabs/assets/css/style.css | 90 ++++++++++---- electron/render/tabs/index.html | 142 +++++++++++++++++++++- 3 files changed, 294 insertions(+), 32 deletions(-) diff --git a/electron/electron.js b/electron/electron.js index c3b5e9cdc..d5ad7bba8 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -910,6 +910,7 @@ function createWebTabWindow(args) { event: 'stop-loading', id: browserView.webContents.id, }).then(_ => { }) + // 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透 if (nativeTheme.shouldUseDarkColors) { browserView.setBackgroundColor('#FFFFFF') @@ -1320,6 +1321,99 @@ ipcMain.on('webTabDestroyAll', (event) => { 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" +}) + /** * 隐藏窗口(mac、win隐藏,其他关闭) */ diff --git a/electron/render/tabs/assets/css/style.css b/electron/render/tabs/assets/css/style.css index f064cc3a6..fac7c7d94 100644 --- a/electron/render/tabs/assets/css/style.css +++ b/electron/render/tabs/assets/css/style.css @@ -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-size: 12px; --tab-transition: background-color 200ms ease-out, color 200ms ease-out; - --tab-cursor: pointer; /* 设置鼠标指针为手型 */ + --tab-cursor: pointer; --tab-color: #7f8792; --tab-background: #EFF0F4; --tab-active-color: #222529; @@ -15,7 +15,8 @@ padding: 0; } -html, body { +html, +body { margin: 0; padding: 0; font-family: 'Roboto', sans-serif; @@ -39,8 +40,43 @@ html, body { -webkit-app-region: drag; } -.nav ul { +/* 导航按钮 */ +.nav-controls { 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; margin-top: 5px; user-select: none; @@ -48,18 +84,17 @@ html, body { overflow-y: hidden; } -.nav ul::-webkit-scrollbar { +.nav-tabs::-webkit-scrollbar { display: none; } -.nav ul li { +.nav-tabs li { display: inline-flex; position: relative; box-sizing: border-box; align-items: center; height: calc(100% - 5px); padding: 7px 8px; - margin: 0 8px 0 0; min-width: 100px; max-width: 240px; scroll-margin: 12px; @@ -70,24 +105,23 @@ html, body { -webkit-app-region: none; } -.nav ul li:first-child { - margin-left: 8px; +.nav-tabs li:first-child { border-left: none; } -.nav ul li.active { +.nav-tabs li.active { color: var(--tab-active-color); background: var(--tab-active-background); 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); } -.nav ul li:not(.active)::after { +.nav-tabs li:not(.active)::after { position: absolute; right: 0; width: 1px; @@ -96,22 +130,24 @@ html, body { content: ''; } -.nav ul li:not(.active):last-child::after { +.nav-tabs li:not(.active):last-child::after { content: none; } /* 浏览器打开 */ -.browser { +.nav-browser { flex-shrink: 0; display: flex; align-items: center; height: 40px; padding: 0 14px; + margin: 0 2px; cursor: pointer; background-color: var(--tab-background); -webkit-app-region: none; } -.browser span { + +.nav-browser span { display: inline-block; width: 18px; height: 18px; @@ -161,6 +197,7 @@ html, body { 0% { transform: scale(0.8) rotate(0deg); } + 100% { transform: scale(0.8) rotate(360deg); } @@ -205,18 +242,17 @@ html, body { } /* 不同平台样式 */ -body.win32 .nav ul { - margin-left: 8px; - margin-right: 186px; +body.win32 .nav { + padding-left: 8px; + 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 ul { - margin-left: 8px; + +body.darwin.full-screen .nav { + padding-left: 8px; } /* 暗黑模式 */ @@ -229,15 +265,15 @@ body.darwin.full-screen .nav ul { --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); } - .browser span { + .nav-browser span { background-image: url(../image/dark/link_normal_selected_icon.png); } .tab-icon.background { background-image: url(../image/dark/link_normal_icon.png); } -} +} \ No newline at end of file diff --git a/electron/render/tabs/index.html b/electron/render/tabs/index.html index 69f0697e0..5f96a665b 100644 --- a/electron/render/tabs/index.html +++ b/electron/render/tabs/index.html @@ -9,7 +9,19 @@
-
- +
@@ -29,10 +41,20 @@ const App = { data() { return { + // 当前激活的标签页ID activeId: 0, + + // 标签页列表 tabs: [], + // 停止定时器 stopTimer: null, + + // 是否可以后退 + canGoBack: false, + + // 是否可以前进 + canGoForward: false, } }, beforeCreate() { @@ -42,6 +64,7 @@ window.__onDispatchEvent = (detail) => { const {id, event} = detail switch (event) { + // 创建标签页 case 'create': this.tabs.push(Object.assign({ id, @@ -52,6 +75,7 @@ }, detail)) break + // 关闭标签页 case 'close': const closeIndex = this.tabs.findIndex(item => item.id === id) if (closeIndex > -1) { @@ -59,11 +83,14 @@ } break + // 切换标签页 case 'switch': this.activeId = id this.scrollTabActive() + this.updateNavigationState() break + // 页面标题 case 'title': if (["HitoseaTask", "DooTask", "about:blank"].includes(detail.title)) { return @@ -75,6 +102,7 @@ } break + // 页面图标 case 'favicon': const faviconItem = this.tabs.find(item => item.id === id) if (faviconItem) { @@ -88,6 +116,7 @@ } break + // 开始加载 case 'start-loading': const startItem = this.tabs.find(item => item.id === id) if (startItem) { @@ -96,19 +125,33 @@ } break + // 停止加载 case 'stop-loading': this.stopTimer = setTimeout(_ => { const stopItem = this.tabs.find(item => item.id === id) if (stopItem) { stopItem.state = 'loaded' } + if (id === this.activeId) { + this.updateNavigationState() + } }, 300) break + // 导航状态 + case 'navigation-state': + if (id === this.activeId) { + this.canGoBack = detail.canGoBack + this.canGoForward = detail.canGoForward + } + break + + // 进入全屏 case 'enter-full-screen': document.body.classList.add('full-screen') break + // 离开全屏 case 'leave-full-screen': document.body.classList.remove('full-screen') break @@ -119,41 +162,85 @@ } }, computed: { + /** + * 获取当前激活的标签页 + * @returns {object|null} + */ activeItem() { if (this.tabs.length === 0) { return null } return this.tabs.find(item => item.id === this.activeId) }, + /** + * 获取页面标题 + * @returns {string} + */ pageTitle() { return this.activeItem ? this.activeItem.title : 'Untitled' }, + /** + * 是否可以打开浏览器 + * @returns {boolean} + */ canBrowser() { return !(this.activeItem && this.isLocalHost(this.activeItem.url)) + }, + /** + * 获取加载状态 + * @returns {boolean} + */ + loadingState() { + return this.activeItem ? this.activeItem.state === 'loading' : false } }, watch: { + /** + * 监听页面标题 + * @param title + */ pageTitle(title) { document.title = title; }, }, methods: { + /** + * 切换标签页 + * @param item + */ onSwitch(item) { this.sendMessage('webTabActivate', item.id) }, + /** + * 关闭标签页 + * @param item + */ onClose(item) { this.sendMessage('webTabClose', item.id); }, + /** + * 打开浏览器 + */ onBrowser() { this.sendMessage('webTabExternal') }, + /** + * 获取标签页图标样式 + * @param item + * @returns {string} + */ iconStyle(item) { return item.icon ? `background-image: url(${item.icon})` : '' }, + /** + * 获取标签页标题 + * @param item + * @returns {string} + */ tabTitle(item) { if (item.title) { return item.title @@ -166,10 +253,13 @@ } }, + /** + * 滚动到当前激活的标签页 + */ scrollTabActive() { setTimeout(() => { 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) { child.scrollIntoView({behavior: 'smooth', block: 'nearest'}) } @@ -179,10 +269,52 @@ }, 0) }, + /** + * 发送消息 + * @param event + * @param args + */ 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 * @param url