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