Skip to content

第9章:实战项目:桌面笔记应用

综合运用前面学到的所有知识,开发一个功能完整的桌面笔记应用

9.1 项目需求分析

我们将开发一个名为 "ElectronNotes" 的桌面笔记应用,具备以下功能:

核心功能

  • ✅ 创建、编辑、删除笔记
  • ✅ 笔记分类和标签管理
  • ✅ 全文搜索功能
  • ✅ Markdown 支持
  • ✅ 文件导入导出
  • ✅ 数据同步和备份

高级功能

  • ✅ 多窗口支持
  • ✅ 主题切换
  • ✅ 快捷键支持
  • ✅ 系统托盘集成
  • ✅ 自动保存
  • ✅ 版本历史

技术栈

  • 主框架: Electron
  • 前端: HTML5 + CSS3 + JavaScript (ES6+)
  • 编辑器: Monaco Editor (VS Code 编辑器)
  • 数据存储: SQLite + electron-store
  • UI 框架: 自定义 CSS + 部分 Web Components
  • 构建工具: electron-builder

9.2 项目架构设计

目录结构

electron-notes/
├── src/
│   ├── main/                    # 主进程
│   │   ├── main.js             # 主进程入口
│   │   ├── menu.js             # 菜单管理
│   │   ├── window-manager.js   # 窗口管理
│   │   ├── database.js         # 数据库管理
│   │   ├── file-manager.js     # 文件管理
│   │   └── tray.js             # 系统托盘
│   ├── renderer/               # 渲染进程
│   │   ├── index.html          # 主页面
│   │   ├── css/                # 样式文件
│   │   ├── js/                 # JavaScript 文件
│   │   └── components/         # 组件
│   ├── preload/                # 预加载脚本
│   │   └── preload.js
│   └── shared/                 # 共享代码
│       ├── constants.js
│       └── utils.js
├── assets/                     # 资源文件
│   ├── icons/
│   ├── images/
│   └── fonts/
├── build/                      # 构建资源
├── dist/                       # 构建输出
├── database/                   # 数据库文件
├── package.json
├── electron-builder.config.js
└── README.md

数据模型设计

javascript
// src/shared/models.js
class Note {
  constructor(data = {}) {
    this.id = data.id || null
    this.title = data.title || '无标题'
    this.content = data.content || ''
    this.categoryId = data.categoryId || null
    this.tags = data.tags || []
    this.createdAt = data.createdAt || new Date()
    this.updatedAt = data.updatedAt || new Date()
    this.isMarkdown = data.isMarkdown || false
    this.isFavorite = data.isFavorite || false
    this.isDeleted = data.isDeleted || false
  }

  toJSON() {
    return {
      id: this.id,
      title: this.title,
      content: this.content,
      categoryId: this.categoryId,
      tags: JSON.stringify(this.tags),
      createdAt: this.createdAt.toISOString(),
      updatedAt: this.updatedAt.toISOString(),
      isMarkdown: this.isMarkdown ? 1 : 0,
      isFavorite: this.isFavorite ? 1 : 0,
      isDeleted: this.isDeleted ? 1 : 0
    }
  }

  static fromJSON(data) {
    return new Note({
      ...data,
      tags: JSON.parse(data.tags || '[]'),
      createdAt: new Date(data.createdAt),
      updatedAt: new Date(data.updatedAt),
      isMarkdown: Boolean(data.isMarkdown),
      isFavorite: Boolean(data.isFavorite),
      isDeleted: Boolean(data.isDeleted)
    })
  }
}

class Category {
  constructor(data = {}) {
    this.id = data.id || null
    this.name = data.name || ''
    this.color = data.color || '#007acc'
    this.icon = data.icon || 'folder'
    this.parentId = data.parentId || null
    this.createdAt = data.createdAt || new Date()
    this.updatedAt = data.updatedAt || new Date()
  }

  toJSON() {
    return {
      id: this.id,
      name: this.name,
      color: this.color,
      icon: this.icon,
      parentId: this.parentId,
      createdAt: this.createdAt.toISOString(),
      updatedAt: this.updatedAt.toISOString()
    }
  }

  static fromJSON(data) {
    return new Category({
      ...data,
      createdAt: new Date(data.createdAt),
      updatedAt: new Date(data.updatedAt)
    })
  }
}

