electron系列8之Electron + Vue 3:构建现代化桌面应用(下)

一、回顾与概览

在上一篇中,我们完成了 Electron + Vue 3 的基础架构搭建。本篇将深入实战,实现完整的桌面应用功能。

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                        本篇内容全景图                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        状态管理 (Pinia)                              │    │
│  │  • 用户状态管理  • 应用配置  • 跨窗口同步  • 持久化存储              │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        数据持久化 (SQLite)                           │    │
│  │  • 数据库初始化  • ORM映射  • CRUD操作  • 迁移管理                   │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        原生功能集成                                   │    │
│  │  • 系统托盘  • 全局快捷键  • 拖拽上传  • 剪贴板                       │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        打包与发布                                     │    │
│  │  • 多平台打包  • 代码签名  • 自动更新  • CI/CD                        │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

二、Pinia 状态管理深度实践

2.1 完整的 Store 实现

src/renderer/stores/user.ts

复制代码
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface User {
  id: string
  name: string
  email: string
  avatar?: string
  role: 'admin' | 'user' | 'guest'
  preferences: UserPreferences
}

export interface UserPreferences {
  theme: 'light' | 'dark' | 'system'
  language: string
  autoSave: boolean
  notifications: boolean
  fontSize: number
}

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  const isLoggedIn = ref(false)
  const loading = ref(false)
  
  // Getters
  const userName = computed(() => user.value?.name || '游客')
  const userAvatar = computed(() => user.value?.avatar || '/default-avatar.png')
  const isAdmin = computed(() => user.value?.role === 'admin')
  
  // 从本地存储加载用户信息
  function loadFromStorage() {
    const savedUser = localStorage.getItem('user')
    const savedToken = localStorage.getItem('token')
    
    if (savedUser && savedToken) {
      user.value = JSON.parse(savedUser)
      token.value = savedToken
      isLoggedIn.value = true
    }
  }
  
  // 登录
  async function login(email: string, password: string): Promise<boolean> {
    loading.value = true
    
    try {
      // 调用主进程 API 进行验证
      const result = await window.electronAPI.auth.login({ email, password })
      
      if (result.success) {
        user.value = result.user
        token.value = result.token
        isLoggedIn.value = true
        
        // 保存到本地存储
        localStorage.setItem('user', JSON.stringify(result.user))
        localStorage.setItem('token', result.token)
        
        // 保存到加密存储
        await window.electronAPI.storage.set('userToken', result.token)
        
        return true
      }
      
      return false
    } finally {
      loading.value = false
    }
  }
  
  // 登出
  async function logout() {
    loading.value = true
    
    try {
      await window.electronAPI.auth.logout()
      
      user.value = null
      token.value = null
      isLoggedIn.value = false
      
      localStorage.removeItem('user')
      localStorage.removeItem('token')
      await window.electronAPI.storage.delete('userToken')
    } finally {
      loading.value = false
    }
  }
  
  // 更新用户信息
  async function updateUser(data: Partial<User>) {
    if (!user.value) return
    
    loading.value = true
    
    try {
      const result = await window.electronAPI.auth.updateUser(data)
      
      if (result.success) {
        user.value = { ...user.value, ...data }
        localStorage.setItem('user', JSON.stringify(user.value))
      }
    } finally {
      loading.value = false
    }
  }
  
  // 更新偏好设置
  async function updatePreferences(preferences: Partial<UserPreferences>) {
    if (!user.value) return
    
    user.value.preferences = { ...user.value.preferences, ...preferences }
    localStorage.setItem('user', JSON.stringify(user.value))
    
    // 应用偏好设置
    applyPreferences(user.value.preferences)
  }
  
  // 应用偏好设置
  function applyPreferences(preferences: UserPreferences) {
    // 应用主题
    if (preferences.theme === 'dark') {
      document.documentElement.setAttribute('data-theme', 'dark')
    } else if (preferences.theme === 'light') {
      document.documentElement.setAttribute('data-theme', 'light')
    } else {
      // 跟随系统
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
      document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light')
    }
    
    // 应用字体大小
    document.documentElement.style.fontSize = `${preferences.fontSize}px`
  }
  
  // 初始化
  loadFromStorage()
  
  return {
    // State
    user,
    token,
    isLoggedIn,
    loading,
    // Getters
    userName,
    userAvatar,
    isAdmin,
    // Actions
    login,
    logout,
    updateUser,
    updatePreferences,
    loadFromStorage
  }
})

