Electron + VUE3 + 实现桌面级应用程序前端模板

3,389 阅读5分钟

Electron + VUE3 + 实现桌面级应用程序前端模板

前言

electron 用的版本是25.6.0

打包工具使用:vite

vue版本:3.3.4

虽然是vue3.x版本,但是大部分的代码还是采用了vue2.x的语法去编写,这样让没有学过vue3的同学们,看起来更直观一点

Electron 是一个用于构建跨平台桌面应用程序的开源框架,由 GitHub 开发和维护。它允许开发者使用 HTMLCSSJavaScript 等前端技术,结合 Node.js 和 Chromium,创建可以在 WindowsmacOSLinux 上运行的桌面应用程序。

  1. 跨平台支持
    • 同一份代码可以在主流桌面平台上运行(Windows、macOS、Linux)。
  2. 基于 Web 技术
    • 使用前端技术(HTML/CSS/JavaScript)来开发界面。
  3. Node.js 集成
    • 支持 Node.js 的完整功能,允许直接调用文件系统、进程、网络等底层功能。
  4. Chromium 渲染引擎
    • 使用 Chromium 提供的高性能 Web 渲染能力,使应用界面与现代浏览器一致。
  5. 强大的生态系统
    • Electron 拥有丰富的第三方插件和库支持,例如文件系统操作、通知、菜单管理等。

页面展示

登录页面

image-20241203091505674

导航栏自定义红绿灯,关闭了原始的

核心代码

主进程代码

import { app, shell, BrowserWindow ,ipcMain} from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'

const login_width = 530;
const register_height = 635;


function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    icon:icon,
    width: login_width,
    height: register_height,
    show: false,
    transparent:true,
    autoHideMenuBar: true,
    titleBarStyle:'hidden',
    resizable:false,
    frame: true,
    mediaAccess:true,
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      webSecurity: false,
    }
  })


  ipcMain.on('toMain',(event, args)=>{
    mainWindow.setResizable(true)
    mainWindow.setSize(((args.screenWidth / 5) * 3) + 150,600)
    mainWindow.setMinimumSize(((args.screenWidth / 5) * 3) + 150,600)
    mainWindow.center();
    mainWindow.setMaximizable(true)
    mainWindow.setMaximumSize(((args.screenWidth / 5) * 3) + 200,700)
  })

  ipcMain.on('minimizing',(event,args)=>{
    event.preventDefault(); // 阻止默认最小化行为
    mainWindow.minimize(); // 最小化到任务栏
  })

  ipcMain.on('expandWindow',(event,args)=>{
    let defaultSize = mainWindow.getSize();
    let maxSize = mainWindow.getMaximumSize();
    if (defaultSize[0] === (((args.screenWidth / 5) * 3) + 150) && defaultSize[1] === 600){
      mainWindow.setResizable(true)
      mainWindow.setSize(((args.screenWidth / 5) * 3) + 200,700)
    }else if (maxSize[0] === (((args.screenWidth / 5) * 3) + 200) && maxSize[1] === 700){
      mainWindow.setResizable(true)
      mainWindow.setSize(((args.screenWidth / 5) * 3) + 150,600)
    }
  })

  mainWindow.on('ready-to-show', () => {
    mainWindow.show()
  })

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
   // 窗口调试 mainWindow.webContents.openDevTools()
    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  } else {
    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  }
}

app.whenReady().then(() => {
  electronApp.setAppUserModelId('com.electron')

  app.on('browser-window-created', (_, window) => {
    optimizer.watchWindowShortcuts(window)
  })

  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})


app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

自定义导航栏组件

<template>
  <div class="title-bar" :style="{width:expandState?'':'510px'}">

    <div class="title-bar-left" :style="{backgroundColor:expandState?'rgba(221, 228, 234,0.9)':'#fff'}">
      <img src="~@/assets/bar/logo.svg" alt="图标" class="icon" />
      <div class="title">流量监控系统</div>
    </div>

    <div class="title-bar-right">
      <div class="operating-button close-button" @click="closeWindow">
        <img src="~@/assets/bar/close.svg" />
      </div>
      <div class="operating-button minimum-button" @click="minimumWindow">
        <img src="~@/assets/bar/minimum.svg" />
      </div>
      <div v-if="expandState" class="operating-button expand-button" @click="expandWindow">
        <img src="~@/assets/bar/expand.svg" />
      </div>
      <div style="width: 10px;"></div>
    </div>


  </div>
