项目基于electron 22.3.34 + vue3+ vite, windows安装包适配win7、win8、win10、win11
项目目录:
package.json文件
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux",
"electron:generate-icons": "electron-icon-builder --input=./resources/icon.png --output=./build --flatten"
},
"dependencies": {
"@electron-toolkit/preload": "^2.0.0",
"@electron-toolkit/utils": "3.0.0",
"electron-updater": "5.3.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "16.18.38",
"@vitejs/plugin-vue": "3.2.0",
"electron": "22.3.24",
"electron-builder": "23.6.0",
"electron-icon-builder": "2.0.1",
"electron-vite": "1.0.10",
"eslint": "8.56.0",
"eslint-plugin-vue": "9.18.1",
"prettier": "2.8.8",
"typescript": "4.9.5",
"vite-plugin-static-copy": "0.13.0",
"vite": "3.2.5",
"vue": "3.2.47",
"vue-router": "4.0.12",
"vue-tsc": "1.0.13"
}
主进程main/index.ts文件:
import {app, shell, BrowserWindow, ipcMain, screen, Tray, nativeImage, Menu, dialog, protocol, globalShortcut} from 'electron'
import {join, dirname} from 'path'
import {electronApp, optimizer, is} from '@electron-toolkit/utils'
import AutoUpdater from './auto-updater';
import {exec} from "child_process";
import * as fs from "fs";
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true
}
}
])
// import icon from '../../resources/icon.png'
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null
let autoUpdater: AutoUpdater
// 获取主显示器信息
// 创建托盘图标
function createTray() {
const iconPath = app.isPackaged
? join(process.resourcesPath, 'app.asar.unpacked/resources/tray.png')
: join(__dirname, '../../resources/tray.png')
const icon = nativeImage.createFromPath(iconPath)
.resize({width: 16, height: 16}) // 适配不同DPI
tray = new Tray(icon)
// 托盘菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '打开',
click: () => {
mainWindow?.show()
}
},
{
label: '检查更新',
click: () => {
if (autoUpdater) {
autoUpdater.initializeAutoCheck()
}
}
},
{
label: '退出',
click: () => {
autoUpdater.updateWindow?.destroy()
mainWindow?.destroy()
app.quit()
}
},
{
label: '卸载',
click: () => {
confirmAndRunUninstaller()
}
}
])
tray.setContextMenu(contextMenu)
tray.setToolTip('自己的标题')
}
function confirmAndRunUninstaller() {
const appPath = process.execPath
// 推导卸载程序路径(假设卸载程序与应用同级目录)
const uninstallerPath = join(
dirname(appPath), // 应用安装目录
'Uninstall 危急值AI提示.exe' // Windows 卸载程序名称
)
// 1. 显示确认对话框
dialog.showMessageBox({
type: 'question',
buttons: ['取消', '确认卸载'],
title: '卸载应用',
message: '确定要彻底卸载此应用吗?'
}).then(({ response }) => {
if (response === 1) {
// 2. 关闭应用进程
app.quit()
// 3. 执行卸载程序(Windows 示例)
if (process.platform === 'win32') {
exec(`"${uninstallerPath}" /S`, (error) => {
if (error) console.error('卸载失败:', error)
})
}
// macOS 处理(需要更复杂的权限逻辑)
else if (process.platform === 'darwin') {
exec(`sh "${uninstallerPath}"`, )
}
}
})
}
function createWindow(): void {
const iconPath = join(__dirname, '../../resources/icon.png')
mainWindow = new BrowserWindow({
width: 400,
height: 120,
show: false,
frame: false,
resizable: false,
alwaysOnTop: true,
title: "危急值AI提示",
skipTaskbar: true,
...(process.platform === 'linux' ? {iconPath} : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return {action: 'deny'}
})
// 初始化自动更新
autoUpdater = new AutoUpdater(mainWindow);
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadURL('app://./index.html#/')
}
ipcMain.on('adjust-window-height', (_, height: number) => {
console.log(height, '--------------->')
adjustWindowSize(height)
})
// IPC 通信处理
ipcMain.on('window-minimize', () => {
mainWindow?.hide()
})
ipcMain.on('window-close', () => {
mainWindow?.hide()
})
mainWindow.on('ready-to-show', positionWindow)
mainWindow.on('show', positionWindow)
mainWindow.on('close', (e) => {
e.preventDefault()
mainWindow?.hide()
})
}
function positionWindow() {
if (!mainWindow) return
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const {width, height} = display.workArea
const [winWidth, winHeight] = mainWindow.getSize()
mainWindow.setPosition(
width - winWidth - 10,
height - winHeight - 10
)
}
// 调整窗口尺寸
function adjustWindowSize(contentHeight: number) {
if (!mainWindow) return
mainWindow.setContentSize(mainWindow.getContentBounds().width, contentHeight)
// 保持右下角位置
positionWindow()
mainWindow.show()
}
app.whenReady().then(() => {
// Set app user model id for windows
protocol.registerFileProtocol('app', (request, callback) => {
const url = new URL(request.url).pathname; // 解析路径
const decodedUrl = decodeURIComponent(url); // 处理中文路径
const baseDir = join(__dirname, '../renderer');
const filePath = join(baseDir, decodedUrl);
// 返回文件或错误
if (fs.existsSync(filePath)) {
callback(filePath);
} else {
console.error('文件未找到:', filePath);
callback({ error: -6 }); // 错误码 -6 表示文件不存在
}
});
electronApp.setAppUserModelId('com.electron')
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
createWindow()
createTray()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
主进程auto-update.ts文件:(可以复制直接使用)
import { autoUpdater, UpdateInfo } from 'electron-updater';
import {BrowserWindow, ipcMain, app, globalShortcut} from 'electron';
import {is} from '@electron-toolkit/utils'
import * as path from 'path';
export default class AutoUpdater {
mainWindow: BrowserWindow;
public updateWindow: BrowserWindow | null = null;
private updateVersionInfo: any
constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow;
this.configure();
this.setupListeners();
this.setupIPC();
this.initializeAutoCheck()
}
// 新增初始化自动检查方法
public initializeAutoCheck() {
if (!app.isPackaged) {
// 开发环境延迟3秒检查,避免干扰调试
setTimeout(() => {
autoUpdater.checkForUpdates().catch(err => {
console.error('Dev update check failed:', err);
});
}, 3000);
} else {
// 生产环境立即检查
autoUpdater.checkForUpdates();
}
}
private configure(): void {
autoUpdater.autoDownload = false;
autoUpdater.allowDowngrade = false;
// 开发环境配置
if (!app.isPackaged) {
autoUpdater.forceDevUpdateConfig = true;
autoUpdater.updateConfigPath = path.join(__dirname, '../../dev-app-update.yml');
autoUpdater.setFeedURL({
provider: 'generic',
url: 'http://101.42.117.183:3000',
channel: 'latest'
});
} else {
autoUpdater.setFeedURL({
provider: 'generic',
url: 'http://192.170.78.131/updateApp',
channel: 'latest'
});
}
}
private setupListeners(): void {
autoUpdater.on('update-available', (info: UpdateInfo) => {
const currentVersion = app.getVersion();
if (info.version === currentVersion) {
this.sendToUpdateWindow('update-not-available');
return;
}
this.updateVersionInfo = {...info, oldVersion: currentVersion}
this.showUpdateWindow();
});
autoUpdater.on('download-progress', (progress) => {
this.sendToUpdateWindow('download-progress', {
percent: Math.floor(progress.percent),
speed: `${(progress.bytesPerSecond / 1024 / 1024).toFixed(1)}MB/s`,
downloaded: `${(progress.transferred / 1024 / 1024).toFixed(1)}MB`,
total: `${(progress.total / 1024 / 1024).toFixed(1)}MB`
});
});
autoUpdater.on('update-downloaded', () => {
this.sendToUpdateWindow('update-downloaded');
});
autoUpdater.on('error', (err) => {
this.sendToUpdateWindow('update-error', err.message);
});
}
private setupIPC(): void {
ipcMain.handle('start-download', () => autoUpdater.downloadUpdate());
ipcMain.handle('install-update', () => autoUpdater.quitAndInstall());
ipcMain.handle('check-update', () => autoUpdater.checkForUpdates());
ipcMain.handle("close-window", () => {
if (this.updateWindow) this.updateWindow.close()
})
ipcMain.handle("install-app", () => autoUpdater.quitAndInstall())
}
private showUpdateWindow(): void {
if (!this.updateWindow) {
this.updateWindow = new BrowserWindow({
width: 400,
height: 400,
show: false,
frame: false,
resizable: false,
transparent: true,
alwaysOnTop: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
nodeIntegration: false,
contextIsolation: true
}
});
if (process.platform === 'win32') {
this.updateWindow.setHasShadow(true)
}
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.updateWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/update`)
} else {
// this.updateWindow.loadFile(join(__dirname, '../renderer/index.html/update'))
this.updateWindow.loadURL(`app://./index.html#/update`)
}
this.updateWindow.webContents.on('did-finish-load', () => {
this.sendToUpdateWindow('update-available', {
oldVersion: this.updateVersionInfo.oldVersion,
version: this.updateVersionInfo.version,
size: (this.updateVersionInfo.files[0].size / 1024 / 1024).toFixed(1) + 'MB'
});
});
globalShortcut.register('Alt+CommandOrControl+I', () => {
this.updateWindow?.webContents.openDevTools()
})
this.updateWindow.on('ready-to-show', () => {
this.updateWindow!.show();
});
this.updateWindow.on('closed', () => {
this.updateWindow = null;
});
}
}
private sendToUpdateWindow(channel: string, ...args: any[]): void {
console.log(`[Main] 发送事件 "${channel}" 到更新窗口,窗口状态:`, {
exists: !!this.updateWindow,
destroyed: this.updateWindow?.isDestroyed()
});
if (this.updateWindow && !this.updateWindow.isDestroyed()) {
this.updateWindow.webContents.send(channel, ...args);
} else {
console.error(`[Main] 无法发送事件 "${channel}":窗口未初始化或已销毁`);
}
}
}
preload/idnex.ts文件:
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable: (callback) => {
// 确保事件名称与主进程发送的完全一致
ipcRenderer.on('update-available', (_, info) => {
callback(info);
});
},
onDownloadProgress: (callback) => {
ipcRenderer.on('download-progress', (_, info) => {
callback(info)
})
},
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),
startDownload: () => ipcRenderer.invoke('start-download'),
closeWindow: () => ipcRenderer.invoke("close-window"),
installApp: () => ipcRenderer.invoke("install-app")
});
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
}
我这里采用的是vue 路由hash方式
renderer下面的update.vue文件:
<script setup lang="ts">
import {onMounted, ref} from "vue"
import {IUpdateProgress} from "../../../types/auto-updater";
const versionTitleRef = ref(null)
const progressBarRef = ref(null)
const progressRingRef = ref(null)
const speedRef = ref(null)
const fileSizeRef = ref(null)
const totalRef = ref(null)
const isDownloading = ref(false)
const fileInfo = ref({})
const version = ref("V0.0.1")
const fileSize = ref("0MB")
const total = ref("0MB")
const speed = ref("0MB/s")
const versionTitle = ref("发现新版本可用")
const isDownloaded = ref(false)
const electronAPI: any = (window as any).electronAPI
const circumference = ref(0)
const setProgress = (percent: any) => {
if (!isDownloading.value) {
// 初次开始下载时添加downloading类
progressRingRef.value.classList.add('downloading');
isDownloading.value = true;
}
const offset = circumference.value - (percent / parseFloat(fileInfo.value.size)) * circumference.value;
progressBarRef.value.style.strokeDashoffset = offset;
}
const onDownload = () => {
if (isDownloading.value) return
isDownloading.value = true;
electronAPI.startDownload()
}
const onInstall = () => {
electronAPI.installApp()
}
const onSkip = () => {
if (isDownloading.value) return
electronAPI.closeWindow()
}
const onDownloaded = () => {
isDownloaded.value = true
versionTitle.value = `下载完成`;
}
const setupListener = () => {
if (!electronAPI) {
console.error('electronAPI 未定义!');
} else {
console.log('electronAPI 已正确挂载:', Object.keys(electronAPI), electronAPI.onUpdateAvailable);
}
dealDefaultProgress()
electronAPI.onDownloadProgress((progress: IUpdateProgress) => {
console.log(progress, '下载进度')
if (progress) {
if (progress.speed) speed.value = `${progress.speed}`;
if (progress.downloaded) {
fileSize.value = `${progress.downloaded}`;
setProgress(parseFloat(progress.downloaded))
}
}
});
electronAPI.onUpdateAvailable((info: any) => {
console.log(info, 'eeeeeeee')
version.value = `V ${info.oldVersion}`
versionTitle.value = `发现新版本 ${info.version} (${info.size})`;
total.value = info.size;
fileInfo.value = info
});
electronAPI.onUpdateDownloaded((info: any) => {
console.log("下载完成", info)
fileSize.value = `${fileInfo.value.size}`;
speed.value = "0MB/s"
setProgress(parseFloat(fileInfo.value.size))
onDownloaded()
})
}
const dealDefaultProgress = () => {
console.log(progressBarRef.value)
const radius = progressBarRef.value.r.baseVal.value;
circumference.value = radius * 2 * Math.PI;
progressBarRef.value.style.strokeDasharray = circumference.value;
progressBarRef.value.style.strokeDashoffset = circumference.value;
}
onMounted(() => {
setupListener()
})
</script>
<template>
<div class="container">
<div ref="progressRingRef" class="progress-ring">
<svg class="progress-circle">
<circle class="progress-background" cx="100" cy="100" r="90"/>
<circle ref="progressBarRef" class="progress-bar" cx="100" cy="100" r="90" stroke-dasharray="439.6"/>
</svg>
<div class="version-info">
<div class="version-number" ref="versionRef" id="version-number">{{ version }}</div>
<div class="download-info">
<div ref="fileSizeRef" class="file-size" id="file-size">{{ fileSize }}</div>
/
<span ref="totalRef" id="total">{{ total }}</span>
</div>
<div ref="speedRef" class="speed" id="speed">{{ speed }}</div>
</div>
</div>
<div ref="versionTitleRef" id="update-status">{{ versionTitle }}</div>
<div class="button-group">
<button v-if="!isDownloaded" :class="`btn download-btn ${isDownloading ? 'disabled' : ''}`"
id="download-btn" @click="onDownload">{{isDownloading ? "下载中..." : "立即下载"}}</button>
<button v-if="!isDownloaded" :class="`btn skip-btn ${isDownloading ? 'disabled' : ''}`" id="skip-btn" @click="onSkip">跳过此版本</button>
<button v-if="isDownloaded" class="btn install-btn" id="install-btn" @click="onInstall">开始安装</button>
</div>
</div>
</template>
<style scoped>
:root {
--primary: #3498db;
--secondary: #95a5a6;
}
body {
background: transparent;
margin: 0;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: rgba(255, 255, 255, 0.95);
border-radius: 24px;
padding: 40px;
//box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
//margin: 10px;
width: 400px;
text-align: center;
-webkit-app-region: drag;
overflow: hidden;
}
.progress-ring {
position: relative;
width: 200px;
height: 200px;
margin: 0 auto 25px;
}
.progress-circle {
transform: rotate(-90deg);
width: 100%;
height: 100%;
}
.progress-circle circle {
fill: none;
stroke-width: 6;
stroke-linecap: round;
}
.progress-background {
stroke: #eee;
}
.progress-bar {
stroke: #3498db;
transition: stroke-dashoffset 0.3s ease;
}
.version-info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.version-number {
font-size: 24px;
font-weight: 600;
color: #2c3e50;
}
.download-info {
display: flex;
align-items: center;
justify-content: center;
color: #7f8c8d;
font-size: 14px;
margin-top: 8px;
}
.file-size, .speed {
color: #7f8c8d;
font-size: 14px;
}
.file-size {
margin-right: 5px;
}
#total {
margin-left: 5px;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 30px;
-webkit-app-region: no-drag;
}
.btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.disabled{
opacity: .5;
cursor: not-allowed;
}
.download-btn, .install-btn {
background: #3498db;
color: white;
}
.skip-btn {
background: #ecf0f1;
color: #95a5a6;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
}
#update-status {
color: #7f8c8d;
margin: 20px 0;
}
</style>
实现效果:
托盘右键可以点击检查更新