class Tag {
  constructor(data = {}) {
    this.id = data.id || null
    this.name = data.name || ''
    this.color = data.color || '#666666'
    this.createdAt = data.createdAt || new Date()
  }

  toJSON() {
    return {
      id: this.id,
      name: this.name,
      color: this.color,
      createdAt: this.createdAt.toISOString()
    }
  }

  static fromJSON(data) {
    return new Tag({
      ...data,
      createdAt: new Date(data.createdAt)
    })
  }
}

module.exports = { Note, Category, Tag }

9.3 主进程实现

主进程入口

javascript
// src/main/main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const WindowManager = require('./window-manager')
const MenuManager = require('./menu')
const DatabaseManager = require('./database')
const FileManager = require('./file-manager')
const TrayManager = require('./tray')

class ElectronNotesApp {
  constructor() {
    this.windowManager = null
    this.menuManager = null
    this.databaseManager = null
    this.fileManager = null
    this.trayManager = null
    
    this.setupApp()
  }

  setupApp() {
    // 设置应用 ID
    app.setAppUserModelId('com.electronotes.app')
    
    // 单实例应用
    const gotTheLock = app.requestSingleInstanceLock()
    
    if (!gotTheLock) {
      app.quit()
      return
    }

    app.on('second-instance', () => {
      // 当运行第二个实例时,聚焦到主窗口
      if (this.windowManager) {
        this.windowManager.focusMainWindow()
      }
    })

    // 应用事件
    app.whenReady().then(() => {
      this.initializeApp()
    })

    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit()
      }
    })

    app.on('activate', () => {
      if (this.windowManager && this.windowManager.getWindowCount() === 0) {
        this.windowManager.createMainWindow()
      }
    })

    app.on('before-quit', () => {
      this.cleanup()
    })
  }

  async initializeApp() {
    try {
      // 初始化数据库
      this.databaseManager = new DatabaseManager()
      await this.databaseManager.initialize()

      // 初始化文件管理器
      this.fileManager = new FileManager(this.databaseManager)

      // 初始化窗口管理器
      this.windowManager = new WindowManager()
      
      // 初始化菜单
      this.menuManager = new MenuManager(this.windowManager, this.fileManager)
      
      // 初始化系统托盘
      this.trayManager = new TrayManager(this.windowManager)

      // 设置 IPC 处理
      this.setupIPC()

      // 创建主窗口
      this.windowManager.createMainWindow()

      console.log('ElectronNotes 应用初始化完成')
    } catch (error) {
      console.error('应用初始化失败:', error)
      app.quit()
    }
  }

  setupIPC() {
    // 笔记相关 IPC
    ipcMain.handle('notes:getAll', () => {
      return this.databaseManager.getAllNotes()
    })

    ipcMain.handle('notes:getById', (event, id) => {
      return this.databaseManager.getNoteById(id)
    })

    ipcMain.handle('notes:create', (event, noteData) => {
      return this.databaseManager.createNote(noteData)
    })

    ipcMain.handle('notes:update', (event, id, noteData) => {
      return this.databaseManager.updateNote(id, noteData)
    })

    ipcMain.handle('notes:delete', (event, id) => {
      return this.databaseManager.deleteNote(id)
    })

    ipcMain.handle('notes:search', (event, query) => {
      return this.databaseManager.searchNotes(query)
    })

    // 分类相关 IPC
    ipcMain.handle('categories:getAll', () => {
      return this.databaseManager.getAllCategories()
    })

    ipcMain.handle('categories:create', (event, categoryData) => {
      return this.databaseManager.createCategory(categoryData)
    })

    ipcMain.handle('categories:update', (event, id, categoryData) => {
      return this.databaseManager.updateCategory(id, categoryData)
    })

    ipcMain.handle('categories:delete', (event, id) => {
      return this.databaseManager.deleteCategory(id)
    })

    // 标签相关 IPC
    ipcMain.handle('tags:getAll', () => {
      return this.databaseManager.getAllTags()
    })

    ipcMain.handle('tags:create', (event, tagData) => {
      return this.databaseManager.createTag(tagData)
    })

    // 文件相关 IPC
    ipcMain.handle('file:import', (event, filePath) => {
      return this.fileManager.importFile(filePath)
    })

    ipcMain.handle('file:export', (event, noteId, format) => {
      return this.fileManager.exportNote(noteId, format)
    })

    ipcMain.handle('file:showOpenDialog', (event, options) => {
      return this.fileManager.showOpenDialog(options)
    })

    ipcMain.handle('file:showSaveDialog', (event, options) => {
      return this.fileManager.showSaveDialog(options)
    })

    // 窗口相关 IPC
    ipcMain.handle('window:createNote', () => {
      return this.windowManager.createNoteWindow()
    })

    ipcMain.handle('window:minimize', (event) => {
      const window = BrowserWindow.fromWebContents(event.sender)
      if (window) window.minimize()
    })

    ipcMain.handle('window:maximize', (event) => {
      const window = BrowserWindow.fromWebContents(event.sender)
      if (window) {
        if (window.isMaximized()) {
          window.unmaximize()
        } else {
          window.maximize()
        }
      }
    })

    ipcMain.handle('window:close', (event) => {
      const window = BrowserWindow.fromWebContents(event.sender)
      if (window) window.close()
    })

    // 应用相关 IPC
    ipcMain.handle('app:getVersion', () => {
      return app.getVersion()
    })

    ipcMain.handle('app:quit', () => {
      app.quit()
    })
  }

  cleanup() {
    if (this.databaseManager) {
      this.databaseManager.close()
    }
    
    if (this.trayManager) {
      this.trayManager.destroy()
    }
  }
}

