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

news2025/6/2 21:20:31

最终效果

在这里插入图片描述

整体架构

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: string
  fileName: string
  filePath: string
  editable?: 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 = content
    currentFilePath.value = filePath
    if (!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 = content
    currentFilePath.value = filePath
    if (!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 = content
    currentFilePath.value = filePath
    if (!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.value
    if (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.coloradmin.cn/o/2394095.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker系列】Docker 容器内安装`ps`命令

博客目录 一、为什么需要在 Docker 容器中安装ps命令二、不同 Linux 发行版的安装方法1. Alpine Linux 镜像的安装方法2. Debian/Ubuntu 镜像的安装方法3. CentOS/RHEL 镜像的安装方法 三、验证安装与基本使用四、永久解决方案:修改 Dockerfile1. Alpine 基础镜像的…

华为OD机试真题——生成哈夫曼树(2025A卷:100分)Java/python/JavaScript/C/C++/GO六种最佳实现

2025 A卷 100分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析; 并提供Java、python、JavaScript、C++、C语言、GO六种语言的最佳实现方式! 本文收录于专栏:《2025华为OD真题目录+全流程解析/备考攻略/经验分享》 华为OD机试真题《生成…

大厂前端研发岗位设计的30道Webpack面试题及解析

文章目录 一、基础核心二、配置进阶三、性能优化四、Loader原理五、Plugin机制六、高级应用七、工程化实战八、原理深挖九、异常处理十、综合场景一、基础核心 Webpack的核心概念是什么? 解析:入口(entry)、输出(output)、加载器(loader)、插件(plugins)、模式(mode)。Loader…

Oracle中EXISTS NOT EXISTS的使用

目录 1.IN与EXISTS EXISTS用法总结 2.NOT IN与NOT EXISTS 3.not in 中 null的用法 4.EXISTS和IN的区别 (面试常问) 1.IN与EXISTS 示例:在 DEPT 表中找出在 EMP 表中存在的部门编号; 方法一:使用in select DEPTNO from DEPT where D…

01.认识Kubernetes

什么是Kubernets 套用官方文档对Kubernetes的定义,翻译成中文的意思是: Kubernetes,也称为k8,是一个用于自动化部署、扩展和管理容器化应用程序的开源系统。 它将组成应用程序的容器分组为逻辑单元,以便于管理和发现…

【PostgreSQL 02】PostgreSQL数据类型革命:JSON、数组与地理信息让你的应用飞起来

PostgreSQL数据类型革命:JSON、数组与地理信息让你的应用飞起来 关键词 PostgreSQL高级数据类型, JSONB, 数组类型, PostGIS, 地理信息系统, NoSQL, 文档数据库, 空间数据, 数据库设计, PostgreSQL扩展 摘要 PostgreSQL的高级数据类型是其区别于传统关系数据库的核心…

Acrobat DC v25.001 最新专业版已破,像word一样编辑PDF!

在数字化时代,PDF文件以其稳定性和通用性成为了文档交流和存储的热门选择。无论是阅读、编辑、转换还是转曲,大家对PDF文件的操作需求日益增加。因此,一款出色的PDF处理软件不仅要满足多样化的需求,还要通过简洁的界面和强大的功能…

桥 接 模 式

在玩游戏的时候我们常常会遇到这样的机制:我们可以随意选择不同的角色,搭配不同的武器。这时只有一个抽象上下文的策略模式就不那么适用了,因为一旦我们使用继承的方式,武器和角色总有一方会变得难以扩展。这时,我们就…

基于 Flink+Paimon+Hologres 搭建淘天集团湖仓一体数据链路

摘要:本文整理自淘天集团高级数据开发工程师朱奥老师在 Flink Forward Asia 2024 流式湖仓论坛的分享。内容主要为以下五部分: 1、项目背景 2、核心策略 3、解决方案 4、项目价值 5、未来计划 01、项目背景 1.1 当前实时数仓架构 当前的淘天实时架构是从…

多杆合一驱动城市空间治理智慧化

引言:城市“杆林困境”与智慧化破局 走在现代城市的街道上,路灯、监控、交通信号灯、5G基站等杆体林立,不仅侵占公共空间,更暴露了城市治理的碎片化问题。如何让这些“沉默的钢铁”升级为城市的“智慧神经元”?答案在…

用QT写一个车速表

主要包含以下绘制步骤: 1、绘制画布: /** 绘制画布 */ void Widget::initCanvas(QPainter &painter) {//消除锯齿painter.setRenderHint(QPainter::Antialiasing,true);//设置底色painter.setBrush(QColor(0,0,0));painter.drawRect(rect());//平移…

数控技术应用理实一体化平台VR实训系统

::产品概述:: 目前我国本科类院校学生普遍存在的问题就是缺少对实际工作的了解,一直在学习相关专业的理论知识,对社会的相关企业的用人情况不了解。这也就直接导致了毕业的学生和社会上的用人单位需求有点脱节,这也是由于我国的现行本科教育侧…

C# 将HTML文档、HTML字符串转换为图片

在.NET开发中,将HTML内容转换为图片的需求广泛存在于报告生成、邮件内容存档、网页快照等场景。Free Spire.Doc for .NET作为一款免费的专业文档处理库,无需Microsoft Word依赖,即可轻松实现这一功能。本文将深入解析HTML文档和字符串转图片两…

界面控件DevExpress WinForms v24.2新版亮点:富文本编辑器功能全新升级

DevExpress WinForms拥有180组件和UI库,能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序,无论是Office风格的界面,还是分析处理大批量的业务数据,它都能轻松胜…

华为云Flexus+DeepSeek征文|华为云 Flexus X 加速 Dify 平台落地:高性能、低成本、强可靠性的云上选择

目录 前言 1 一键部署 Dify 平台的完整步骤 1.1 选择模板 1.2 参数配置 1.3 资源栈设置 1.4 配置确认与部署 2 Flexus X 服务器的技术优势 2.1 柔性算力随心配 2.2 一直加速一直快 2.3 越用越省降本多 2.4 安全可靠更放心 3 Flexus X 在 Dify 解决方案中的性能体验…

Jenkins 2.479.1安装和邮箱配置教程

1.安装 在JDK安装并设置环境变量完成后,下载官网对应的war版本,在对应目录下打开命令行窗口并输入 java -jar jenkins.war其余参数感兴趣可以自行查阅,这里启动的 jenkins 服务默认占用8080端口,在浏览器输入 localhost:8080进入…

DFS入门刷题c++

目录 821. 跳台阶 - AcWing题库 ​92. 递归实现指数型枚举 - AcWing题库 ​P1706 全排列问题 - 洛谷 (luogu.com.cn) P1157 组合的输出 - 洛谷 (luogu.com.cn) ​P1036 [NOIP 2002 普及组] 选数 - 洛谷 (luogu.com.cn) P2089 烤鸡 - 洛谷 (luogu.com.cn) P1088 [NOIP 2…

ToolsSet之:十六进制及二进制编辑运算工具

ToolsSet是微软商店中的一款包含数十种实用工具数百种细分功能的工具集合应用,应用基本功能介绍可以查看以下文章: Windows应用ToolsSet介绍https://blog.csdn.net/BinField/article/details/145898264 ToolsSet中Number菜单下的Hex Operate工具可以进…

【Python训练营打卡】day40 @浙大疏锦行

DAY 40 训练和测试的规范写法 知识点回顾: 1. 彩色和灰度图片测试和训练的规范写法:封装在函数中 2. 展平操作:除第一个维度batchsize外全部展平 3. dropout操作:训练阶段随机丢弃神经元,测试阶段eval模式关闭dropo…

MCP Server的五种主流架构:从原理到实践的深度解析

🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 在AI大模型与外部数据交互的浪潮中,MCP Server(Model Context Protocol Server)已成为连接模型与现实世界的桥梁。本文…