</template>


<script>
export default {
  name: "TitleBar",
  data(){
    return{
      expandState:false,
    }
  },
  mounted() {
    this.expandState = !(this.$route.path === "/" || this.$route.path === '/login');
  },
  methods: {
    closeWindow() {
      window.close(); // 关闭窗口
    },
    minimumWindow() {
      window.ipcRenderer.send('minimizing')
    },
    expandWindow() {
      window.ipcRenderer.send('expandWindow',{
        screenWidth:window.screen.width,
        screenHeight:window.screen.height,
      })
    },
  },
};
</script>

<style scoped>
@import "./index.css";
</style>

注册页面

image-20241203091759577

其他登录方式

人脸登录

效果展示图:

image-20241203092122845

人脸登录采用的是effet.js进行登录的

官方文档:faceeffet.com/

首先安装人脸动画插件

npm install face-effet

具体实现代码 face.vue:

<template>
  <div class="face-main">
    <div class="faceMain">
    </div>
    <button v-loading="backLoading" class="btn" @click="backReturn"> 返回登录
    </button>
  </div>
</template>

<script>
//  初次加载会很慢,请耐心等待,
// face-effet插件官网:https://faceeffet.com/
import 'face-effet/effet/effet.css'
import effet from 'face-effet/effet/effet'
export default {
  name: "index",
  data(){
    return{
      faceEffet:effet,
      backLoading:false,
    }
  },
  beforeDestroy(){
    if (this.faceEffet){
      this.faceEffet.close();
    }
  },
  mounted() {
    try {
      this.faceEffet.restart();
    }catch (e) {
      this.faceEffet.init({
        el:'faceMain',
        callBack:this.callBack
      })
    }
  },
  methods:{
    callBack(data){
      if (data.progressMessage === 'success'){ // 判断阶段是否成功
        if (!data.base64Array || data.base64Array.length === 0){
          return;
        }
        Promise.all(data.base64Array).then((base64Strings) => {
          // 人脸数据:base64Strings
          // 这里调用后端
          if (base64Strings){
            this.faceEffet.close();
            setTimeout(()=>{
              this.$router.push({
                path:'/main',
                query:{
                  base64:base64Strings[0]
                }
              });
              const screenWidth = window.screen.width;
              const screenHeight = window.screen.height;
              window.ipcRenderer.send('toMain',{
                username:'xxxxx',
                token:'',
                screenWidth:screenWidth,
                screenHeight:screenHeight
              })

            },1000)
          }
        }).catch((error) => {
          console.error("Error resolving promises:", error);
        });
      }
    },
    backReturn(){
      if (this.backLoading){
        return
      }
      // 先销毁人脸容器
      this.faceEffet.close();
      this.backLoading = true
      // 等待1s后跳转
      setTimeout(()=>{
        this.backLoading = false
        this.$router.push('/login');
      },1000)

    }
  }
}
</script>

<style>
@import "./index.css";
</style>

注意index.html相关协议需要这样配置

  <meta
    http-equiv="Content-Security-Policy"
    content="
      default-src 'self';
      script-src 'self' 'unsafe-eval' blob: https://unpkg.com;
      style-src 'self' 'unsafe-inline';
      img-src * data:;
      connect-src 'self' blob: https://unpkg.com data:;
      worker-src 'self' blob:;
    "
  />

主进程代码

为了开发阶段便捷性,临时关闭了webSecurity,生产环境需要开启确保安全。

const login_width = 530;
const register_height = 635;
  const mainWindow = new BrowserWindow({
    icon:icon,
    width: login_width,
    height: register_height,
    show: false,
    transparent:true,
    autoHideMenuBar: true,
    titleBarStyle:'hidden',
    resizable:false,
    frame: true,
    mediaAccess:true,
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      webSecurity: false,
    }
  })

网页SSO登录

