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

发布于:2025-05-31 ⋅ 阅读:(27) ⋅ 点赞:(0)

最终效果

在这里插入图片描述

整体架构

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'
        }

网站公告

今日签到

点亮在社区的每一天
去签到