// 创建应用实例
new ElectronNotesApp()

窗口管理器

javascript
// src/main/window-manager.js
const { BrowserWindow, screen } = require('electron')
const path = require('path')

class WindowManager {
  constructor() {
    this.windows = new Map()
    this.mainWindow = null
  }

  createMainWindow() {
    if (this.mainWindow && !this.mainWindow.isDestroyed()) {
      this.mainWindow.focus()
      return this.mainWindow
    }

    const { width, height } = screen.getPrimaryDisplay().workAreaSize
    
    this.mainWindow = new BrowserWindow({
      width: Math.min(1200, width - 100),
      height: Math.min(800, height - 100),
      minWidth: 800,
      minHeight: 600,
      show: false,
      titleBarStyle: 'hidden',
      titleBarOverlay: {
        color: '#2f3241',
        symbolColor: '#ffffff'
      },
      webPreferences: {
        nodeIntegration: false,
        contextIsolation: true,
        preload: path.join(__dirname, '../preload/preload.js'),
        webSecurity: true,
        allowRunningInsecureContent: false
      }
    })

    this.mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))

    // 窗口事件
    this.mainWindow.once('ready-to-show', () => {
      this.mainWindow.show()
    })

    this.mainWindow.on('closed', () => {
      this.mainWindow = null
      this.windows.delete('main')
    })

    this.mainWindow.on('focus', () => {
      this.mainWindow.lastActivity = Date.now()
    })

    this.windows.set('main', this.mainWindow)
    return this.mainWindow
  }

  createNoteWindow(noteId = null) {
    const noteWindow = new BrowserWindow({
      width: 800,
      height: 600,
      minWidth: 600,
      minHeight: 400,
      show: false,
      parent: this.mainWindow,
      webPreferences: {
        nodeIntegration: false,
        contextIsolation: true,
        preload: path.join(__dirname, '../preload/preload.js')
      }
    })

    const windowId = `note-${Date.now()}`
    
    // 加载笔记编辑页面
    const noteUrl = noteId ? 
      `file://${path.join(__dirname, '../renderer/note.html')}?id=${noteId}` :
      `file://${path.join(__dirname, '../renderer/note.html')}`
    
    noteWindow.loadURL(noteUrl)

    noteWindow.once('ready-to-show', () => {
      noteWindow.show()
    })

    noteWindow.on('closed', () => {
      this.windows.delete(windowId)
    })

    this.windows.set(windowId, noteWindow)
    return noteWindow
  }

  focusMainWindow() {
    if (this.mainWindow && !this.mainWindow.isDestroyed()) {
      if (this.mainWindow.isMinimized()) {
        this.mainWindow.restore()
      }
      this.mainWindow.focus()
    } else {
      this.createMainWindow()
    }
  }

  getWindowCount() {
    return this.windows.size
  }

  closeAllWindows() {
    this.windows.forEach(window => {
      if (!window.isDestroyed()) {
        window.close()
      }
    })
    this.windows.clear()
  }
}

module.exports = WindowManager

数据库管理器

javascript
// src/main/database.js
const sqlite3 = require('sqlite3').verbose()
const path = require('path')
const { app } = require('electron')
const { Note, Category, Tag } = require('../shared/models')