2.2 跨窗口状态同步

src/renderer/stores/sync.ts

复制代码
import { defineStore } from 'pinia'
import { ref } from 'vue'

// 跨窗口同步服务
export const useSyncStore = defineStore('sync', () => {
  const syncEnabled = ref(true)
  const pendingSyncs = ref<Map<string, any>>(new Map())
  
  // 初始化跨窗口通信
  function initCrossWindowSync() {
    if (!syncEnabled.value) return
    
    // 监听来自其他窗口的消息
    window.addEventListener('storage', (event) => {
      if (event.key?.startsWith('sync:')) {
        const data = JSON.parse(event.newValue || '{}')
        handleSyncMessage(data)
      }
    })
    
    // 监听主进程广播
    window.electronAPI?.ipc.on('store:sync', (data: any) => {
      handleSyncMessage(data)
    })
  }
  
  // 处理同步消息
  function handleSyncMessage(data: any) {
    const { storeName, action, payload, timestamp } = data
    
    // 避免处理自己发送的消息
    if (timestamp === (window as any).__lastSyncTimestamp) return
    
    switch (action) {
      case 'UPDATE':
        applyUpdate(storeName, payload)
        break
      case 'DELETE':
        applyDelete(storeName, payload)
        break
      case 'CLEAR':
        applyClear(storeName)
        break
    }
  }
  
  // 广播状态变更
  function broadcast(storeName: string, action: string, payload: any) {
    if (!syncEnabled.value) return
    
    const data = {
      storeName,
      action,
      payload,
      timestamp: Date.now()
    }
    
    // 存储到 localStorage 触发其他窗口的 storage 事件
    localStorage.setItem(`sync:${storeName}`, JSON.stringify(data))
    setTimeout(() => {
      localStorage.removeItem(`sync:${storeName}`)
    }, 100)
    
    // 通过主进程广播
    window.electronAPI?.ipc.send('store:broadcast', data)
  }
  
  // 应用更新
  function applyUpdate(storeName: string, payload: any) {
    // 根据 store 名称更新对应的 store
    // 具体实现取决于各个 store 的更新逻辑
    console.log(`[Sync] Updating ${storeName}:`, payload)
  }
  
  function applyDelete(storeName: string, payload: any) {
    console.log(`[Sync] Deleting from ${storeName}:`, payload)
  }
  
  function applyClear(storeName: string) {
    console.log(`[Sync] Clearing ${storeName}`)
  }
  
  return {
    syncEnabled,
    initCrossWindowSync,
    broadcast
  }
})

三、数据库集成(SQLite + TypeORM)

3.1 安装依赖

复制代码
npm install typeorm better-sqlite3 reflect-metadata
npm install @types/better-sqlite3 -D

3.2 数据库配置

src/main/database/data-source.ts

复制代码
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { app } from 'electron'
import path from 'path'
import { UserEntity } from './entities/UserEntity'
import { DocumentEntity } from './entities/DocumentEntity'
import { SettingEntity } from './entities/SettingEntity'

const dbPath = path.join(app.getPath('userData'), 'database.sqlite')

export const AppDataSource = new DataSource({
  type: 'better-sqlite3',
  database: dbPath,
  synchronize: true,  // 开发环境使用,生产环境应设为 false
  logging: process.env.NODE_ENV === 'development',
  entities: [UserEntity, DocumentEntity, SettingEntity],
  migrations: ['src/main/database/migrations/**/*.ts'],
  subscribers: ['src/main/database/subscribers/**/*.ts']
})

// 初始化数据库
export async function initializeDatabase() {
  try {
    await AppDataSource.initialize()
    console.log('Database initialized successfully')
  } catch (error) {
    console.error('Database initialization failed:', error)
  }
}

3.3 实体定义

src/main/database/entities/UserEntity.ts

TypeScript 复制代码
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'
import { DocumentEntity } from './DocumentEntity'

@Entity('users')
export class UserEntity {
  @PrimaryGeneratedColumn()
  id: number
  
  @Column({ unique: true })
  email: string
  
  @Column()
  name: string
  
  @Column({ select: false })
  passwordHash: string
  
  @Column({ default: 'user' })
  role: string
  
