最終效果
整體架構
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'}