Electron-vite【实战】MD 编辑器 -- 系统菜单(含菜单封装,新建文件,打开文件,打开文件夹,保存文件,退出系统)

article/2025/8/6 3:31:07

最终效果

在这里插入图片描述

整体架构

src/main/index.ts

import { createMenu } from './menu'

在 const mainWindow 后

  // 加载菜单createMenu(mainWindow)

src/main/menu.ts

import { BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, dialog, shell } from 'electron'
import fs from 'fs/promises'
import path from 'path'
import { FileItem } from '../types'
// 系统菜单
const createMenu = (mainWindow: BrowserWindow): void => {const menuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [{label: '文件',submenu: []}]const menu: Menu = Menu.buildFromTemplate(menuTemplate)Menu.setApplicationMenu(menu)
}

submenu 内添加自定义的菜单

src/types.ts

export interface FileItem {content: stringfileName: stringfilePath: stringeditable?: boolean
}

新建文件

在这里插入图片描述

src/main/menu.ts

        {label: '新建',accelerator: 'CmdOrCtrl+N',click: async () => {const { canceled, filePath } = await dialog.showSaveDialog({filters: [{name: 'Markdown Files',extensions: ['md']}]})if (!canceled) {try {await fs.writeFile(filePath, '')mainWindow.webContents.send('open-file', {content: '',filePath: filePath,fileName: path.basename(filePath)})} catch (error) {console.error('创建文件时出错:', error)}}}},

src/renderer/src/App.vue

  window.electron.ipcRenderer.on('open-file', (_, { content, fileName, filePath }) => {markdownContent.value = contentcurrentFilePath.value = filePathif (!isFileExists(filePath)) {fileList.value.unshift({content,fileName,filePath})}})

打开文件

在这里插入图片描述

src/main/menu.ts

        {label: '打开文件',accelerator: 'CmdOrCtrl+O',click: async () => {const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {filters: [{ name: 'Markdown Files', extensions: ['md', 'markdown'] }],properties: ['openFile']})if (!canceled) {const content = await fs.readFile(filePaths[0], 'utf-8')mainWindow.webContents.send('open-file', {content,filePath: filePaths[0],fileName: path.basename(filePaths[0])})}return null}},

src/renderer/src/App.vue

  window.electron.ipcRenderer.on('open-file', (_, { content, fileName, filePath }) => {markdownContent.value = contentcurrentFilePath.value = filePathif (!isFileExists(filePath)) {fileList.value.unshift({content,fileName,filePath})}})

打开文件夹

src/main/menu.ts

        {label: '打开文件夹',accelerator: 'CmdOrCtrl+K',click: async () => {const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {properties: ['openDirectory']})if (!canceled) {const folderPath = filePaths[0]try {const files = await fs.readdir(folderPath)const mdFiles = files.filter((file) =>['.md', '.markdown'].includes(path.extname(file)))const fileList: FileItem[] = []for (const mdFile of mdFiles) {const filePath = path.join(folderPath, mdFile)const content = await fs.readFile(filePath, 'utf-8')fileList.push({content,filePath,fileName: mdFile})}mainWindow.webContents.send('open-dir', fileList)mainWindow.webContents.send('open-file', fileList[0])} catch (error) {console.error('读取文件夹失败:', error)}}return null}},