  @Column({ nullable: true })
  avatar: string
  
  @Column({ type: 'json', nullable: true })
  preferences: any
  
  @CreateDateColumn()
  createdAt: Date
  
  @UpdateDateColumn()
  updatedAt: Date
  
  @OneToMany(() => DocumentEntity, (document) => document.user)
  documents: DocumentEntity[]
}

src/main/database/entities/DocumentEntity.ts

TypeScript 复制代码
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm'
import { UserEntity } from './UserEntity'

@Entity('documents')
export class DocumentEntity {
  @PrimaryGeneratedColumn()
  id: number
  
  @Column()
  title: string
  
  @Column({ type: 'text' })
  content: string
  
  @Column({ nullable: true })
  filePath: string
  
  @Column({ default: 0 })
  wordCount: number
  
  @ManyToOne(() => UserEntity, (user) => user.documents)
  user: UserEntity
  
  @Column()
  userId: number
  
  @CreateDateColumn()
  createdAt: Date
  
  @UpdateDateColumn()
  updatedAt: Date
}

3.4 数据库服务层

src/main/database/services/DocumentService.ts

TypeScript 复制代码
import { AppDataSource } from '../data-source'
import { DocumentEntity } from '../entities/DocumentEntity'

export class DocumentService {
  private documentRepository = AppDataSource.getRepository(DocumentEntity)
  
  // 创建文档
  async createDocument(userId: number, data: Partial<DocumentEntity>): Promise<DocumentEntity> {
    const document = this.documentRepository.create({
      ...data,
      userId
    })
    
    return await this.documentRepository.save(document)
  }
  
  // 获取用户的所有文档
  async getUserDocuments(userId: number): Promise<DocumentEntity[]> {
    return await this.documentRepository.find({
      where: { userId },
      order: { updatedAt: 'DESC' }
    })
  }
  
  // 获取单个文档
  async getDocument(id: number, userId: number): Promise<DocumentEntity | null> {
    return await this.documentRepository.findOne({
      where: { id, userId }
    })
  }
  
  // 更新文档
  async updateDocument(id: number, userId: number, data: Partial<DocumentEntity>): Promise<DocumentEntity | null> {
    const document = await this.getDocument(id, userId)
    
    if (!document) return null
    
    Object.assign(document, data)
    return await this.documentRepository.save(document)
  }
  
  // 删除文档
  async deleteDocument(id: number, userId: number): Promise<boolean> {
    const result = await this.documentRepository.delete({ id, userId })
    return result.affected > 0
  }
  
  // 搜索文档
  async searchDocuments(userId: number, keyword: string): Promise<DocumentEntity[]> {
    const queryBuilder = this.documentRepository.createQueryBuilder('document')
    
    return await queryBuilder
      .where('document.userId = :userId', { userId })
      .andWhere('document.title LIKE :keyword OR document.content LIKE :keyword', {
        keyword: `%${keyword}%`
      })
      .orderBy('document.updatedAt', 'DESC')
      .getMany()
  }
  
  // 统计文档数量
  async getDocumentCount(userId: number): Promise<number> {
    return await this.documentRepository.count({ where: { userId } })
  }
}

3.5 IPC 数据库处理器

src/main/ipc/databaseHandlers.ts

TypeScript 复制代码
import { ipcMain } from 'electron'
import { DocumentService } from '../database/services/DocumentService'

const documentService = new DocumentService()

export function databaseHandlers() {
  // 获取所有文档
  ipcMain.handle('db:documents:get-all', async (event) => {
    const userId = event.sender.id  // 实际应用中应从 session 获取
    return await documentService.getUserDocuments(userId)
  })
  
  // 获取单个文档
  ipcMain.handle('db:documents:get', async (event, id: number) => {
    const userId = event.sender.id
    return await documentService.getDocument(id, userId)
  })
  
  // 创建文档
  ipcMain.handle('db:documents:create', async (event, data: any) => {
    const userId = event.sender.id
    return await documentService.createDocument(userId, data)
  })
  
  // 更新文档
  ipcMain.handle('db:documents:update', async (event, id: number, data: any) => {
    const userId = event.sender.id
    return await documentService.updateDocument(id, userId, data)
  })
  
  // 删除文档
  ipcMain.handle('db:documents:delete', async (event, id: number) => {
    const userId = event.sender.id
    return await documentService.deleteDocument(id, userId)
  })
  
  // 搜索文档
  ipcMain.handle('db:documents:search', async (event, keyword: string) => {
    const userId = event.sender.id
    return await documentService.searchDocuments(userId, keyword)
  })
}