class DatabaseManager {
  constructor() {
    this.db = null
    this.dbPath = path.join(app.getPath('userData'), 'notes.db')
  }

  async initialize() {
    return new Promise((resolve, reject) => {
      this.db = new sqlite3.Database(this.dbPath, (err) => {
        if (err) {
          reject(err)
          return
        }

        console.log('数据库连接成功:', this.dbPath)
        this.createTables().then(resolve).catch(reject)
      })
    })
  }

  async createTables() {
    const tables = [
      // 分类表
      `CREATE TABLE IF NOT EXISTS categories (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        color TEXT DEFAULT '#007acc',
        icon TEXT DEFAULT 'folder',
        parentId INTEGER,
        createdAt TEXT NOT NULL,
        updatedAt TEXT NOT NULL,
        FOREIGN KEY (parentId) REFERENCES categories (id)
      )`,

      // 标签表
      `CREATE TABLE IF NOT EXISTS tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL UNIQUE,
        color TEXT DEFAULT '#666666',
        createdAt TEXT NOT NULL
      )`,

      // 笔记表
      `CREATE TABLE IF NOT EXISTS notes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        content TEXT,
        categoryId INTEGER,
        tags TEXT DEFAULT '[]',
        createdAt TEXT NOT NULL,
        updatedAt TEXT NOT NULL,
        isMarkdown INTEGER DEFAULT 0,
        isFavorite INTEGER DEFAULT 0,
        isDeleted INTEGER DEFAULT 0,
        FOREIGN KEY (categoryId) REFERENCES categories (id)
      )`,

      // 笔记版本历史表
      `CREATE TABLE IF NOT EXISTS note_versions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        noteId INTEGER NOT NULL,
        title TEXT NOT NULL,
        content TEXT,
        version INTEGER NOT NULL,
        createdAt TEXT NOT NULL,
        FOREIGN KEY (noteId) REFERENCES notes (id)
      )`
    ]

    for (const sql of tables) {
      await this.run(sql)
    }

    // 创建索引
    await this.createIndexes()

    // 插入默认数据
    await this.insertDefaultData()
  }

  async createIndexes() {
    const indexes = [
      'CREATE INDEX IF NOT EXISTS idx_notes_title ON notes(title)',
      'CREATE INDEX IF NOT EXISTS idx_notes_category ON notes(categoryId)',
      'CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(createdAt)',
      'CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updatedAt)',
      'CREATE INDEX IF NOT EXISTS idx_notes_deleted ON notes(isDeleted)',
      'CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parentId)',
      'CREATE INDEX IF NOT EXISTS idx_note_versions_note ON note_versions(noteId)'
    ]

    for (const sql of indexes) {
      await this.run(sql)
    }
  }

  async insertDefaultData() {
    // 检查是否已有数据
    const categoryCount = await this.get('SELECT COUNT(*) as count FROM categories')

    if (categoryCount.count === 0) {
      // 插入默认分类
      const defaultCategories = [
        { name: '工作', color: '#007acc', icon: 'briefcase' },
        { name: '个人', color: '#28a745', icon: 'user' },
        { name: '学习', color: '#ffc107', icon: 'book' },
        { name: '想法', color: '#dc3545', icon: 'lightbulb' }
      ]

      for (const category of defaultCategories) {
        await this.createCategory(category)
      }

      // 插入欢迎笔记
      const welcomeNote = {
        title: '欢迎使用 ElectronNotes',
        content: `# 欢迎使用 ElectronNotes

这是一个功能强大的桌面笔记应用,支持以下特性:

## 主要功能
- ✅ Markdown 编辑支持
- ✅ 分类和标签管理
- ✅ 全文搜索
- ✅ 文件导入导出
- ✅ 多窗口编辑

## 快捷键
- \`Ctrl+N\`: 新建笔记
- \`Ctrl+S\`: 保存笔记
- \`Ctrl+F\`: 搜索笔记
- \`Ctrl+,\`: 打开设置

开始创建你的第一个笔记吧!`,
        categoryId: 1,
        isMarkdown: true
      }

      await this.createNote(welcomeNote)
    }
  }

  // 基础数据库操作方法
  run(sql, params = []) {
    return new Promise((resolve, reject) => {
      this.db.run(sql, params, function(err) {
        if (err) {
          reject(err)
        } else {
          resolve({ id: this.lastID, changes: this.changes })
        }
      })
    })
  }

