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'
        }
相关推荐
qq. 28040339845 小时前
vim 的基本使用
linux·编辑器·vim
爱吃巧克力的程序媛5 小时前
Vim 中设置插入模式下输入中文
linux·编辑器·vim
今天又在摸鱼6 小时前
vscode实用配置
ide·vscode·编辑器
深色風信子10 小时前
Eclipse 插件开发 5.2 编辑器 获取当前编辑器
java·eclipse·编辑器·eclipse 编辑器获取·eclipse 插件
hbwhmama12 小时前
WIN11使用vscode搭建c语言开发环境
ide·vscode·编辑器
砖头拍死你12 小时前
搭建基于VsCode的ESP32的开发环境教程
ide·vscode·编辑器
深色風信子1 天前
Eclipse 插件开发 5.3 编辑器 监听输入
java·eclipse·编辑器·编辑器 监听输入·插件 监听输入
集成显卡1 天前
图片压缩工具 | Electron+Vue3+Rsbuild开发桌面应用
前端·javascript·electron·vue
dqsh062 天前
ESP32-C3 Vscode+ESP-IDF开发环境搭建 保姆级教程
ide·vscode·物联网·编辑器·智能家居·iot