四、原生功能集成

4.1 系统托盘

src/main/native/tray.ts

TypeScript 复制代码
import { Tray, Menu, app, nativeImage, BrowserWindow } from 'electron'
import path from 'path'

let tray: Tray | null = null

export function initTray(mainWindow: BrowserWindow) {
  // 创建托盘图标
  const iconPath = path.join(__dirname, '../../../resources/tray-icon.png')
  const icon = nativeImage.createFromPath(iconPath)
  
  // macOS 需要设置托盘图标大小
  const trayIcon = icon.resize({ width: 16, height: 16 })
  tray = new Tray(trayIcon)
  
  // 创建托盘菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示主窗口',
      click: () => {
        if (mainWindow.isMinimized()) mainWindow.restore()
        mainWindow.show()
        mainWindow.focus()
      }
    },
    {
      label: '新建文档',
      click: () => {
        mainWindow.webContents.send('tray:new-document')
      }
    },
    { type: 'separator' },
    {
      label: '设置',
      click: () => {
        mainWindow.webContents.send('tray:open-settings')
      }
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => {
        app.quit()
      }
    }
  ])
  
  tray.setToolTip('Vue Electron App')
  tray.setContextMenu(contextMenu)
  
  // 点击托盘图标显示/隐藏窗口
  tray.on('click', () => {
    if (mainWindow.isVisible()) {
      mainWindow.hide()
    } else {
      mainWindow.show()
      mainWindow.focus()
    }
  })
}

export function updateTrayMenu(mainWindow: BrowserWindow) {
  // 动态更新托盘菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示主窗口',
      click: () => {
        mainWindow.show()
        mainWindow.focus()
      }
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => app.quit()
    }
  ])
  
  tray?.setContextMenu(contextMenu)
}

4.2 全局快捷键

src/main/native/shortcut.ts

javascript 复制代码
import { globalShortcut, BrowserWindow } from 'electron'

const shortcuts: Map<string, () => void> = new Map()

export function registerShortcuts(mainWindow: BrowserWindow) {
  // 注册快捷键
  const shortcutConfigs = [
    {
      accelerator: 'CommandOrControl+N',
      callback: () => {
        mainWindow.webContents.send('shortcut:new-document')
      }
    },
    {
      accelerator: 'CommandOrControl+S',
      callback: () => {
        mainWindow.webContents.send('shortcut:save-document')
      }
    },
    {
      accelerator: 'CommandOrControl+O',
      callback: () => {
        mainWindow.webContents.send('shortcut:open-document')
      }
    },
    {
      accelerator: 'CommandOrControl+F',
      callback: () => {
        mainWindow.webContents.send('shortcut:search')
      }
    },
    {
      accelerator: 'CommandOrControl+Shift+I',
      callback: () => {
        mainWindow.webContents.toggleDevTools()
      }
    },
    {
      accelerator: 'F11',
      callback: () => {
        if (mainWindow.isFullScreen()) {
          mainWindow.setFullScreen(false)
        } else {
          mainWindow.setFullScreen(true)
        }
      }
    }
  ]
  
  for (const config of shortcutConfigs) {
    const success = globalShortcut.register(config.accelerator, config.callback)
    
    if (success) {
      shortcuts.set(config.accelerator, config.callback)
      console.log(`Shortcut registered: ${config.accelerator}`)
    } else {
      console.warn(`Failed to register shortcut: ${config.accelerator}`)
    }
  }
}

export function unregisterShortcuts() {
  for (const [accelerator, callback] of shortcuts) {
    globalShortcut.unregister(accelerator)
    console.log(`Shortcut unregistered: ${accelerator}`)
  }
  shortcuts.clear()
}

// 注册媒体快捷键(可选)
export function registerMediaShortcuts(mainWindow: BrowserWindow) {
  const mediaShortcuts = [
    {
      accelerator: 'MediaPlayPause',
      callback: () => {
        mainWindow.webContents.send('media:play-pause')
      }
    },
    {
      accelerator: 'MediaNextTrack',
      callback: () => {
        mainWindow.webContents.send('media:next')
      }
    },
    {
      accelerator: 'MediaPreviousTrack',
      callback: () => {
        mainWindow.webContents.send('media:previous')
      }
    }
  ]
  
  for (const config of mediaShortcuts) {
    globalShortcut.register(config.accelerator, config.callback)
  }
}

