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

最终效果

整体架构

src/main/index.ts

c 复制代码
import { createMenu } from './menu'

在 const mainWindow 后

c 复制代码
  // 加载菜单
  createMenu(mainWindow)

src/main/menu.ts

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

ts 复制代码
export interface FileItem {
  content: string
  fileName: string
  filePath: string
  editable?: boolean
}

新建文件

src/main/menu.ts

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

ts 复制代码
  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

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

ts 复制代码
  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

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

ts 复制代码
  window.electron.ipcRenderer.on('open-dir', (_, newFileList) => {
    // 使用 splice 方法更新数组
    fileList.value.splice(0, fileList.value.length, ...newFileList)
  })
ts 复制代码
  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

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

src/renderer/src/App.vue

c 复制代码
  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

c 复制代码
  // 处理新建文件请求
  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

c 复制代码
import { setupIPC } from './ipc'
c 复制代码
setupIPC(mainWindow)

src/main/ipc.ts

c 复制代码
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

c 复制代码
        {
          label: '退出',
          role: 'quit'
        }
相关推荐
hero_heart7 小时前
vscode中c_cpp_properities.cpp文件生成和作用
ide·vscode·编辑器
FrostedLotus·霜莲10 小时前
C++主流编辑器特点比较
开发语言·c++·编辑器
DO ITNOW13 小时前
Cursor/VScode ,点击运行按钮,就打开新的终端,如何设置为在当前终端运行文件而不是重新打开终端----一招搞定篇
ide·vscode·编辑器
小妖66613 小时前
若 VSCode 添加到文件夹内右键菜单中显示
ide·vscode·编辑器
freshman_y14 小时前
VSCode遇到的一些小毛病(自动保存、运行后光标不再处于编辑区)
ide·vscode·编辑器
死也不注释1 天前
【Unity 编辑器工具开发:GUILayout 与 EditorGUILayout 对比分析】
unity·编辑器·游戏引擎
邢同学爱折腾2 天前
当前端轮播图遇上Electron: 变身一款丝滑的 图片查看器
javascript·electron
vvilkim2 天前
使用Electron开发跨平台本地文件管理器:从入门到实践
前端·javascript·electron
musk12122 天前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
vvilkim2 天前
Electron 安全最佳实践:构建安全的桌面应用
javascript·安全·electron