src/renderer/src/App.vue

  window.electron.ipcRenderer.on('open-dir', (_, newFileList) => {// 使用 splice 方法更新数组fileList.value.splice(0, fileList.value.length, ...newFileList)})
  window.electron.ipcRenderer.on('open-file', (_, { content, fileName, filePath }) => {markdownContent.value = contentcurrentFilePath.value = filePathif (!isFileExists(filePath)) {fileList.value.unshift({content,fileName,filePath})}})

保存

src/main/menu.ts

        {label: '保存',accelerator: 'CmdOrCtrl+S',click: () => {mainWindow.webContents.send('save-file')}},

src/renderer/src/App.vue

  window.electron.ipcRenderer.on('save-file', () => {const content = markdownContent.valueif (currentFilePath.value) {// 存在文件路径时,保存文件const filePath = currentFilePath.value// 更新文件列表内容fileList.value.forEach((file) => {if (file.filePath === filePath) {file.content = content}})window.electron.ipcRenderer.send('save-file', { content, filePath })} else {// 无文件路径时,新建文件window.electron.ipcRenderer.send('new-file', content)}})

src/main/ipc.ts

  // 处理新建文件请求ipcMain.on('new-file', async (_e, content) => {const { canceled, filePath } = await dialog.showSaveDialog({filters: [{name: 'Markdown Files',extensions: ['md']}]})if (!canceled) {try {await fs.writeFile(filePath, content)mainWindow.webContents.send('open-file', {content: content,filePath: filePath,fileName: path.basename(filePath)})} catch (error) {console.error('创建文件时出错:', error)}}})// 处理保存文件请求ipcMain.on('save-file', async (_e, data) => {try {await fs.writeFile(data.filePath, data.content, 'utf-8')} catch (error) {console.error('保存文件失败:', error)}})

ipc.ts 的架构

src/main/index.ts

import { setupIPC } from './ipc'
setupIPC(mainWindow)

src/main/ipc.ts

import { ipcMain, BrowserWindow, shell, dialog } from 'electron'
import fs from 'fs/promises'
import path from 'path'
import { createContextMenu } from './menu'
export function setupIPC(mainWindow: BrowserWindow): void {// IPC相关代码
}

退出

src/main/menu.ts

        {label: '退出',role: 'quit'}

http://www.hkcw.cn/article/slQTurBLuQ.shtml

相关文章

天气预报中的AI:更准确的预测如何实现

如今的天气预报早已不是简单的看云识天气,而是变成了一场数据与算法的科技博弈。当你在手机App上查看未来两小时的降雨概率时,背后可能是AI模型分析了全球数万颗气象卫星的数据;当你收到台风路径预警短信时,或许是AI提前五天就锁定…

虚拟化数据恢复—XenServer虚拟机虚拟磁盘文件丢失的数据恢复案例

虚拟化环境: 某品牌720服务器中有一组通过型号为H710P的RAID卡4块STAT硬盘组建的RAID10,上层部署Xen Server服务器虚拟化平台。虚拟机安装的Windows Server系统,运行Web服务器。有系统盘 数据盘两个虚拟机磁盘。 虚拟化故障: 机…

Java 之殇:从中流砥柱到“被温柔替代”

—— 一位老派 Java 工程师的自述 今天看到一篇江苏的作者发出的《公司Rust团队全员被裁,只因把服务写得「太稳定」:“项目0故障、0报警,那养着3个Rust工程师没用啊”》帖子。看到那篇文章第一反应也是:这八成是 AI 编的。但说实…

vscode一直连接不上虚拟机或者虚拟机容器怎么办?

1. 检查并修复文件权限 右键点击 C:\Users\20325\.ssh\config 文件,选择 属性 → 安全 选项卡。 确保只有你的用户账户有完全控制权限,移除其他用户(如 Hena\Administrator)的权限。 如果 .ssh 文件夹权限也有问题,同…

面试中的项目经验考查:如何让实战经历成为你的决胜王牌

阅读原文 "你在项目中遇到的最大困难是什么?" 当面试官抛出这个问题时,你是否曾感到一阵心虚?是否担心自己的回答显得单薄无力?在竞争激烈的技术岗位面试中,项目经验往往是决定成败的关键因素。资深HR甚至建…

基于Java(SSH框架)+MySQL 实现(Web)公司通用门户(CMS)网站

一、公司通用门户网站的设计与实现 摘要:随着IT应用的深入普及,各行各业都积累了大量的信息资源,实现企业内部信息技术资源的有效整合和精益化管理,是越来越多公司企业的迫切需求。公司门户网站是一个企业向外宣传企业品牌和展示…

vue3实现鼠标悬浮div动画效果

需求 鼠标悬浮在div上显示下载按钮和信息&#xff0c;同时保持下面的div位置不变&#xff1b;当鼠标移走的时候就隐藏恢复原样。 效果&#xff1a; 代码 <script setup> const software ref([{id: "one",title: "软件",container: [{id: "123…

数据结构与算法之单链表面试题(新浪、百度、腾讯)

单链表面试题&#xff08;新浪、百度、腾讯&#xff09; 求单链表中的有效节点的个数 public int getCount(HeroNode head) {Hero1 cur head.getNext();int count 0;while(cur ! null) {count;cur cur.getNext();}return count;}查找单链表中的倒数第k个结点【新浪面试题】…

Google Play推出新功能:用户可直接向Gemini提问应用相关问题

5 月 30 日消息&#xff0c;谷歌在Google Play中广泛推出了由 Gemini AI 提供支持的“向Google Play询问此应用”功能&#xff0c;该功能已正式出现在Google Play的46.1.39-31 版本中。 “向Google Play询问此应用”这项功能&#xff0c;将 Gemini AI 直接集成到Google Play中&…

PyTorch学习(1):张量(Tensor)核心操作详解

PyTorch学习(1)&#xff1a;张量&#xff08;Tensor&#xff09;核心操作详解 一、张量&#xff08;Tensor&#xff09;核心操作详解 张量是PyTorch的基础数据结构&#xff0c;类似于NumPy的ndarray&#xff0c;但支持GPU加速和自动微分。 1. 张量创建与基础属性 import to…

农村土地承包经营权二轮延包—生成地块的KJZB字段

"关于地块的空间坐标&#xff08;KJZB&#xff09;字段&#xff0c;可能稍微复杂一点&#xff0c;用脚本生成较好。空间坐标&#xff0c;目前有两种表达&#xff1a;方案一&#xff0c;根据地块上界址点的个数依次填上&#xff08;如4个为J1/J2/J3/J4&#xff09;&#xf…

时空数据智能分析的原理和案例分享

在当今数字化时代,时空数据如同隐藏在海量信息中的宝藏,蕴含着丰富的价值,等待我们去挖掘和利用。从城市交通的实时监测与优化,到自然灾害的预警与防范,从精准农业的智能管理,到金融市场的动态分析,时空数据的身影无处不在,深刻地影响着我们生活的方方面面。DeepSeek,…

专场回顾 | 重新定义交互,智能硬件的未来设计

自2022年起&#xff0c;中国智能硬件行业呈现出蓬勃发展的态势&#xff0c;市场规模不断扩大。一个多月前&#xff0c;“小智AI”在短视频平台的爆火将智能硬件带向了大众视野&#xff0c;也意味着智能硬件已不再仅仅停留在概念和技术层面&#xff0c;而是加速迈向实际落地应用…

解决访问网站提示“405 很抱歉,由于您访问的URL有可能对网站造成安全威胁,您的访问被阻断”问题

一、问题描述 本来前几天都可以正常访问的网站&#xff0c;但是今天当我们访问网站的时候会显示“405 很抱歉&#xff0c;由于您访问的URL有可能对网站造成安全威胁&#xff0c;您的访问被阻断。您的请求ID是&#xff1a;XXXX”&#xff0c;而不能正常的访问网站&#xff0c;如…

十二、【核心功能篇】测试用例列表与搜索:高效展示和查找海量用例

【核心功能篇】测试用例列表与搜索&#xff1a;高效展示和查找海量用例 前言准备工作第一步&#xff1a;更新 API 服务以支持分页和更完善的搜索第二步&#xff1a;创建测试用例列表页面组件 (src/views/testcase/TestCaseListView.vue)第三步&#xff1a;测试列表、搜索、筛选…

Windows环境下PHP,在PowerShell控制台输出中文乱码

解决方法&#xff1a; 以管理员运行PowerShell , 输入&#xff1a; chcp 65001 重启控制台&#xff1b;然后就正常输出中文&#xff1b;

安卓apk安装包签名步骤

1.获取apk对应的原始证书&#xff08;问前端要&#xff09; 2.打开命令窗口win r 输入 cmd 3.输入 cd .android 定位到 .android 文件夹 4.执行证书签名命令 keytool -genkey -v -keystore 前端提供的.keystore -alias 自定义别名信息 -keyalg RSA -validity 10000 密钥为&a…

C与C++相互调用

C与C为什么相互调用的方式不同 C 和 C 之间的相互调用方式存在区别&#xff0c;主要是由于 C 和 C 语言本身的设计和特性不同。 函数调用和参数传递方式不同 &#xff1a; C 和 C 在函数调用和参数传递方面有一些不同之处。 C 使用标准 的函数调用约定&#xff0c;而 …

Nest全栈到失业(附加):Mysql+TypeOrm构建CRUD

前置内容 在此之前,我希望你准备好一个docker环境,以及魔法的网络哦 自己创建一个项目哈,使用nest new XXX Docker 什么是docker?相信很多人都知道了,说白了,就是一个镜像容器;以mysql为例,你在电脑上使用mysql5.6啥的,他电脑上是5.7啥的,然后数据内容不兼容了,怎么办了?他卸…

InnoDB引擎逻辑存储结构及架构

简化理解版 想象 InnoDB 是一个高效运转的仓库&#xff1a; 核心内存区 (大脑 & 高速缓存 - 干活超快的地方) 缓冲池 Buffer Pool (最最核心&#xff01;)&#xff1a; 作用&#xff1a; 相当于仓库的“高频货架”。把最常用的数据&#xff08;表数据、索引&#xff09;从…