4.3 拖拽上传

src/renderer/composables/useDragDrop.ts

javascript 复制代码
import { ref, onMounted, onUnmounted } from 'vue'

export function useDragDrop(targetElement: Ref<HTMLElement | null>) {
  const isDragging = ref(false)
  const draggedFiles = ref<File[]>([])
  
  const handleDragEnter = (e: DragEvent) => {
    e.preventDefault()
    isDragging.value = true
  }
  
  const handleDragOver = (e: DragEvent) => {
    e.preventDefault()
    e.dataTransfer!.dropEffect = 'copy'
  }
  
  const handleDragLeave = (e: DragEvent) => {
    e.preventDefault()
    isDragging.value = false
  }
  
  const handleDrop = async (e: DragEvent) => {
    e.preventDefault()
    isDragging.value = false
    
    const files = Array.from(e.dataTransfer?.files || [])
    draggedFiles.value = files
    
    // 处理拖拽的文件
    for (const file of files) {
      // 读取文件内容
      const content = await readFileContent(file)
      
      // 发送到主进程处理
      await window.electronAPI.file.processDroppedFile({
        path: file.path,
        name: file.name,
        content
      })
    }
  }
  
  const readFileContent = (file: File): Promise<string> => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onload = (e) => resolve(e.target?.result as string)
      reader.onerror = reject
      reader.readAsText(file)
    })
  }
  
  onMounted(() => {
    const element = targetElement.value
    if (element) {
      element.addEventListener('dragenter', handleDragEnter)
      element.addEventListener('dragover', handleDragOver)
      element.addEventListener('dragleave', handleDragLeave)
      element.addEventListener('drop', handleDrop)
    }
  })
  
  onUnmounted(() => {
    const element = targetElement.value
    if (element) {
      element.removeEventListener('dragenter', handleDragEnter)
      element.removeEventListener('dragover', handleDragOver)
      element.removeEventListener('dragleave', handleDragLeave)
      element.removeEventListener('drop', handleDrop)
    }
  })
  
  return {
    isDragging,
    draggedFiles
  }
}

4.4 剪贴板操作

src/renderer/composables/useClipboard.ts

javascript 复制代码
import { ref } from 'vue'

export function useClipboard() {
  const copied = ref(false)
  const clipboardContent = ref('')
  
  // 复制文本
  async function copyText(text: string): Promise<boolean> {
    try {
      // 优先使用 Electron API
      if (window.electronAPI?.clipboard) {
        await window.electronAPI.clipboard.writeText(text)
      } else {
        // 降级到 Web API
        await navigator.clipboard.writeText(text)
      }
      
      copied.value = true
      clipboardContent.value = text
      
      setTimeout(() => {
        copied.value = false
      }, 2000)
      
      return true
    } catch (error) {
      console.error('Failed to copy:', error)
      return false
    }
  }
  
  // 读取文本
  async function readText(): Promise<string> {
    try {
      if (window.electronAPI?.clipboard) {
        return await window.electronAPI.clipboard.readText()
      } else {
        return await navigator.clipboard.readText()
      }
    } catch (error) {
      console.error('Failed to read:', error)
      return ''
    }
  }
  
  // 复制图片
  async function copyImage(imageData: string): Promise<boolean> {
    try {
      if (window.electronAPI?.clipboard) {
        await window.electronAPI.clipboard.writeImage(imageData)
        return true
      }
      return false
    } catch (error) {
      console.error('Failed to copy image:', error)
      return false
    }
  }
  
  // 监听剪贴板变化
  function watchClipboard(callback: (content: string) => void) {
    let lastContent = ''
    
    const interval = setInterval(async () => {
      const content = await readText()
      if (content !== lastContent) {
        lastContent = content
        callback(content)
      }
    }, 500)
    
    return () => clearInterval(interval)
  }
  
  return {
    copied,
    clipboardContent,
    copyText,
    readText,
    copyImage,
    watchClipboard
  }
}

五、高级功能页面

5.1 文档编辑器页面

src/renderer/views/Documents.vue

