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>
posted @ 2025-03-29 14:00  小枫同学  阅读(635)  评论(0)    收藏  举报