Electron
Electron 是一个基于 Chromium + Node.js 的跨平台桌面应用开发框架。
三层架构模型
要把指令发给操作系统,必须经过这条路径:
- 渲染进程 (Renderer):Vue 界面,负责“发号施令”(如:点击按钮)。
- 预加载脚本 (Preload):中间人,负责“传话”。基于安全考虑,它是唯一能同时接触到 Vue 和 Node.js 的地方。
- 主进程 (Main):真正的执行者,负责“干活”(如:读写文件、弹对话框)。
官网链接
快速创建项目
创建项目
pnpm view @quick-start/create-electron versions
pnpm create @quick-start/electron@1.0.30 electron-demo2
选择:
√ Select a framework: » vue
√ Add TypeScript? ... No / Yes
√ Add Electron updater plugin? ... No / Yes
√ Enable Electron download mirror proxy? ... No / Yes2
3
4
安装依赖
cd electron-demo
pnpm install2
启动开发环境
pnpm run dev
集成 ElementPlus
安装 ElementPlus
pnpm add element-plus@2.13.0在 Vue 项目中全局引入 ElementPlus
编辑 src/renderer/main.ts:
import { createApp } from 'vue'
import App from './App.vue'
// 引入 ElementPlus 样式和组件
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus) // 全局注册 ElementPlus
app.mount('#app')2
3
4
5
6
7
8
9
10
使用示例
在 src/renderer/App.vue 中使用 ElementPlus 组件:
<template>
<el-container class="app-wrapper">
<el-aside width="200px" class="aside">
<div class="brand">
<el-icon color="#409EFF" :size="24"><Management /></el-icon>
<span>控制台</span>
</div>
<el-menu default-active="1" class="border-none">
<el-menu-item index="1">
<el-icon><House /></el-icon>
<span>首页概览</span>
</el-menu-item>
<el-menu-item index="2">
<el-icon><Files /></el-icon>
<span>资源管理</span>
</el-menu-item>
<el-menu-item index="3">
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<el-breadcrumb separator="/">
<el-breadcrumb-item>项目</el-breadcrumb-item>
<el-breadcrumb-item>当前页面</el-breadcrumb-item>
</el-breadcrumb>
<div class="user-info">
<el-button type="primary" plain size="small" @click="handleRefresh">刷新数据</el-button>
</div>
</el-header>
<el-main class="main">
<el-row :gutter="20" class="margin-b-20">
<el-col :span="8" v-for="i in 3" :key="i">
<el-card shadow="never">
<template #header>指标 0{{ i }}</template>
<div class="card-value">1,28{{ i }}</div>
<el-text size="small" type="info">较昨日 +12%</el-text>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" header="参数配置">
<el-form :model="form" label-width="100px" style="max-width: 500px">
<el-form-item label="任务名称">
<el-input v-model="form.name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="优先级">
<el-radio-group v-model="form.priority">
<el-radio label="紧急" />
<el-radio label="普通" />
</el-radio-group>
</el-form-item>
<el-form-item label="备注说明">
<el-input v-model="form.desc" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交保存</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { Management, House, Files, Setting } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const form = reactive({
name: '',
priority: '普通',
desc: ''
})
const onSubmit = () => {
ElMessage.success('配置已成功同步')
}
const onReset = () => {
form.name = ''
form.priority = '普通'
form.desc = ''
}
const handleRefresh = () => {
ElMessage('正在同步最新数据...')
}
</script>
<style scoped>
/* 基础布局控制 */
.app-wrapper {
height: 100vh;
background-color: #f5f7fa; /* 淡淡的底色让白色卡片更立体 */
}
/* 侧边栏 */
.aside {
background-color: #ffffff;
border-right: 1px solid #e6e6e6;
}
.brand {
height: 60px;
display: flex;
align-items: center;
padding: 0 20px;
gap: 10px;
font-weight: bold;
font-size: 18px;
border-bottom: 1px solid #f0f0f0;
}
.border-none {
border: none;
}
/* 顶部栏 */
.header {
background-color: #ffffff;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
/* 内容区 */
.main {
padding: 20px;
}
.margin-b-20 {
margin-bottom: 20px;
}
.card-value {
font-size: 28px;
font-weight: bold;
color: #303133;
}
/* 统一卡片样式:去除圆角阴影,走极简风 */
:deep(.el-card) {
border-radius: 4px;
border: 1px solid #e6e6e6;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155

调用操作系统
系统对话框(选择文件/保存)
这是最常见的需求,比如让用户选择一个文件夹或图片。
主进程 (src/main/index.ts):
在主进程中监听渲染进程发来的 select-file 请求,并调用系统对话框。
import { app, shell, BrowserWindow, ipcMain, dialog } from 'electron'
import { join } from 'path'
// ... 其他默认引入
function createWindow(): void {
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
// --- 核心代码:处理文件选择请求 ---
ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({
title: '请选择图片文件',
properties: ['openFile', 'multiSelections'],
filters: [{ name: 'Images', extensions: ['jpg', 'png', 'gif'] }]
})
if (result.canceled) {
return []
} else {
return result.filePaths // 返回路径数组
}
})
// ------------------------------
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
// ... 其他加载逻辑
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
预加载脚本 (src/preload/index.ts):
这是安全屏障。我们在这里把主进程的能力暴露给 window 对象。
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露一个名为 selectFile 的方法供 Vue 调用
selectFile: () => ipcRenderer.invoke('select-file')
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
类型声明 src/preload/index.d.ts
declare global {
interface Window {
electronAPI: {
selectFile: () => Promise<string[]>
}
}
}2
3
4
5
6
7
渲染进程:Vue 界面实现 (src/renderer/src/App.vue):
这里我们使用 ElementPlus 做一个干净的白色界面,展示选择结果。
<template>
<el-container class="app-container">
<el-main>
<el-card shadow="never" header="系统对话框集成示例">
<div class="action-bar">
<el-button type="primary" @click="handleOpenFile">
<el-icon style="margin-right: 4px;"><FolderOpened /></el-icon>
选择本地图片
</el-button>
<el-text class="hint" type="info">调用 electron.dialog 模块</el-text>
</div>
<el-divider content-position="left">选择结果</el-divider>
<div v-if="fileList.length > 0" class="result-list">
<el-alert
v-for="(path, index) in fileList"
:key="index"
:title="path"
type="success"
:closable="false"
show-icon
style="margin-bottom: 8px;"
/>
</div>
<el-empty v-else description="暂未选择任何文件" :image-size="100" />
</el-card>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { FolderOpened } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const fileList = ref<string[]>([])
const handleOpenFile = async () => {
try {
// 调用 preload 中暴露的接口
const paths = await window.electronAPI.selectFile()
if (paths && paths.length > 0) {
fileList.value = paths
ElMessage.success(`成功选择 ${paths.length} 个文件`)
} else {
ElMessage.info('取消了选择')
}
} catch (error) {
ElMessage.error('调用系统对话框失败')
console.error(error)
}
}
</script>
<style>
/* 全局清除默认边距,确保填充 */
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
background-color: #f5f7fa;
}
.app-container {
height: 100vh;
}
.action-bar {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.result-list {
max-height: 400px;
overflow-y: auto;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

系统信息与内存监控
这个示例演示如何从操作系统获取硬件状态,并在 Vue 界面中以图形化方式展示。
- 主进程 (
src/main/index.ts)
我们引入 Node.js 的 os 模块,通过 ipcMain 定时或按需返回内存数据。
TypeScript
import { ipcMain, app } from 'electron'
import os from 'os'
// 处理获取系统静态信息的请求
ipcMain.handle('get-system-info', () => {
return {
platform: os.platform(), // 操作系统平台
arch: os.arch(), // 架构
cpuModel: os.cpus()[0].model,
totalMem: (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'
}
})
// 处理获取动态内存占用情况
ipcMain.handle('get-memory-usage', () => {
const free = os.freemem()
const total = os.totalmem()
const usedPercentage = ((total - free) / total * 100).toFixed(1)
return parseFloat(usedPercentage)
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 预加载脚本 (
src/preload/index.ts)
将这两个新方法暴露给渲染进程。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
selectFile: () => ipcRenderer.invoke('select-file'),
// 新增:获取静态系统信息
getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
// 新增:获取动态内存百分比
getMemoryUsage: () => ipcRenderer.invoke('get-memory-usage')
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 类型声明 (
src/preload/index.d.ts)
补充接口定义,保证 VS Code 里的代码补全和类型检查。
TypeScript
declare global {
interface Window {
electronAPI: {
selectFile: () => Promise<string[]>,
getSystemInfo: () => Promise<{
platform: string,
arch: string,
cpuModel: string,
totalMem: string
}>,
getMemoryUsage: () => Promise<number>
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
- 渲染进程 (
src/renderer/src/App.vue)
利用 Element Plus 的 el-descriptions 展示信息,用 el-progress 制作内存监控仪表盘。
代码段
<template>
<el-container class="app-container">
<el-main>
<el-row :gutter="20">
<el-col :span="14">
<el-card shadow="never" header="硬件配置">
<el-descriptions :column="1" border>
<el-descriptions-item label="操作系统">{{ sysInfo.platform }} ({{ sysInfo.arch }})</el-descriptions-item>
<el-descriptions-item label="处理器">{{ sysInfo.cpuModel }}</el-descriptions-item>
<el-descriptions-item label="物理内存">{{ sysInfo.totalMem }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="10">
<el-card shadow="never" header="内存占用率">
<div class="gauge-container">
<el-progress
type="dashboard"
:percentage="memoryUsage"
:color="customColors"
/>
<p class="monitor-text">每 2 秒实时刷新</p>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const sysInfo = ref({ platform: '', arch: '', cpuModel: '', totalMem: '' })
const memoryUsage = ref(0)
let timer: any = null
// 进度条颜色分段
const customColors = [
{ color: '#67c23a', percentage: 40 },
{ color: '#e6a23c', percentage: 70 },
{ color: '#f56c6c', percentage: 100 },
]
onMounted(async () => {
// 1. 初始化获取静态信息
sysInfo.value = await window.electronAPI.getSystemInfo()
// 2. 开启定时轮询获取内存
updateMemory()
timer = setInterval(updateMemory, 2000)
})
const updateMemory = async () => {
memoryUsage.value = await window.electronAPI.getMemoryUsage()
}
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style scoped>
.gauge-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
}
.monitor-text {
font-size: 12px;
color: #909399;
margin-top: 10px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

系统原生通知发送
- 主进程 (
src/main/index.ts)
我们引入 Notification 模块。注意:某些系统(如 Windows)发送通知需要设置 AppUserModelId。
TypeScript
import { ipcMain, Notification } from 'electron'
// 处理发送通知的请求
ipcMain.on('send-os-notification', (_event, title, body) => {
// 检查系统是否支持通知
if (Notification.isSupported()) {
const notice = new Notification({
title: title, // 通知标题
body: body, // 通知内容
silent: false, // 是否静音
timeoutType: 'default'
})
notice.show()
// 也可以监听点击事件
notice.on('click', () => {
console.log('用户点击了通知')
})
}
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 预加载脚本 (
src/preload/index.ts)
由于发送通知通常不需要等待返回值,我们使用 send 而不是 invoke。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
selectFile: () => ipcRenderer.invoke('select-file'),
getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
getMemoryUsage: () => ipcRenderer.invoke('get-memory-usage'),
// 新增:向主进程发送通知指令
sendNotification: (title: string, body: string) =>
ipcRenderer.send('send-os-notification', title, body)
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 类型声明 (
src/preload/index.d.ts)
在接口中添加该方法的定义。
TypeScript
declare global {
interface Window {
electronAPI: {
selectFile: () => Promise<string[]>,
getSystemInfo: () => Promise<{
platform: string,
arch: string,
cpuModel: string,
totalMem: string
}>,
getMemoryUsage: () => Promise<number>,
// 新增声明
sendNotification: (title: string, body: string) => void
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 渲染进程 (
src/renderer/src/App.vue)
在 Vue 中,我们做一个简单的表单,让用户可以自定义通知内容。
代码段
<template>
<el-container class="app-container">
<el-main>
<el-card shadow="never" header="系统通知推送">
<el-form label-width="80px" style="max-width: 400px">
<el-form-item label="通知标题">
<el-input v-model="noticeForm.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="通知内容">
<el-input
v-model="noticeForm.body"
type="textarea"
placeholder="请输入想要推送的详细内容"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="pushNotification">
<el-icon style="margin-right: 4px;"><Bell /></el-icon>
立即推送
</el-button>
</el-form-item>
</el-form>
<el-divider />
<el-text type="info">
<el-icon><InfoFilled /></el-icon>
点击按钮后,你将在操作系统的通知中心看到这条消息。
</el-text>
</el-card>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { Bell, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const noticeForm = reactive({
title: '来自 Electron 的问候',
body: '这是一条通过系统原生 API 发送的通知测试。'
})
const pushNotification = () => {
if (!noticeForm.title || !noticeForm.body) {
ElMessage.warning('标题和内容不能为空')
return
}
// 调用 preload 中的接口
window.electronAPI.sendNotification(noticeForm.title, noticeForm.body)
ElMessage.success('指令已发送')
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

外壳操作 (Shell) 集成
- 主进程 (
src/main/index.ts)
我们引入 shell 模块。主进程负责处理打开外部链接和定位文件的逻辑。
TypeScript
import { ipcMain, shell } from 'electron'
// 监听打开外部链接的请求
ipcMain.on('open-external-url', (_event, url: string) => {
shell.openExternal(url)
})
// 监听在文件夹中显示文件的请求
ipcMain.on('show-item-in-folder', (_event, filePath: string) => {
shell.showItemInFolder(filePath)
})2
3
4
5
6
7
8
9
10
11
- 预加载脚本 (
src/preload/index.ts)
将这两个 Shell 操作安全地暴露给 Vue。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
selectFile: () => ipcRenderer.invoke('select-file'),
getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
getMemoryUsage: () => ipcRenderer.invoke('get-memory-usage'),
sendNotification: (title: string, body: string) => ipcRenderer.send('send-os-notification', title, body),
// 新增:Shell 相关操作
openUrl: (url: string) => ipcRenderer.send('open-external-url', url),
showFile: (path: string) => ipcRenderer.send('show-item-in-folder', path)
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 类型声明 (
src/preload/index.d.ts)
补充接口定义,让开发环境不报错。
TypeScript
declare global {
interface Window {
electronAPI: {
selectFile: () => Promise<string[]>,
getSystemInfo: () => Promise<{ platform: string, arch: string, cpuModel: string, totalMem: string }>,
getMemoryUsage: () => Promise<number>,
sendNotification: (title: string, body: string) => void,
// 新增声明
openUrl: (url: string) => void,
showFile: (path: string) => void
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
- 渲染进程 (
src/renderer/src/App.vue)
我们结合之前的“文件选择”示例,实现一个功能:选中文件后,点击按钮可以直接在 Windows 资源管理器(或 Mac Finder)中定位到它。
代码段
<template>
<el-container class="app-container">
<el-main>
<el-card shadow="never" header="外壳操作 (Shell) 示例">
<div class="section">
<el-text size="large" strong>1. 外部链接</el-text>
<p class="description">在系统默认浏览器中打开指定网址:</p>
<el-button type="primary" plain @click="handleOpenLink">
<el-icon style="margin-right: 4px;"><Link /></el-icon>
访问 Electron 官网
</el-button>
</div>
<el-divider />
<div class="section">
<el-text size="large" strong>2. 文件定位</el-text>
<p class="description">先选择一个文件,然后尝试在文件夹中显示它:</p>
<el-space direction="vertical" alignment="start">
<el-button type="success" @click="handlePickFile">
第一步:选择文件
</el-button>
<div v-if="targetPath" class="path-box">
<el-tag closable @close="targetPath = ''">{{ targetPath }}</el-tag>
<el-button
type="info"
link
style="margin-left: 10px;"
@click="handleShowInFolder"
>
<el-icon><Position /></el-icon> 在文件夹中显示
</el-button>
</div>
</el-space>
</div>
</el-card>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Link, Position } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const targetPath = ref('')
// 打开网页
const handleOpenLink = () => {
window.electronAPI.openUrl('https://www.electronjs.org/')
}
// 选择文件(复用之前的逻辑)
const handlePickFile = async () => {
const paths = await window.electronAPI.selectFile()
if (paths && paths.length > 0) {
targetPath.value = paths[0]
}
}
// 在资源管理器中打开并选中
const handleShowInFolder = () => {
if (!targetPath.value) return
window.electronAPI.showFile(targetPath.value)
ElMessage.info('已请求系统打开目录')
}
</script>
<style scoped>
.section {
margin-bottom: 20px;
}
.description {
font-size: 14px;
color: #606266;
margin: 10px 0;
}
.path-box {
margin-top: 15px;
padding: 10px;
background-color: #f0f2f5;
border-radius: 4px;
display: flex;
align-items: center;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

剪贴板与电源状态监控
- 主进程 (
src/main/index.ts)
我们引入 clipboard 和 powerMonitor 模块。注意:电量信息可以通过 Web 标准 API 获取,但主进程可以监控系统休眠、锁屏等更底层的事件。
TypeScript
import { ipcMain, clipboard, powerMonitor } from 'electron'
// --- 剪贴板处理 ---
ipcMain.handle('read-clipboard', () => {
return clipboard.readText()
})
ipcMain.on('write-clipboard', (_event, text: string) => {
clipboard.writeText(text)
})
// --- 电源/系统状态监控 ---
// 监听系统休眠(这是一个主动推送的例子,也可以通过 invoke 获取)
ipcMain.handle('get-system-idle-state', () => {
return powerMonitor.getSystemIdleState(1) // 获取当前是否为空闲
})
// 监听锁屏事件(示例:发送给渲染进程)
powerMonitor.on('lock-screen', () => {
// 可以通过 mainWindow.webContents.send 通知前端
console.log('系统已锁屏')
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 预加载脚本 (
src/preload/index.ts)
将剪贴板的读写方法和电源状态查询暴露给 Vue。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
// 剪贴板
copyText: (text: string) => ipcRenderer.send('write-clipboard', text),
readText: () => ipcRenderer.invoke('read-clipboard'),
// 电源状态
getIdleState: () => ipcRenderer.invoke('get-system-idle-state'),
// 复用之前的接口...
selectFile: () => ipcRenderer.invoke('select-file'),
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 类型声明 (
src/preload/index.d.ts)
定义接口,确保 TypeScript 的严谨性。
TypeScript
declare global {
interface Window {
electronAPI: {
copyText: (text: string) => void,
readText: () => Promise<string>,
getIdleState: () => Promise<string>,
// ... 其他已有的声明
}
}
}2
3
4
5
6
7
8
9
10
- 渲染进程 (
src/renderer/src/App.vue)
在界面上,我们实现一个剪贴板管理器,并利用浏览器原生的 navigator.getBattery() 配合 Electron 的状态查询。
代码段
<template>
<el-container class="app-container">
<el-main>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="never" header="剪贴板助手">
<el-input
v-model="copyInput"
placeholder="输入要复制的内容"
class="margin-b-10"
>
<template #append>
<el-button @click="handleCopy">复制</el-button>
</template>
</el-input>
<el-divider>读取测试</el-divider>
<div class="clipboard-box">
<el-button type="success" plain @click="handleRead">
从系统剪贴板读取
</el-button>
<p class="result-text">内容:{{ clipboardContent || '暂无内容' }}</p>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never" header="电源与状态">
<div class="battery-info">
<el-statistic title="当前电量" :value="batteryLevel * 100" suffix="%">
<template #prefix>
<el-icon :color="batteryLevel > 0.2 ? '#67c23a' : '#f56c6c'">
<Lightning />
</el-icon>
</template>
</el-statistic>
<el-divider direction="vertical" />
<el-tag :type="isCharging ? 'success' : 'info'">
{{ isCharging ? '正在充电' : '使用电池中' }}
</el-tag>
</div>
<div class="system-state margin-t-20">
<el-button size="small" @click="checkIdle">检查系统是否空闲</el-button>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Lightning } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 剪贴板逻辑
const copyInput = ref('')
const clipboardContent = ref('')
const handleCopy = () => {
if (!copyInput.value) return
window.electronAPI.copyText(copyInput.value)
ElMessage.success('已写入剪贴板')
}
const handleRead = async () => {
clipboardContent.value = await window.electronAPI.readText()
ElMessage.info('读取成功')
}
// 电源逻辑 (结合 Web API)
const batteryLevel = ref(0)
const isCharging = ref(false)
onMounted(async () => {
// 获取电池对象 (浏览器原生 API 在 Electron 中同样适用)
const battery: any = await (navigator as any).getBattery()
batteryLevel.value = battery.level
isCharging.value = battery.charging
// 监听电池变化
battery.addEventListener('levelchange', () => {
batteryLevel.value = battery.level
})
})
const checkIdle = async () => {
const state = await window.electronAPI.getIdleState()
ElMessage(`当前系统状态: ${state}`)
}
</script>
<style scoped>
.margin-b-10 { margin-bottom: 10px; }
.margin-t-20 { margin-top: 20px; }
.clipboard-box {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
}
.result-text {
margin-top: 10px;
font-size: 13px;
color: #606266;
word-break: break-all;
}
.battery-info {
display: flex;
align-items: center;
justify-content: space-around;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

自定义窗口控制 (无边框窗口)
- 主进程 (
src/main/index.ts)
在创建窗口时设置 frame: false 以隐藏系统边框。同时通过 ipcMain 监听来自前端的窗口指令(最小化、最大化、关闭)。
TypeScript
import { app, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
function createWindow(): void {
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
frame: false, // 核心:隐藏系统默认标题栏和边框
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
// --- 核心代码:窗口控制逻辑 ---
ipcMain.on('window-control', (_event, action: 'minimize' | 'maximize' | 'close') => {
const win = BrowserWindow.getFocusedWindow()
if (!win) return
switch (action) {
case 'minimize':
win.minimize()
break
case 'maximize':
// 切换最大化与还原状态
win.isMaximized() ? win.unmaximize() : win.maximize()
break
case 'close':
win.close()
break
}
})
// ------------------------------
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- 预加载脚本 (
src/preload/index.ts)
将窗口控制指令安全地暴露给 window.electronAPI。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露窗口控制方法
controlWindow: (action: 'minimize' | 'maximize' | 'close') =>
ipcRenderer.send('window-control', action),
// 保持之前的接口...
readText: () => ipcRenderer.invoke('read-clipboard')
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 类型声明 (
src/preload/index.d.ts)
更新 TypeScript 定义,确保开发时的代码补全。
TypeScript
declare global {
interface Window {
electronAPI: {
controlWindow: (action: 'minimize' | 'maximize' | 'close') => void,
readText: () => Promise<string>
// ... 其他已有的声明
}
}
}2
3
4
5
6
7
8
9
- 渲染进程 (
src/renderer/src/App.vue)
在 Vue 中手动构建标题栏。注意:CSS 中的 -webkit-app-region 属性是实现拖拽的关键。
代码段
<template>
<div class="window-wrapper">
<header class="custom-title-bar">
<div class="drag-area">
<el-icon class="app-logo"><Platform /></el-icon>
<span class="app-title">我的桌面应用</span>
</div>
<div class="control-group">
<div class="btn minimize" @click="handleAction('minimize')">
<el-icon><SemiSelect /></el-icon>
</div>
<div class="btn maximize" @click="handleAction('maximize')">
<el-icon><FullScreen /></el-icon>
</div>
<div class="btn close" @click="handleAction('close')">
<el-icon><Close /></el-icon>
</div>
</div>
</header>
<el-main class="page-body">
<el-card shadow="never">
<template #header>无边框模式已开启</template>
<el-result icon="success" title="自定义标题栏" sub-title="现在你可以完全控制窗口的外观了">
<template #extra>
<el-button type="primary">开始探索</el-button>
</template>
</el-result>
</el-card>
</el-main>
</div>
</template>
<script setup lang="ts">
import { Platform, SemiSelect, FullScreen, Close } from '@element-plus/icons-vue'
const handleAction = (action: 'minimize' | 'maximize' | 'close') => {
window.electronAPI.controlWindow(action)
}
</script>
<style>
/* 确保应用铺满整个窗口 */
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
.window-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
/* 加上淡淡的边框,防止无边框窗口在纯白背景下看不见边缘 */
border: 1px solid #ebeef5;
box-sizing: border-box;
}
/* 标题栏样式 */
.custom-title-bar {
height: 32px;
background-color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none; /* 禁止选中文本 */
}
/* 核心:可拖拽区域 */
.drag-area {
flex: 1;
height: 100%;
display: flex;
align-items: center;
padding-left: 12px;
font-size: 12px;
color: #606266;
-webkit-app-region: drag; /* 标记为可拖动窗口区域 */
}
.app-logo {
margin-right: 8px;
color: #409EFF;
}
/* 核心:操作按钮区域必须设为 no-drag */
.control-group {
display: flex;
height: 100%;
-webkit-app-region: no-drag; /* 恢复点击,否则无法触发 click 事件 */
}
.btn {
width: 46px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s;
color: #909399;
}
.btn:hover {
background-color: #f5f7fa;
}
/* 关闭按钮通常使用红色高亮 */
.btn.close:hover {
background-color: #f56c6c;
color: #ffffff;
}
.page-body {
flex: 1;
background-color: #f5f7fa;
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
实现原理与避坑
-webkit-app-region: drag:这个 CSS 属性让操作系统认为该 HTML 元素是窗口的“非客户区”,从而响应鼠标的拖拽操作。-webkit-app-region: no-drag:这是最容易忽略的点。如果没有在按钮上设置它,按钮会被拖拽层“覆盖”,导致无法响应click事件。- 双击行为:在设置了
drag的区域,Windows 和 macOS 默认都会支持双击最大化/还原窗口。 - 边框重要性:由于去掉了系统阴影和边框,建议在最外层 CSS 加一个
1px的边框或阴影,否则你的应用在白色桌面背景下会变得“隐形”。

本地配置持久化 (electron-store)
- 安装依赖
在项目根目录运行:
Bash
pnpm add electron-store@11.0.2- 主进程 (
src/main/index.ts)
在主进程中初始化 Store,并监听读写配置的请求。
TypeScript
import { app, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
// 定义全局 store 变量
let store: any
// 核心:异步初始化 Store
async function initStore() {
const { default: Store } = await import('electron-store')
store = new Store({
defaults: {
'user-settings': {
username: '新用户',
darkMode: false
}
}
})
}
function createWindow(): void {
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
// --- 核心逻辑:确保 store 已初始化再监听 ---
ipcMain.on('set-config', (_event, key: string, value: any) => {
store?.set(key, value)
})
ipcMain.handle('get-config', (_event, key: string) => {
return store?.get(key)
})
// ------------------------------------
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
}
// 修改 app 的启动逻辑
app.whenReady().then(async () => {
await initStore() // 1. 先初始化 Store
createWindow() // 2. 再创建窗口
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- 预加载脚本 (
src/preload/index.ts)
将配置操作接口通过桥接层暴露给 Vue。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
// 写入配置
setConfig: (key: string, value: any) => ipcRenderer.send('set-config', key, value),
// 读取配置
getConfig: (key: string) => ipcRenderer.invoke('get-config', key),
// 重置配置
resetConfig: () => ipcRenderer.send('reset-config'),
// 保持之前的控制指令等...
controlWindow: (action: string) => ipcRenderer.send('window-control', action)
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 类型声明 (
src/preload/index.d.ts)
更新 TypeScript 定义,确保 settings 对象有正确的提示。
TypeScript
declare global {
interface Window {
electronAPI: {
setConfig: (key: string, value: any) => void,
getConfig: (key: string) => Promise<any>,
resetConfig: () => void,
controlWindow: (action: string) => void
}
}
}2
3
4
5
6
7
8
9
10
- 渲染进程 (
src/renderer/src/App.vue)
在 Vue 中,我们实现一个设置表单。当用户修改设置时,实时同步到本地文件。
代码段
<template>
<el-container class="app-container">
<el-main>
<el-card shadow="never" header="应用个性化设置">
<el-form :model="settings" label-width="100px" style="max-width: 460px">
<el-form-item label="用户昵称">
<el-input
v-model="settings.username"
placeholder="请输入昵称"
@change="saveSettings"
/>
</el-form-item>
<el-form-item label="深色模式">
<el-switch v-model="settings.darkMode" @change="saveSettings" />
</el-form-item>
<el-form-item label="界面字号">
<el-slider
v-model="settings.fontSize"
:min="12"
:max="24"
@change="saveSettings"
/>
</el-form-item>
<el-divider />
<el-form-item>
<el-button type="danger" plain @click="handleReset">
<el-icon style="margin-right: 4px;"><RefreshRight /></el-icon>
恢复默认设置
</el-button>
</el-form-item>
</el-form>
<div class="status-tip">
<el-text type="info" size="small">
* 修改将实时保存至本地 JSON 文件,重启应用不会丢失。
</el-text>
</div>
</el-card>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { RefreshRight } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 使用 reactive 响应式对象存储设置
const settings = reactive({
username: '',
darkMode: false,
fontSize: 14
})
// 组件挂载时读取本地保存的配置
onMounted(async () => {
const saved = await window.electronAPI.getConfig('user-settings')
if (saved) {
// 将保存的值合并到响应式对象中
Object.assign(settings, saved)
}
})
// 保存配置到主进程
const saveSettings = () => {
window.electronAPI.setConfig('user-settings', { ...settings })
ElMessage.success('配置已自动保存')
}
// 重置配置逻辑
const handleReset = () => {
ElMessageBox.confirm('确定要重置所有配置吗?', '警告', {
type: 'warning'
}).then(() => {
window.electronAPI.resetConfig()
// 重新获取默认值或刷新页面
location.reload()
})
}
</script>
<style scoped>
.app-container {
height: 100vh;
background-color: #f5f7fa;
}
.status-tip {
margin-top: 20px;
padding: 10px;
background: #fff;
border-radius: 4px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
实现原理与优势
- 存储位置:
electron-store默认会将数据存储在系统的userData目录中。- Windows:
C:\Users\用户名\AppData\Roaming\你的应用名\config.json - macOS:
~/Library/Application Support/你的应用名/config.json
- Windows:
- 安全性:比起
localStorage,这种方式更难被用户误删,且支持更复杂的 JS 对象结构。 - 开发体验:在开发过程中,你可以直接打开这个
config.json文件查看数据,甚至手动修改它,应用读取时会自动同步。

主进程主动推送 (WebContents)
- 主进程 (
src/main/index.ts)
主进程使用 mainWindow.webContents.send 向渲染进程“发号施令”。我们以网络状态监控和系统心跳为例。
TypeScript
import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron'
function createWindow(): void {
const mainWindow = new BrowserWindow({
// ... 基础配置
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
// --- 核心代码:主动推送逻辑 ---
// 场景 A:定时发送系统信息(如心跳/时间)
setInterval(() => {
if (mainWindow) {
const now = new Date().toLocaleTimeString()
// 主动向渲染进程发送 'server-push-time' 消息
mainWindow.webContents.send('server-push-time', now)
}
}, 5000)
// 场景 B:监听系统事件(如深色模式切换)并推送
nativeTheme.on('updated', () => {
if (mainWindow) {
mainWindow.webContents.send('theme-changed', nativeTheme.shouldUseDarkColors)
}
})
// ------------------------------
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 预加载脚本 (
src/preload/index.ts)
预加载脚本不再是简单的调用,而是提供一个**“订阅器”**(监听器),让 Vue 能够绑定回调函数。
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electronAPI', {
// 定义监听器:接收一个 callback 函数作为参数
onUpdateTime: (callback: (time: string) => void) => {
// 监听来自主进程的消息
ipcRenderer.on('server-push-time', (_event, time) => callback(time))
},
// 定义监听器:接收深色模式状态
onThemeChange: (callback: (isDark: boolean) => void) => {
ipcRenderer.on('theme-changed', (_event, isDark) => callback(isDark))
}
})
} catch (error) {
console.error(error)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 类型声明 (
src/preload/index.d.ts)
为监听器函数定义类型,注意回调函数的参数类型。
TypeScript
declare global {
interface Window {
electronAPI: {
// 监听器函数定义
onUpdateTime: (callback: (time: string) => void) => void,
onThemeChange: (callback: (isDark: boolean) => void) => void,
// 保持之前的接口...
setConfig: (key: string, value: any) => void
}
}
}2
3
4
5
6
7
8
9
10
11
- 渲染进程 (
src/renderer/src/App.vue)
在 Vue 组件挂载时(onMounted)开启监听,并在卸载时(如有必要)移除监听。
代码段
<template>
<el-container class="app-container">
<el-main>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="never" header="主进程实时推送 (心跳)">
<template #extra>
<el-tag type="success" size="small">监听中</el-tag>
</template>
<div class="time-display">
<el-statistic title="系统同步时间" :value="currentTime" />
</div>
<p class="hint">主进程每 5 秒通过 webContents.send 主动推送</p>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never" header="系统主题状态">
<el-result
:icon="isDark ? 'info' : 'warning'"
:title="isDark ? '深色模式' : '浅色模式'"
sub-title="感知操作系统主题切换"
>
<template #icon>
<el-icon :size="48" :color="isDark ? '#409EFF' : '#E6A23C'">
<component :is="isDark ? 'Moon' : 'Sunny'" />
</el-icon>
</template>
</el-result>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Moon, Sunny } from '@element-plus/icons-vue'
const currentTime = ref('等待推送...')
const isDark = ref(false)
onMounted(() => {
// 1. 订阅时间推送
window.electronAPI.onUpdateTime((time: string) => {
currentTime.value = time
})
// 2. 订阅主题变化推送
window.electronAPI.onThemeChange((darkState: boolean) => {
isDark.value = darkState
})
})
</script>
<style scoped>
.app-container {
height: 100vh;
background-color: #f5f7fa;
}
.time-display {
padding: 20px 0;
text-align: center;
}
.hint {
font-size: 12px;
color: #909399;
text-align: center;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
💡 核心要点
- 单向流动:
webContents.send是主进程对渲染进程的“广播”或“定向投喂”。 - 生命周期:在 Vue 中,一旦你在
onMounted开启了监听,它会一直运行。如果你有多个页面,记得在onUnmounted中清理监听器,防止重复绑定(通过ipcRenderer.removeAllListeners)。 - 应用场景:除了示例中的心跳和主题,最常用的场景包括:检查更新进度、文件下载进度条、网络连通性监听(主进程检测
online/offline并通知前端)。
