最终效果
整体架构
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'}