javascript 复制代码
<template>
  <div class="documents-view">
    <div class="documents-sidebar">
      <div class="sidebar-header">
        <button @click="createNewDocument" class="btn-new">
          <span>+</span> 新建文档
        </button>
      </div>
      
      <div class="search-box">
        <input
          v-model="searchKeyword"
          type="text"
          placeholder="搜索文档..."
          @input="searchDocuments"
        />
      </div>
      
      <div class="document-list">
        <div
          v-for="doc in documents"
          :key="doc.id"
          :class="['document-item', { active: currentDocument?.id === doc.id }]"
          @click="selectDocument(doc)"
        >
          <div class="doc-icon">📄</div>
          <div class="doc-info">
            <div class="doc-title">{{ doc.title }}</div>
            <div class="doc-date">{{ formatDate(doc.updatedAt) }}</div>
          </div>
          <button class="doc-delete" @click.stop="deleteDocument(doc.id)">🗑️</button>
        </div>
        
        <div v-if="documents.length === 0" class="empty-state">
          暂无文档,点击上方按钮创建
        </div>
      </div>
    </div>
    
    <div class="documents-editor">
      <div v-if="currentDocument" class="editor-container">
        <div class="editor-toolbar">
          <input
            v-model="currentDocument.title"
            class="editor-title"
            placeholder="文档标题"
          />
          <div class="toolbar-actions">
            <button @click="saveDocument" :disabled="saving" class="btn-save">
              {{ saving ? '保存中...' : '保存' }}
            </button>
            <button @click="exportDocument" class="btn-export">导出</button>
          </div>
        </div>
        
        <textarea
          v-model="currentDocument.content"
          class="editor-content"
          placeholder="开始编写..."
          @keydown.ctrl.s.prevent="saveDocument"
        ></textarea>
        
        <div class="editor-status">
          <span>字数: {{ wordCount }}</span>
          <span>最后保存: {{ formatDate(currentDocument.updatedAt) }}</span>
        </div>
      </div>
      
      <div v-else class="editor-empty">
        <p>选择或创建一个文档开始编辑</p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
import { useFileSystem } from '@/composables/useFileSystem'

interface Document {
  id: number
  title: string
  content: string
  createdAt: string
  updatedAt: string
}

const documents = ref<Document[]>([])
const currentDocument = ref<Document | null>(null)
const searchKeyword = ref('')
const saving = ref(false)

const { readFile, writeFile, selectFile, saveFileDialog } = useFileSystem()

const wordCount = computed(() => {
  if (!currentDocument.value?.content) return 0
  return currentDocument.value.content.trim().split(/\s+/).length
})

// 加载文档列表
async function loadDocuments() {
  const result = await window.electronAPI.db.documents.getAll()
  documents.value = result
}

// 创建新文档
async function createNewDocument() {
  const newDoc = {
    title: '未命名文档',
    content: ''
  }
  
  const result = await window.electronAPI.db.documents.create(newDoc)
  documents.value.unshift(result)
  currentDocument.value = result
}

// 选择文档
function selectDocument(doc: Document) {
  currentDocument.value = { ...doc }
}

// 保存文档
async function saveDocument() {
  if (!currentDocument.value) return
  
  saving.value = true
  
  try {
    await window.electronAPI.db.documents.update(
      currentDocument.value.id,
      {
        title: currentDocument.value.title,
        content: currentDocument.value.content
      }
    )
    
    // 更新列表中的文档
    const index = documents.value.findIndex(d => d.id === currentDocument.value!.id)
    if (index !== -1) {
      documents.value[index] = { ...currentDocument.value }
    }
    
    // 显示保存成功通知
    window.electronAPI?.system.showNotification('保存成功', '文档已保存')
  } catch (error) {
    console.error('保存失败:', error)
    window.electronAPI?.system.showNotification('保存失败', '请重试')
  } finally {
    saving.value = false
  }
}

// 删除文档
async function deleteDocument(id: number) {
  const confirmed = confirm('确定要删除这个文档吗?')
  if (!confirmed) return
  
  await window.electronAPI.db.documents.delete(id)
  documents.value = documents.value.filter(d => d.id !== id)
  
  if (currentDocument.value?.id === id) {
    currentDocument.value = null
  }
}

