electron浏览器模式多标签方案
main.js
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({})
}
app.whenReady().then(() => {
createWindow()
const tabManager = new TabManager(mainWindow, baseUrl);
//打开标签
ipcMain.handle('open-tab', (event, tabInfo) => {
tabManager.createTab(tabInfo);
})
//改变标签
ipcMain.handle('change-tab', (event, tabInfo) => {
tabManager.changeTab(tabInfo);
})
//关闭标签
ipcMain.handle('close-tab', (event, view) => {
return tabManager.closeTab(view);
})
//关闭所有标签,这里应该是多余的
ipcMain.handle('close-all-tab', (event) => {
return tabManager.closeAllTab();
})
//获取所有标签(窗口对象)
ipcMain.handle('get-tabs', (event) => {
return tabManager.getTabs();
})
//隐藏标签
ipcMain.handle('hidden-tab', (event, view) => {
return tabManager.hiddenTab(view);
})
}
tab-manager
import { WebContentsView } from 'electron';
import { join } from 'path';
import { ipcMain } from 'electron';
import Logger from 'electron-log';
class TabManager {
static instance = null;
static mainWindow = null;
static baseUrl = null;
views = [];
activeView = null;
x = 0
y = 70
constructor(mainWindow, baseUrl) {
if (TabManager.instance) {
return TabManager.instance;
}
TabManager.mainWindow = mainWindow;
// TabManager.baseUrl = baseUrl;
TabManager.instance = this;
}
listenMainWindowSize (view) {
console.log(TabManager.mainWindow)
// 监听窗口大小变化
TabManager.mainWindow.on('resize', () => this.showView(view));
TabManager.mainWindow.on('maximize', () => this.showView(view));
TabManager.mainWindow.on('unmaximize', () => this.showView(view));
}
showView (view) {
const bounds = TabManager.mainWindow.getBounds(); // 获取窗口大小
view.setBounds({ x: this.x, y: this.y, width: bounds.width, height: bounds.height - 70 });
}
hiddenViewe (view) {
view.setBounds({ x: this.x, y: this.y, width: 0, height: 0 });
}
createTab (tabInfo) {
for (let i = 0; i < this.views.length; i++) {
//隐藏所有页面
// this.views[i].view.setBounds({ x: 0, y: 70, width: 0, height: 0 });
this.hiddenViewe(this.views[i].view);
}
//创建子页面
const view = new WebContentsView(
{
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: true
}
}
);
let baseUrl
if (process.env['ELECTRON_RENDERER_URL']) {
baseUrl = process.env['ELECTRON_RENDERER_URL'];
} else {
baseUrl = join(__dirname, '../renderer/index.html')
}
// baseUrl = join(__dirname, '../renderer/index.html')
Logger.info("baseUrl2 => " + baseUrl)
view.webContents.loadURL(baseUrl + '#' + tabInfo.url);
this.showView(view);
this.listenMainWindowSize(view);
view.webContents.openDevTools();
TabManager.mainWindow.contentView.addChildView(view);
const viewInfo = {
view: view,
url: tabInfo.url,
time: tabInfo.time
}
this.views.push(viewInfo);
this.activeView = viewInfo
}
findView (view, views) {
for (let i = 0; i < views.length; i++) {
if (views[i].url == view.url && views[i].time == view.time) {
return views[i];
}
}
return null
}
changeTab (tabInfo) {
const views = this.views;
for (let i = 0; i < views.length; i++) {
// views[i].view.setBounds({ x: 0, y: 70, width: 0, height: 0 });
this.hiddenViewe(views[i].view);
if (views[i].url == tabInfo.url && views[i].time == tabInfo.time) {
// views[i].view.setBounds({ x: 0, y: 70, width: 950, height: 650 });
this.showView(views[i].view);
this.activeView = views[i];
}
}
}
closeTab (view) {
const views = this.views;
for (let i = 0; i < views.length; i++) {
if (views[i].url == view.url && views[i].time == view.time) {
TabManager.mainWindow.contentView.removeChildView(views[i].view);
views[i].view.webContents.destroy();
views.splice(i, 1);
}
}
// try {
// for (let i = 0; i < views.length; i++) {
// if (views[i].url == view.url && views[i].time == view.time) {
// TabManager.mainWindow.contentView.removeChildView(views[i].view);
// views[i].view.webContents.destroy();
// views.splice(i, 1);
// console.log(TabManager.mainWindow.contentView.children.includes(views[i].view))
// }
// }
// console.log(views)
// return true
// } catch (e) {
// console.error(e)
// return false
// }
}
closeTabAll () {
for (let i = 0; i < this.views.length; i++) {
TabManager.mainWindow.contentView.removeChildView(this.views[i].view);
this.views[i].view.webContents.destroy();
this.views.splice(i, 1);
}
}
hiddenTab (view) {
if (!view) {
for (let i = 0; i < this.views.length; i++) {
// this.views[i].view.setBounds({ x: 0, y: 70, width: 0, height: 0 });
this.hiddenViewe(this.views[i].view);
}
return
}
const views = this.views;
for (let i = 0; i < views.length; i++) {
if (views[i].url == view.url && views[i].time == view.time) {
// views[i].view.setBounds({ x: 0, y: 70, width: 0, height: 0 });
this.hiddenViewe(views[i].view);
}
}
}
getTabs () {
return this.views
}
}
// module.exports = TabManager;
export default TabManager
preload
openTab: (tabInfo) => {
return ipcRenderer.invoke('open-tab', tabInfo)
},
changeTab: (tabInfo) => {
return ipcRenderer.invoke('change-tab', tabInfo)
},
closeTab: (view) => {
return ipcRenderer.invoke('close-tab', view)
},
getTabs: () => {
return ipcRenderer.invoke('get-tabs')
},
closeAllTabs: () => {
return ipcRenderer.invoke('close-all-tabs')
},
hiddenTab: (view) => {
return ipcRenderer.invoke('hidden-tab', view)
}
render
router.js
const router = createRouter({
//使用hash模式
history: createWebHashHistory(import.meta.env.BASE_URL),
// history: createMemoryHistory(import.meta.env.BASE_URL),
routes,
})
vue
<template>
<div id="home">
<el-tabs v-model="editableTabsValue" type="card" @tab-remove="removeTab" class="custom-tabs" @tab-click="tabClick">
<el-tab-pane name="application-center">
<template #label>
<div class="custom-tabs-label">
<img src="/icon/apps/app-center.png" class="app-title-icon">
<span>应用中心</span>
</div>
</template>
<div id="apps">
<div class="app" v-for="app in apps" @click="openApp(app)">
<div class="icon">
<img :src="(app.icon)">
</div>
<div class="name">
<span v-text="app.title"></span>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane v-for="item in openApps" :key="item.name" :label="item.title" :name="item.name" :closable="true">
<template #label>
<div class="custom-tabs-label">
<img :src="item.icon" class="app-title-icon">
<span>{{ item.title }}</span>
</div>
</template>
<div class="home-body">
<!-- <RouterView v-if="item.name == editableTabsValue"></RouterView> -->
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Select } from '@element-plus/icons-vue'
import { ElMessage, type TabPaneName, type TabsPaneContext } from 'element-plus'
import { useRouter, RouterView } from 'vue-router'
import { cloneDeep } from 'lodash'
const router = useRouter()
const editableTabsValue = ref('application-center')
const apps = [
{
title: '测试',
name: 'test',
icon: './icon/apps/test.png',
url: '/home/test'
},
{
title: '日程表',
name: 'schedule',
icon: './icon/apps/rili.png',
url: '/home/schedule/week/index'
},
{
title: '入网安评',
name: 'rwap',
icon: './icon/apps/rwap.png',
url: '/home/rwap/order/index'
},
{
title: '笔记本',
name: 'notebook',
icon: './icon/apps/notebook.png',
url: '/home/notebook'
},
{
title: '设置',
name: 'setting',
icon: './icon/apps/setting.png',
url: '/home/setting'
}
]
const openApps = ref<typeof apps>([])
const tabs = ref([])
function openApp(app: typeof apps[0]) {
if (openApps.value.find((item) => item.name == app.name)) {
ElMessage.warning(app.title + '已打开')
editableTabsValue.value = app.name
return null
}
// window.open(app.url)
openApps.value.push(app)
editableTabsValue.value = app.name
const time = Date.now()
const tabInfo = {
...app,
time
}
tabs.value.push(tabInfo)
window.api.openTab(tabInfo)
}
async function removeTab(targetName: TabPaneName) {
editableTabsValue.value = 'application-center'
for (let i = 0; i < openApps.value.length; i++) {
if (openApps.value[i].name == targetName) {
openApps.value.splice(i, 1)
}
}
const view = tabs.value.find((item) => item.name == targetName)
tabs.value.splice(tabs.value.findIndex((item) => item.name == targetName), 1)
await window.api.closeTab(cloneDeep(view))
}
function tabClick(tab: TabsPaneContext) {
const timer = ref<number | null>(null)
function debounce(fn: () => void, delay = 1) {
if (timer.value) {
clearTimeout(timer.value)
}
timer.value = window.setTimeout(() => {
fn()
timer.value = null
}, delay)
}
debounce(() => {
const view = tabs.value.find((item) => item.name == tab.paneName)
if (!view && editableTabsValue.value == 'application-center') {
window.api.hiddenTab()
}
if (view) {
window.api.changeTab(cloneDeep(view))
}
})
}
</script>
<style scoped lang="less">
#apps {
display: flex;
height: 70px;
}
#home {
width: 100%;
height: calc(100% - 30px);
overflow-y: auto;
position: relative;
}
.app {
cursor: pointer;
padding: 4px;
border-radius: 5px;
margin: 4px;
user-select: none;
.name {
text-align: center;
color: #5c5c5c;
}
}
.app:hover .name {
// background-color: #f3f3f3;
color: #409EFF !important;
}
.icon {
display: flex;
justify-content: center;
}
.icon,
.icon img {
height: 40px;
}
.custom-tabs-label {
display: flex;
justify-content: center;
user-select: none;
}
.app-title-icon {
height: 20px;
}
.custom-tabs {
// height: calc(100% - 40px);
height: 100%;
}
.custom-tabs :deep(.el-tabs__content) {
height: 100%;
.el-tab-pane {
height: 100%;
}
}
.home-body {
overflow-y: auto;
height: 100%;
}
</style>
本文来自博客园,作者:小枫同学,除网络转载的部分,其他版权属于作者和博客园所有,未经作者或博客园许可,禁止转载、复制、重新发布完整或者部分文字、代码、图片等信息,否则将保留追究法律责任的权利(如博客侵权了您的作品,本人再次表示抱歉,请将原创地址发送至下文邮箱,核实后立刻删除。)。查阅文章的同学,由于网络爬虫严重,有些代码并不会完整贴出来或者存在bug,不过你可以发送邮件到xfstune@126.com获取新代码,记得附上文章链接

浙公网安备 33010602011771号