  get(sql, params = []) {
    return new Promise((resolve, reject) => {
      this.db.get(sql, params, (err, row) => {
        if (err) {
          reject(err)
        } else {
          resolve(row)
        }
      })
    })
  }

  all(sql, params = []) {
    return new Promise((resolve, reject) => {
      this.db.all(sql, params, (err, rows) => {
        if (err) {
          reject(err)
        } else {
          resolve(rows)
        }
      })
    })
  }

  // 笔记相关方法
  async getAllNotes(includeDeleted = false) {
    const sql = includeDeleted ?
      'SELECT * FROM notes ORDER BY updatedAt DESC' :
      'SELECT * FROM notes WHERE isDeleted = 0 ORDER BY updatedAt DESC'

    const rows = await this.all(sql)
    return rows.map(row => Note.fromJSON(row))
  }

  async getNoteById(id) {
    const row = await this.get('SELECT * FROM notes WHERE id = ?', [id])
    return row ? Note.fromJSON(row) : null
  }

  async createNote(noteData) {
    const note = new Note(noteData)
    note.updatedAt = new Date()

    const result = await this.run(
      `INSERT INTO notes (title, content, categoryId, tags, createdAt, updatedAt, isMarkdown, isFavorite, isDeleted)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [
        note.title,
        note.content,
        note.categoryId,
        JSON.stringify(note.tags),
        note.createdAt.toISOString(),
        note.updatedAt.toISOString(),
        note.isMarkdown ? 1 : 0,
        note.isFavorite ? 1 : 0,
        note.isDeleted ? 1 : 0
      ]
    )

    note.id = result.id

    // 创建版本历史
    await this.createNoteVersion(note.id, note.title, note.content, 1)

    return note
  }

  async updateNote(id, noteData) {
    const existingNote = await this.getNoteById(id)
    if (!existingNote) {
      throw new Error('笔记不存在')
    }

    const updatedNote = new Note({ ...existingNote, ...noteData })
    updatedNote.id = id
    updatedNote.updatedAt = new Date()

    await this.run(
      `UPDATE notes SET title = ?, content = ?, categoryId = ?, tags = ?,
       updatedAt = ?, isMarkdown = ?, isFavorite = ? WHERE id = ?`,
      [
        updatedNote.title,
        updatedNote.content,
        updatedNote.categoryId,
        JSON.stringify(updatedNote.tags),
        updatedNote.updatedAt.toISOString(),
        updatedNote.isMarkdown ? 1 : 0,
        updatedNote.isFavorite ? 1 : 0,
        id
      ]
    )

    // 创建新版本历史
    const versions = await this.getNoteVersions(id)
    const nextVersion = versions.length + 1
    await this.createNoteVersion(id, updatedNote.title, updatedNote.content, nextVersion)

    return updatedNote
  }

  async deleteNote(id) {
    // 软删除
    await this.run('UPDATE notes SET isDeleted = 1, updatedAt = ? WHERE id = ?',
      [new Date().toISOString(), id])
    return true
  }

  async searchNotes(query) {
    const sql = `
      SELECT * FROM notes
      WHERE isDeleted = 0 AND (title LIKE ? OR content LIKE ?)
      ORDER BY updatedAt DESC
    `
    const searchTerm = `%${query}%`
    const rows = await this.all(sql, [searchTerm, searchTerm])
    return rows.map(row => Note.fromJSON(row))
  }

  // 分类相关方法
  async getAllCategories() {
    const rows = await this.all('SELECT * FROM categories ORDER BY name')
    return rows.map(row => Category.fromJSON(row))
  }

  async createCategory(categoryData) {
    const category = new Category(categoryData)

    const result = await this.run(
      'INSERT INTO categories (name, color, icon, parentId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)',
      [
        category.name,
        category.color,
        category.icon,
        category.parentId,
        category.createdAt.toISOString(),
        category.updatedAt.toISOString()
      ]
    )

    category.id = result.id
    return category
  }

  async updateCategory(id, categoryData) {
    const category = new Category(categoryData)
    category.updatedAt = new Date()

    await this.run(
      'UPDATE categories SET name = ?, color = ?, icon = ?, parentId = ?, updatedAt = ? WHERE id = ?',
      [category.name, category.color, category.icon, category.parentId, category.updatedAt.toISOString(), id]
    )

    return { ...category, id }
  }

  async deleteCategory(id) {
    // 检查是否有笔记使用此分类
    const noteCount = await this.get('SELECT COUNT(*) as count FROM notes WHERE categoryId = ? AND isDeleted = 0', [id])

    if (noteCount.count > 0) {
      throw new Error('无法删除包含笔记的分类')
    }

    await this.run('DELETE FROM categories WHERE id = ?', [id])
    return true
  }

  // 标签相关方法
  async getAllTags() {
    const rows = await this.all('SELECT * FROM tags ORDER BY name')
    return rows.map(row => Tag.fromJSON(row))
  }

  async createTag(tagData) {
    const tag = new Tag(tagData)

    const result = await this.run(
      'INSERT INTO tags (name, color, createdAt) VALUES (?, ?, ?)',
      [tag.name, tag.color, tag.createdAt.toISOString()]
    )

    tag.id = result.id
    return tag
  }

  // 版本历史相关方法
  async createNoteVersion(noteId, title, content, version) {
    await this.run(
      'INSERT INTO note_versions (noteId, title, content, version, createdAt) VALUES (?, ?, ?, ?, ?)',
      [noteId, title, content, version, new Date().toISOString()]
    )
  }

  async getNoteVersions(noteId) {
    return await this.all(
      'SELECT * FROM note_versions WHERE noteId = ? ORDER BY version DESC',
      [noteId]
    )
  }

  // 统计信息
  async getStatistics() {
    const totalNotes = await this.get('SELECT COUNT(*) as count FROM notes WHERE isDeleted = 0')
    const totalCategories = await this.get('SELECT COUNT(*) as count FROM categories')
    const totalTags = await this.get('SELECT COUNT(*) as count FROM tags')
    const favoriteNotes = await this.get('SELECT COUNT(*) as count FROM notes WHERE isFavorite = 1 AND isDeleted = 0')

    return {
      totalNotes: totalNotes.count,
      totalCategories: totalCategories.count,
      totalTags: totalTags.count,
      favoriteNotes: favoriteNotes.count
    }
  }

  close() {
    if (this.db) {
      this.db.close((err) => {
        if (err) {
          console.error('关闭数据库时出错:', err)
        } else {
          console.log('数据库连接已关闭')
        }
      })
    }
  }
}

module.exports = DatabaseManager

9.4 渲染进程实现

主页面 HTML

html
<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ElectronNotes</title>
    <link rel="stylesheet" href="css/main.css">
    <link rel="stylesheet" href="css/components.css">
</head>
<body>
    <div id="app">
        <!-- 标题栏 -->
        <div class="titlebar">
            <div class="titlebar-drag-region">
                <div class="titlebar-title">ElectronNotes</div>
            </div>
            <div class="titlebar-controls">
                <button class="titlebar-button" id="minimize-btn">
                    <svg width="12" height="12" viewBox="0 0 12 12">
                        <rect x="0" y="5" width="12" height="2"/>
                    </svg>
                </button>
                <button class="titlebar-button" id="maximize-btn">
                    <svg width="12" height="12" viewBox="0 0 12 12">
                        <rect x="0" y="0" width="12" height="12" fill="none" stroke="currentColor"/>
                    </svg>
                </button>
                <button class="titlebar-button close" id="close-btn">
                    <svg width="12" height="12" viewBox="0 0 12 12">
                        <path d="M0,0 L12,12 M12,0 L0,12" stroke="currentColor" stroke-width="2"/>
                    </svg>
                </button>
            </div>
        </div>

        <!-- 主要内容区域 -->
        <div class="main-container">
            <!-- 侧边栏 -->
            <div class="sidebar">
                <!-- 搜索栏 -->
                <div class="search-container">
                    <input type="text" id="search-input" placeholder="搜索笔记..." class="search-input">
                    <button id="search-btn" class="search-btn">
                        <svg width="16" height="16" viewBox="0 0 16 16">
                            <circle cx="6" cy="6" r="5" fill="none" stroke="currentColor"/>
                            <path d="m11 11 5 5" stroke="currentColor"/>
                        </svg>
                    </button>
                </div>

                <!-- 快速操作 -->
                <div class="quick-actions">
                    <button id="new-note-btn" class="btn btn-primary">
                        <svg width="16" height="16" viewBox="0 0 16 16">
                            <path d="M8 0v16M0 8h16" stroke="currentColor" stroke-width="2"/>
                        </svg>
                        新建笔记
                    </button>
                </div>

                <!-- 分类列表 -->
                <div class="categories-section">
                    <h3 class="section-title">分类</h3>
                    <div id="categories-list" class="categories-list">
                        <!-- 分类项将通过 JavaScript 动态生成 -->
                    </div>
                    <button id="add-category-btn" class="add-btn">+ 添加分类</button>
                </div>

                <!-- 标签列表 -->
                <div class="tags-section">
                    <h3 class="section-title">标签</h3>
                    <div id="tags-list" class="tags-list">
                        <!-- 标签项将通过 JavaScript 动态生成 -->
                    </div>
                </div>
            </div>

            <!-- 笔记列表 -->
            <div class="notes-panel">
                <div class="notes-header">
                    <h2 id="notes-title">所有笔记</h2>
                    <div class="notes-actions">
                        <button id="sort-btn" class="icon-btn" title="排序">
                            <svg width="16" height="16" viewBox="0 0 16 16">
                                <path d="M3 3h10M3 8h7M3 13h4" stroke="currentColor"/>
                            </svg>
                        </button>
                        <button id="view-mode-btn" class="icon-btn" title="视图模式">
                            <svg width="16" height="16" viewBox="0 0 16 16">
                                <rect x="1" y="1" width="6" height="6"/>
                                <rect x="9" y="1" width="6" height="6"/>
                                <rect x="1" y="9" width="6" height="6"/>
                                <rect x="9" y="9" width="6" height="6"/>
                            </svg>
                        </button>
                    </div>
                </div>
                <div id="notes-list" class="notes-list">
                    <!-- 笔记项将通过 JavaScript 动态生成 -->
                </div>
            </div>

            <!-- 笔记编辑器 -->
            <div class="editor-panel">
                <div class="editor-header">
                    <input type="text" id="note-title" placeholder="笔记标题..." class="note-title-input">
                    <div class="editor-actions">
                        <button id="markdown-toggle" class="icon-btn" title="Markdown 模式">
                            <svg width="16" height="16" viewBox="0 0 16 16">
                                <path d="M2 2h12v12H2z" fill="none" stroke="currentColor"/>
                                <path d="M5 5v6M8 5v6M11 5v6" stroke="currentColor"/>
                            </svg>
                        </button>
                        <button id="favorite-btn" class="icon-btn" title="收藏">
                            <svg width="16" height="16" viewBox="0 0 16 16">
                                <path d="M8 1l2 6h6l-5 4 2 6-5-4-5 4 2-6-5-4h6z" fill="none" stroke="currentColor"/>
                            </svg>
                        </button>
                        <button id="save-btn" class="btn btn-primary">保存</button>
                    </div>
                </div>
                <div class="editor-container">
                    <div id="editor" class="editor"></div>
                    <div id="preview" class="preview hidden"></div>
                </div>
            </div>
        </div>

        <!-- 状态栏 -->
        <div class="statusbar">
            <div class="statusbar-left">
                <span id="notes-count">0 个笔记</span>
            </div>
            <div class="statusbar-right">
                <span id="current-time"></span>
            </div>
        </div>
    </div>

    <!-- 模态对话框 -->
    <div id="modal-overlay" class="modal-overlay hidden">
        <div class="modal">
            <div class="modal-header">
                <h3 id="modal-title">标题</h3>
                <button id="modal-close" class="modal-close">×</button>
            </div>
            <div class="modal-body" id="modal-body">
                <!-- 模态内容 -->
            </div>
            <div class="modal-footer">
                <button id="modal-cancel" class="btn btn-secondary">取消</button>
                <button id="modal-confirm" class="btn btn-primary">确定</button>
            </div>
        </div>
    </div>

    <script src="js/app.js"></script>
</body>
</html>

本章小结

通过本章学习,你应该已经:

  • ✅ 了解了完整项目的需求分析方法
  • ✅ 掌握了项目架构设计的思路
  • ✅ 学会了数据模型的设计
  • ✅ 实现了主进程的核心功能
  • ✅ 掌握了窗口管理的最佳实践
  • ✅ 实现了完整的数据库管理系统
  • ✅ 创建了主页面的 HTML 结构

在接下来的内容中,我们将继续实现 CSS 样式、JavaScript 逻辑和完整的用户交互功能!

Released under the MIT License.