功能描述
  • 使用系统默认浏览器打开 SSO 登录页面。
  • 利用自定义协议捕获登录后的回调信息。
  • 关闭监听后返回授权信息。

代码示例
javascript复制代码const { app, shell, protocol } = require('electron');
const { URL } = require('url');

function openSSOLogin(loginUrl, callback) {
    // Register a custom protocol to handle the SSO callback
    const customProtocol = 'custom-app';
    
    protocol.registerHttpProtocol(customProtocol, (req) => {
        try {
            const url = new URL(req.url);
            const params = Object.fromEntries(url.searchParams.entries());
            callback(null, params); // Send back the captured parameters
        } catch (err) {
            callback(err);
        }
    });

    // Open the SSO login URL in the default browser
    shell.openExternal(loginUrl);
}

// Example Usage
app.whenReady().then(() => {
    const ssoLoginUrl = 'https://example-sso.com/login?client_id=APP_ID&redirect_uri=custom-app://callback';

    openSSOLogin(ssoLoginUrl, (error, data) => {
        if (error) {
            console.error('SSO Login Error:', error);
        } else {
            console.log('SSO Login Successful, Data:', data);
        }
    });
});

步骤说明
  1. 自定义协议
    • redirect_uri 设置中配置一个自定义协议。
    • Electron 的 protocol.registerHttpProtocol 用于捕获这个自定义协议的回调。
  2. 回调解析
    • SSO 登录成功后,服务器会将授权信息(如 codetoken)附加到回调 URL 的查询参数中。
    • 回调数据通过 callback 参数返回给调用方。
  3. 打开浏览器
    • 使用 shell.openExternal() 打开系统浏览器登录。

注意事项
  • 回调协议注册 确保在 SSO 平台配置 redirect_uri 时,注册的自定义协议是允许的。
  • 安全性 在生产环境中,验证返回的授权数据的真实性,确保不会被中间人打入。

主页面

桌面页面

image-20241203092751905

充值页面

image-20241203092918400

其他页面

image-20241203092949293

退出登录

image-20241203093042948

关于渲染进程跟主进程的交互

我们已自定义导航栏为列子

在我们的preload下面的index.js

核心的:window.ipcRenderer = ipcRenderer

import { contextBridge,ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
window.ipcRenderer = ipcRenderer // 将ipcRenderer定义到window上面
const api = {}
if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld('electron', electronAPI)
    contextBridge.exposeInMainWorld('api', api)
  } catch (error) {
    console.error(error)
  }
} else {
  window.electron = electronAPI
  window.api = api
}

关闭窗口

直接调用close就行了

关闭窗口

closeWindow() {
    window.close(); // 关闭窗口
},

最小化

minimumWindow() {
    window.ipcRenderer.send('minimizing')
},

发送一个 minimizing 消息

在主进程中接收消息,然后响应

import { app, shell, BrowserWindow ,ipcMain} from 'electron'

ipcMain.on('minimizing',(event,args)=>{
  event.preventDefault(); // 阻止默认最小化行为
  mainWindow.minimize(); // 最小化到任务栏
})

扩大或者缩小

expandWindow() {
    window.ipcRenderer.send('expandWindow',{
        screenWidth:window.screen.width,
        screenHeight:window.screen.height,
    })
},

主进程的处理

主要判断是否为原始窗口,如果是原始窗口大小,则放大,否则则缩小

ipcMain.on('expandWindow',(event,args)=>{
    let defaultSize = mainWindow.getSize();
    let maxSize = mainWindow.getMaximumSize();
    if (defaultSize[0] === (((args.screenWidth / 5) * 3) + 150) && defaultSize[1] === 600){
        mainWindow.setResizable(true)
        mainWindow.setSize(((args.screenWidth / 5) * 3) + 200,700)
    }else if (maxSize[0] === (((args.screenWidth / 5) * 3) + 200) && maxSize[1] === 700){
        mainWindow.setResizable(true)
        mainWindow.setSize(((args.screenWidth / 5) * 3) + 150,600)
    }
})

项目开源地址

gitee:gitee.com/susantyp/fl…

github:github.com/typsusan/fl…