// 搜索文档
async function searchDocuments() {
  if (searchKeyword.value.trim()) {
    const results = await window.electronAPI.db.documents.search(searchKeyword.value)
    documents.value = results
  } else {
    await loadDocuments()
  }
}

// 导出文档
async function exportDocument() {
  if (!currentDocument.value) return
  
  const filePath = await saveFileDialog(`${currentDocument.value.title}.md`)
  if (filePath) {
    await writeFile(filePath, currentDocument.value.content)
    window.electronAPI?.system.showNotification('导出成功', `文档已保存到 ${filePath}`)
  }
}

// 格式化日期
function formatDate(dateStr: string) {
  const date = new Date(dateStr)
  return date.toLocaleString('zh-CN')
}

// 监听快捷键
function handleShortcut(event: any, action: string) {
  switch (action) {
    case 'new-document':
      createNewDocument()
      break
    case 'save-document':
      saveDocument()
      break
    case 'search':
      document.querySelector('.search-box input')?.focus()
      break
  }
}

onMounted(() => {
  loadDocuments()
  
  // 监听快捷键
  window.electronAPI?.ipc.on('shortcut:new-document', handleShortcut)
  window.electronAPI?.ipc.on('shortcut:save-document', handleShortcut)
  window.electronAPI?.ipc.on('shortcut:search', handleShortcut)
})

onUnmounted(() => {
  window.electronAPI?.ipc.removeListener('shortcut:new-document', handleShortcut)
  window.electronAPI?.ipc.removeListener('shortcut:save-document', handleShortcut)
  window.electronAPI?.ipc.removeListener('shortcut:search', handleShortcut)
})
</script>

<style scoped>
.documents-view {
  display: flex;
  height: 100%;
  gap: 1px;
  background: var(--border-color);
}

.documents-sidebar {
  width: 280px;
  background: var(--bg-primary);
  display: flex;
  flex-direction: column;
}

.sidebar-header {
  padding: 16px;
  border-bottom: 1px solid var(--border-color);
}

.btn-new {
  width: 100%;
  padding: 8px 12px;
  background: var(--primary-color);
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.search-box {
  padding: 12px;
  border-bottom: 1px solid var(--border-color);
}

.search-box input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--bg-secondary);
  color: var(--text-primary);
}

.document-list {
  flex: 1;
  overflow-y: auto;
}

.document-item {
  display: flex;
  align-items: center;
  padding: 12px;
  gap: 12px;
  cursor: pointer;
  transition: background 0.2s;
}

.document-item:hover {
  background: var(--hover-bg);
}

.document-item.active {
  background: var(--primary-color);
  color: white;
}

.doc-icon {
  font-size: 20px;
}

.doc-info {
  flex: 1;
}

.doc-title {
  font-weight: 500;
  margin-bottom: 4px;
}

.doc-date {
  font-size: 12px;
  opacity: 0.7;
}

.doc-delete {
  background: none;
  border: none;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.2s;
}

.doc-delete:hover {
  opacity: 1;
}

.documents-editor {
  flex: 1;
  background: var(--bg-primary);
}

.editor-container {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.editor-toolbar {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

.editor-title {
  flex: 1;
  font-size: 18px;
  font-weight: 600;
  border: none;
  background: transparent;
  color: var(--text-primary);
  outline: none;
}

.toolbar-actions {
  display: flex;
  gap: 8px;
}

.btn-save,
.btn-export {
  padding: 6px 12px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

.btn-save {
  background: var(--primary-color);
  color: white;
}

.btn-export {
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  color: var(--text-primary);
}

.editor-content {
  flex: 1;
  padding: 20px;
  border: none;
  resize: none;
  font-family: 'Monaco', 'Menlo', monospace;
  font-size: 14px;
  line-height: 1.6;
  background: var(--bg-primary);
  color: var(--text-primary);
  outline: none;
}

.editor-status {
  padding: 8px 16px;
  border-top: 1px solid var(--border-color);
  font-size: 12px;
  color: var(--text-secondary);
  display: flex;
  justify-content: space-between;
}

.editor-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--text-secondary);
}

.empty-state {
  padding: 40px;
  text-align: center;
  color: var(--text-secondary);
}
</style>

六、打包与发布配置

6.1 electron-builder 完整配置

electron-builder.json

javascript 复制代码
{
  "appId": "com.electron.vue-demo",
  "productName": "Vue Electron App",
  "copyright": "Copyright © 2024",
  "directories": {
    "output": "release"
  },
  "files": [
    "dist-electron/**/*",
    "dist/**/*",
    "!**/node_modules/*/{test,__tests__,tests,example,docs}",
    "!**/*.{iml,o,obj,log,map,md,txt,py,sh}"
  ],
  "asar": true,
  "asarUnpack": [
    "**/node_modules/better-sqlite3/**/*"
  ],
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      }
    ],
    "icon": "build/icon.ico",
    "publisherName": "Your Company",
    "signingHashAlgorithms": ["sha256"],
    "signDlls": true
  },
  "nsis": {
    "oneClick": false,
    "perMachine": true,
    "allowToChangeInstallationDirectory": true,
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true
  },
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "zip",
        "arch": ["x64", "arm64"]
      }
    ],
    "icon": "build/icon.icns",
    "category": "public.app-category.productivity",
    "hardenedRuntime": true,
    "gatekeeperAssess": true
  },
  "dmg": {
    "title": "Vue Electron App ${version}",
    "icon": "build/icon.icns",
    "background": "build/dmg-background.png"
  },
  "linux": {
    "target": ["AppImage", "deb"],
    "icon": "build/icon.png",
    "category": "Utility",
    "maintainer": "support@example.com"
  },
  "publish": {
    "provider": "github",
    "releaseType": "release",
    "private": false
  }
}

6.2 构建脚本

package.json scripts:

javascript 复制代码
{
  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "preview": "electron-vite preview",
    "electron:build": "npm run build && electron-builder",
    "electron:build:win": "npm run build && electron-builder --win",
    "electron:build:mac": "npm run build && electron-builder --mac",
    "electron:build:linux": "npm run build && electron-builder --linux",
    "release": "npm run build && electron-builder --publish always",
    "postinstall": "electron-builder install-app-deps"
  }
}

七、性能优化总结

7.1 优化检查清单

优化项 实现方式 效果
路由懒加载 () => import('./views/xxx.vue') 首屏加载 -40%
组件懒加载 defineAsyncComponent 按需加载组件
虚拟滚动 使用 vue-virtual-scroller 长列表性能 +10x
防抖节流 useDebounceFn, useThrottleFn 减少无效渲染
请求缓存 实现 RequestCache 网络请求 -70%
图片懒加载 使用 IntersectionObserver 首屏流量 -50%
Web Worker 重任务放到 Worker UI 不卡顿
数据库索引 为查询字段添加索引 查询速度 +5x

7.2 生产环境建议

javascript 复制代码
// 生产环境配置建议
const productionConfig = {
  // 1. 关闭 devTools
  devTools: false,
  
  // 2. 开启沙箱
  sandbox: true,
  
  // 3. 关闭 webSecurity(谨慎)
  webSecurity: true,
  
  // 4. 开启 GPU 加速
  hardwareAcceleration: true,
  
  // 5. 启用 V8 缓存
  // 使用 v8-compile-cache 包
  
  // 6. 数据库优化
  database: {
    // 启用 WAL 模式
    // 设置合适的缓存大小
    // 定期执行 VACUUM
  }
}
相关推荐
西西学代码2 小时前
查找设备页面(amap_map)
开发语言·前端·javascript
浩星2 小时前
electron系列7之Electron + Vue 3:构建现代化桌面应用(上)
javascript·vue.js·electron
m0_738120722 小时前
渗透测试基础ctfshow——Web应用安全与防护(四)
前端·安全·web安全·网络安全·flask·弱口令爆破
似水流年QC2 小时前
Chrome Performance 面板前端性能分析从入门到实战
前端·chrome
君穆南2 小时前
docker里面的minio的备份方法
前端
Thomas21432 小时前
--remote-debugging-port=9222 和 chrome://inspect/#remote-debugging 区别
前端·chrome
Luna-player2 小时前
二本生找前端工作
前端
M ? A2 小时前
Vue3 转 React 工具 VuReact v1.6.0 更新:useAttrs 完美兼容,修复模板迁移 / 类型错误
前端·javascript·vue.js·react.js·开源·vureact
计算机学姐2 小时前
基于SpringBoot的充电桩预约管理系统【阶梯电费+个性化推荐+数据可视化】
java·vue.js·spring boot·后端·mysql·信息可视化·mybatis