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

一、为什么选择 Vue 3 + Electron?

复制代码
Vue 3 核心优势:                                                 
  ✅ Composition API - 逻辑复用更优雅                             
  ✅ 响应式系统 - 性能优于 React                                  
  ✅ 单文件组件 - 模板/脚本/样式一体化                            
  ✅ 更小的运行时 - ~16KB gzip                                    
  ✅ 更好的 TypeScript 支持                                       
  ✅ 平滑的学习曲线    

二、项目初始化

2.1 创建 Vue 3 项目

javascript 复制代码
# 使用 create-vue 创建项目
npm create vue@latest electron-vue-demo

# 选择以下配置:
# ✔ TypeScript? ... Yes
# ✔ JSX Support? ... No
# ✔ Vue Router? ... Yes
# ✔ Pinia? ... Yes
# ✔ Vitest? ... Yes
# ✔ ESLint? ... Yes
# ✔ Prettier? ... Yes

cd electron-vue-demo

2.2 添加 Electron

javascript 复制代码
# 安装 Electron
npm install electron electron-builder electron-vite -D

# 安装 electron-vite 插件
npm install @electron-toolkit/preload -D

2.3 配置 electron-vite

electron.vite.config.ts

javascript 复制代码
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  main: {
    build: {
      outDir: 'dist-electron/main',
      rollupOptions: {
        external: ['electron']
      }
    },
    resolve: {
      alias: {
        '@main': path.resolve(__dirname, 'src/main')
      }
    }
  },
  preload: {
    build: {
      outDir: 'dist-electron/preload',
      rollupOptions: {
        input: path.resolve(__dirname, 'src/preload/index.ts'),
        external: ['electron']
      }
    }
  },
  renderer: {
    root: '.',
    build: {
      outDir: 'dist/renderer',
      rollupOptions: {
        input: path.resolve(__dirname, 'index.html')
      }
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src/renderer')
      }
    },
    plugins: [vue()],
    server: {
      port: 5173,
      strictPort: true
    }
  }
})

2.4 目录结构

javascript 复制代码
electron-vue-demo/
├── src/
│   ├── main/                    # 主进程代码
│   │   ├── index.ts            # 入口
│   │   ├── window/             # 窗口管理
│   │   ├── ipc/                # IPC 处理器
│   │   └── native/             # 原生功能
│   │
│   ├── preload/                # 预加载脚本
│   │   └── index.ts
│   │
│   └── renderer/               # Vue 3 渲染进程
│       ├── main.ts             # Vue 入口
│       ├── App.vue             # 根组件
│       ├── assets/             # 静态资源
│       ├── components/         # 公共组件
│       ├── views/              # 页面组件
│       ├── router/             # 路由配置
│       ├── stores/             # Pinia 状态
│       ├── composables/        # 组合式函数
│       └── styles/             # 全局样式
│
├── index.html                  # HTML 模板
├── package.json
├── electron.vite.config.ts
└── tsconfig.json

2.5 package.json 配置

javascript 复制代码
{
  "name": "electron-vue-demo",
  "version": "1.0.0",
  "description": "Modern Electron + Vue 3 Desktop Application",
  "main": "dist-electron/main/index.js",
  "scripts": {
    "dev": "electron-vite dev",
    "dev:renderer": "vite",
    "build": "electron-vite build",
    "preview": "electron-vite preview",
    "electron:build": "npm run build && electron-builder",
    "lint": "eslint . --ext .ts,.tsx,.vue",
    "type-check": "vue-tsc --noEmit"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "@vitejs/plugin-vue": "^4.5.0",
    "electron": "^28.0.0",
    "electron-builder": "^24.9.0",
    "electron-vite": "^2.0.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vue": "^3.4.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0"
  }
}

三、主进程实现

3.1 主进程入口

src/main/index.ts

javascript 复制代码
import { app, BrowserWindow, Menu } from 'electron'
import { electronApp, optimizer } from '@electron-toolkit/preload'
import { createMainWindow } from './window/mainWindow'
import { setupIpcHandlers } from './ipc'
import { initTray } from './native/tray'
import { initMenu } from './native/menu'
import { setupAutoUpdater } from './updater'
import { logger } from './utils/logger'

// 开发环境判断
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged

// 单实例锁定
const gotTheLock = app.requestSingleInstanceLock()

if (!gotTheLock) {
  app.quit()
} else {
  app.on('second-instance', () => {
    // 有人试图运行第二个实例,聚焦主窗口
    const mainWindow = BrowserWindow.getAllWindows()[0]
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore()
      mainWindow.focus()
    }
  })
}

// 应用准备就绪
app.whenReady().then(async () => {
  // 设置 Electron 应用
  electronApp.setAppUserModelId('com.electron.vue-demo')
  
  // 开发环境设置
  if (isDev) {
    // 安装 Vue Devtools
    try {
      const { default: installExtension, VUEJS_DEVTOOLS } = await import('electron-devtools-installer')
      await installExtension(VUEJS_DEVTOOLS)
      logger.info('Vue Devtools installed')
    } catch (error) {
      logger.warn('Failed to install Vue Devtools:', error)
    }
  }
  
  // 优化窗口行为
  optimizer.watchWindowShortcuts()
  
  // 创建主窗口
  const mainWindow = createMainWindow()
  
  // 初始化 IPC 处理器
  setupIpcHandlers()
  
  // 初始化系统托盘
  initTray(mainWindow)
  
  // 初始化菜单
  initMenu(mainWindow)
  
  // 设置自动更新
  if (!isDev) {
    setupAutoUpdater(mainWindow)
  }
  
  // macOS: 点击 dock 图标时重新创建窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createMainWindow()
    }
  })
})

// 所有窗口关闭时
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// 应用退出前
app.on('will-quit', () => {
  logger.info('Application is quitting')
})

3.2 主窗口管理

src/main/window/mainWindow.ts

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

const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged

let mainWindow: BrowserWindow | null = null

export function createMainWindow(): BrowserWindow {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    frame: true,
    titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
    show: false,  // 先隐藏,ready-to-show 再显示
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: false,
      webSecurity: !isDev  // 开发环境允许跨域
    },
    icon: path.join(__dirname, '../../../resources/icon.png')
  })
  
  // 加载页面
  if (isDev) {
    mainWindow.loadURL('http://localhost:5173')
    mainWindow.webContents.openDevTools()
  } else {
    mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
  }
  
  // 窗口准备就绪后显示
  mainWindow.once('ready-to-show', () => {
    mainWindow?.show()
    
    // 开发环境自动打开 DevTools
    if (isDev) {
      mainWindow?.webContents.openDevTools()
    }
  })
  
  // 处理外部链接
  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })
  
  // 窗口关闭时清理
  mainWindow.on('closed', () => {
    mainWindow = null
  })
  
  return mainWindow
}

export function getMainWindow(): BrowserWindow | null {
  return mainWindow
}

3.3 IPC 处理器

src/main/ipc/index.ts

javascript 复制代码
import { ipcMain } from 'electron'
import { fileHandlers } from './fileHandlers'
import { systemHandlers } from './systemHandlers'
import { windowHandlers } from './windowHandlers'

export function setupIpcHandlers() {
  // 文件操作处理器
  fileHandlers()
  
  // 系统操作处理器
  systemHandlers()
  
  // 窗口操作处理器
  windowHandlers()
}

src/main/ipc/fileHandlers.ts

javascript 复制代码
import { ipcMain, dialog } from 'electron'
import fs from 'fs/promises'
import path from 'path'

export function fileHandlers() {
  // 读取文件
  ipcMain.handle('file:read', async (event, filePath: string) => {
    try {
      const content = await fs.readFile(filePath, 'utf-8')
      return { success: true, content }
    } catch (error) {
      return { success: false, error: error.message }
    }
  })
  
  // 写入文件
  ipcMain.handle('file:write', async (event, filePath: string, content: string) => {
    try {
      await fs.writeFile(filePath, content, 'utf-8')
      return { success: true }
    } catch (error) {
      return { success: false, error: error.message }
    }
  })
  
  // 选择文件
  ipcMain.handle('file:select', async () => {
    const result = await dialog.showOpenDialog({
      properties: ['openFile'],
      filters: [
        { name: 'Text Files', extensions: ['txt', 'md'] },
        { name: 'JSON Files', extensions: ['json'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    })
    
    if (result.canceled) return null
    return result.filePaths[0]
  })
  
  // 选择目录
  ipcMain.handle('file:select-directory', async () => {
    const result = await dialog.showOpenDialog({
      properties: ['openDirectory']
    })
    
    if (result.canceled) return null
    return result.filePaths[0]
  })
  
  // 保存文件对话框
  ipcMain.handle('file:save-dialog', async (event, defaultName?: string) => {
    const result = await dialog.showSaveDialog({
      defaultPath: defaultName,
      filters: [
        { name: 'Text Files', extensions: ['txt', 'md'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    })
    
    if (result.canceled) return null
    return result.filePath
  })
}

src/main/ipc/systemHandlers.ts

javascript 复制代码
import { ipcMain, shell, nativeTheme } from 'electron'
import os from 'os'

export function systemHandlers() {
  // 获取系统信息
  ipcMain.handle('system:info', async () => {
    return {
      platform: process.platform,
      arch: process.arch,
      version: process.version,
      electronVersion: process.versions.electron,
      chromeVersion: process.versions.chrome,
      nodeVersion: process.versions.node,
      hostname: os.hostname(),
      cpus: os.cpus().length,
      totalMemory: os.totalmem(),
      freeMemory: os.freemem()
    }
  })
  
  // 打开外部链接
  ipcMain.handle('system:open-external', async (event, url: string) => {
    await shell.openExternal(url)
  })
  
  // 显示通知
  ipcMain.handle('system:notification', async (event, title: string, body: string) => {
    // 主进程通知会在所有窗口上显示
    const { Notification } = await import('electron')
    new Notification({ title, body }).show()
  })
  
  // 获取主题
  ipcMain.handle('system:theme', async () => {
    return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
  })
  
  // 监听主题变化(主动推送)
  nativeTheme.on('updated', () => {
    const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
    const windows = BrowserWindow.getAllWindows()
    windows.forEach(win => {
      win.webContents.send('system:theme-changed', theme)
    })
  })
}

src/main/ipc/windowHandlers.ts

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

export function windowHandlers() {
  // 窗口最小化
  ipcMain.on('window:minimize', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    win?.minimize()
  })
  
  // 窗口最大化/还原
  ipcMain.on('window:maximize', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    if (win?.isMaximized()) {
      win.unmaximize()
    } else {
      win?.maximize()
    }
  })
  
  // 关闭窗口
  ipcMain.on('window:close', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    win?.close()
  })
  
  // 获取窗口最大化状态
  ipcMain.handle('window:is-maximized', (event) => {
    const win = BrowserWindow.fromWebContents(event.sender)
    return win?.isMaximized() || false
  })
}

四、预加载脚本

src/preload/index.ts

javascript 复制代码
import { contextBridge, ipcRenderer } from 'electron'

// 定义暴露的 API 类型
export interface ElectronAPI {
  // 文件操作
  file: {
    read: (path: string) => Promise<{ success: boolean; content?: string; error?: string }>
    write: (path: string, content: string) => Promise<{ success: boolean; error?: string }>
    select: () => Promise<string | null>
    selectDirectory: () => Promise<string | null>
    saveDialog: (defaultName?: string) => Promise<string | null>
  }
  
  // 系统操作
  system: {
    getInfo: () => Promise<SystemInfo>
    openExternal: (url: string) => Promise<void>
    showNotification: (title: string, body: string) => Promise<void>
    getTheme: () => Promise<'light' | 'dark'>
    onThemeChange: (callback: (theme: 'light' | 'dark') => void) => () => void
  }
  
  // 窗口操作
  window: {
    minimize: () => void
    maximize: () => void
    close: () => void
    isMaximized: () => Promise<boolean>
  }
  
  // 版本信息
  versions: {
    electron: () => string
    node: () => string
    chrome: () => string
  }
}

interface SystemInfo {
  platform: string
  arch: string
  version: string
  electronVersion: string
  chromeVersion: string
  nodeVersion: string
  hostname: string
  cpus: number
  totalMemory: number
  freeMemory: number
}

// 实现 API
const electronAPI: ElectronAPI = {
  file: {
    read: (path) => ipcRenderer.invoke('file:read', path),
    write: (path, content) => ipcRenderer.invoke('file:write', path, content),
    select: () => ipcRenderer.invoke('file:select'),
    selectDirectory: () => ipcRenderer.invoke('file:select-directory'),
    saveDialog: (defaultName) => ipcRenderer.invoke('file:save-dialog', defaultName)
  },
  
  system: {
    getInfo: () => ipcRenderer.invoke('system:info'),
    openExternal: (url) => ipcRenderer.invoke('system:open-external', url),
    showNotification: (title, body) => ipcRenderer.invoke('system:notification', title, body),
    getTheme: () => ipcRenderer.invoke('system:theme'),
    onThemeChange: (callback) => {
      const handler = (_event: any, theme: 'light' | 'dark') => callback(theme)
      ipcRenderer.on('system:theme-changed', handler)
      return () => ipcRenderer.removeListener('system:theme-changed', handler)
    }
  },
  
  window: {
    minimize: () => ipcRenderer.send('window:minimize'),
    maximize: () => ipcRenderer.send('window:maximize'),
    close: () => ipcRenderer.send('window:close'),
    isMaximized: () => ipcRenderer.invoke('window:is-maximized')
  },
  
  versions: {
    electron: () => process.versions.electron,
    node: () => process.versions.node,
    chrome: () => process.versions.chrome
  }
}

// 暴露 API 到渲染进程
contextBridge.exposeInMainWorld('electronAPI', electronAPI)

// 类型声明
declare global {
  interface Window {
    electronAPI: ElectronAPI
  }
}

五、Vue 3 渲染进程

5.1 入口文件

src/renderer/main.ts

javascript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './styles/global.css'

// 创建应用
const app = createApp(App)

// 使用插件
app.use(createPinia())
app.use(router)

// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
  console.error('Vue Error:', err, info)
  window.electronAPI?.system.showNotification('错误', err?.toString() || '未知错误')
}

// 挂载
app.mount('#app')

// 开发环境输出信息
if (import.meta.env.DEV) {
  console.log('Electron API available:', !!window.electronAPI)
  console.log('Electron versions:', window.electronAPI?.versions)
}

5.2 根组件

src/renderer/App.vue

javascript 复制代码
<template>
  <div class="app-container" :class="{ dark: isDarkTheme }">
    <!-- 自定义标题栏 -->
    <div v-if="!isFullScreen" class="title-bar">
      <div class="title-bar-drag-area"></div>
      <div class="window-controls">
        <button @click="minimizeWindow" class="control-btn">─</button>
        <button @click="maximizeWindow" class="control-btn">
          {{ isMaximized ? '❐' : '□' }}
        </button>
        <button @click="closeWindow" class="control-btn close">✕</button>
      </div>
    </div>

    <!-- 主内容区 -->
    <div class="main-layout">
      <aside class="sidebar">
        <div class="logo">
          <img src="/vite.svg" alt="Logo" />
          <span>Vue Electron</span>
        </div>
        
        <nav class="nav-menu">
          <router-link to="/" class="nav-item">
            <span class="icon">🏠</span>
            <span>仪表盘</span>
          </router-link>
          <router-link to="/files" class="nav-item">
            <span class="icon">📁</span>
            <span>文件管理</span>
          </router-link>
          <router-link to="/settings" class="nav-item">
            <span class="icon">⚙️</span>
            <span>设置</span>
          </router-link>
          <router-link to="/about" class="nav-item">
            <span class="icon">ℹ️</span>
            <span>关于</span>
          </router-link>
        </nav>
        
        <div class="system-info">
          <div class="info-item">
            <span>Node:</span>
            <span>{{ versions.node }}</span>
          </div>
          <div class="info-item">
            <span>Electron:</span>
            <span>{{ versions.electron }}</span>
          </div>
        </div>
      </aside>
      
      <main class="content">
        <router-view v-slot="{ Component }">
          <transition name="fade" mode="out-in">
            <component :is="Component" />
          </transition>
        </router-view>
      </main>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const isDarkTheme = ref(false)
const isMaximized = ref(false)
const isFullScreen = ref(false)
const versions = ref({
  node: '',
  electron: '',
  chrome: ''
})

// 窗口控制
const minimizeWindow = () => {
  window.electronAPI?.window.minimize()
}

const maximizeWindow = async () => {
  window.electronAPI?.window.maximize()
  isMaximized.value = await window.electronAPI?.window.isMaximized() || false
}

const closeWindow = () => {
  window.electronAPI?.window.close()
}

// 主题监听
let unsubscribeTheme: (() => void) | undefined

onMounted(async () => {
  // 获取版本信息
  versions.value = {
    node: window.electronAPI?.versions.node() || '',
    electron: window.electronAPI?.versions.electron() || '',
    chrome: window.electronAPI?.versions.chrome() || ''
  }
  
  // 获取当前主题
  const theme = await window.electronAPI?.system.getTheme()
  isDarkTheme.value = theme === 'dark'
  
  // 监听主题变化
  unsubscribeTheme = window.electronAPI?.system.onThemeChange((theme) => {
    isDarkTheme.value = theme === 'dark'
    document.documentElement.setAttribute('data-theme', theme)
  })
  
  // 获取窗口状态
  isMaximized.value = await window.electronAPI?.window.isMaximized() || false
})

onUnmounted(() => {
  unsubscribeTheme?.()
})
</script>

<style scoped>
.app-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: var(--bg-primary);
  color: var(--text-primary);
}

.title-bar {
  height: 32px;
  display: flex;
  justify-content: space-between;
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border-color);
  -webkit-app-region: drag;
}

.title-bar-drag-area {
  flex: 1;
  -webkit-app-region: drag;
}

.window-controls {
  display: flex;
  -webkit-app-region: no-drag;
}

.control-btn {
  width: 46px;
  height: 32px;
  border: none;
  background: transparent;
  color: var(--text-primary);
  cursor: pointer;
  font-size: 14px;
  transition: background 0.2s;
}

.control-btn:hover {
  background: var(--hover-bg);
}

.control-btn.close:hover {
  background: #e81123;
  color: white;
}

.main-layout {
  display: flex;
  flex: 1;
  overflow: hidden;
}

.sidebar {
  width: 240px;
  background: var(--bg-secondary);
  border-right: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
}

.logo {
  padding: 20px;
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 18px;
  font-weight: 600;
  border-bottom: 1px solid var(--border-color);
}

.logo img {
  width: 32px;
  height: 32px;
}

.nav-menu {
  flex: 1;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  color: var(--text-primary);
  text-decoration: none;
  transition: background 0.2s;
}

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

.nav-item.router-link-active {
  background: var(--primary-color);
  color: white;
}

.system-info {
  padding: 16px;
  border-top: 1px solid var(--border-color);
  font-size: 12px;
  color: var(--text-secondary);
}

.info-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 4px;
}

.content {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

5.3 路由配置

src/renderer/router/index.ts

javascript 复制代码
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '仪表盘' }
  },
  {
    path: '/files',
    name: 'Files',
    component: () => import('@/views/Files.vue'),
    meta: { title: '文件管理' }
  },
  {
    path: '/settings',
    name: 'Settings',
    component: () => import('@/views/Settings.vue'),
    meta: { title: '设置' }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue'),
    meta: { title: '关于' }
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  // 更新窗口标题
  document.title = `${to.meta.title || 'Electron App'} - Vue Electron`
  next()
})

export default router

5.4 Pinia Store

src/renderer/stores/app.ts

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

export const useAppStore = defineStore('app', () => {
  // State
  const theme = ref<'light' | 'dark'>('light')
  const sidebarCollapsed = ref(false)
  const notifications = ref<Array<{ id: string; title: string; message: string; type: string }>>([])
  
  // Getters
  const isDarkTheme = computed(() => theme.value === 'dark')
  
  // Actions
  function setTheme(newTheme: 'light' | 'dark') {
    theme.value = newTheme
    localStorage.setItem('theme', newTheme)
    document.documentElement.setAttribute('data-theme', newTheme)
  }
  
  function toggleTheme() {
    setTheme(theme.value === 'light' ? 'dark' : 'light')
  }
  
  function toggleSidebar() {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }
  
  function addNotification(title: string, message: string, type: string = 'info') {
    const id = Date.now().toString()
    notifications.value.push({ id, title, message, type })
    
    // 5秒后自动移除
    setTimeout(() => {
      removeNotification(id)
    }, 5000)
  }
  
  function removeNotification(id: string) {
    const index = notifications.value.findIndex(n => n.id === id)
    if (index !== -1) {
      notifications.value.splice(index, 1)
    }
  }
  
  // 初始化
  function init() {
    const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
    if (savedTheme) {
      theme.value = savedTheme
      document.documentElement.setAttribute('data-theme', savedTheme)
    } else {
      // 跟随系统主题
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
      theme.value = prefersDark ? 'dark' : 'light'
    }
  }
  
  init()
  
  return {
    // State
    theme,
    sidebarCollapsed,
    notifications,
    // Getters
    isDarkTheme,
    // Actions
    setTheme,
    toggleTheme,
    toggleSidebar,
    addNotification,
    removeNotification
  }
})

5.5 组合式函数

src/renderer/composables/useFileSystem.ts

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

export function useFileSystem() {
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  // 读取文件
  async function readFile(filePath: string) {
    loading.value = true
    error.value = null
    
    try {
      const result = await window.electronAPI.file.read(filePath)
      if (result.success) {
        return result.content
      } else {
        error.value = result.error || '读取文件失败'
        return null
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : '未知错误'
      return null
    } finally {
      loading.value = false
    }
  }
  
  // 写入文件
  async function writeFile(filePath: string, content: string) {
    loading.value = true
    error.value = null
    
    try {
      const result = await window.electronAPI.file.write(filePath, content)
      if (!result.success) {
        error.value = result.error || '写入文件失败'
      }
      return result.success
    } catch (err) {
      error.value = err instanceof Error ? err.message : '未知错误'
      return false
    } finally {
      loading.value = false
    }
  }
  
  // 选择文件
  async function selectFile() {
    return await window.electronAPI.file.select()
  }
  
  // 选择目录
  async function selectDirectory() {
    return await window.electronAPI.file.selectDirectory()
  }
  
  // 保存文件对话框
  async function saveFileDialog(defaultName?: string) {
    return await window.electronAPI.file.saveDialog(defaultName)
  }
  
  return {
    loading,
    error,
    readFile,
    writeFile,
    selectFile,
    selectDirectory,
    saveFileDialog
  }
}

六、全局样式

src/renderer/styles/global.css

javascript 复制代码
:root {
  /* Light theme */
  --bg-primary: #ffffff;
  --bg-secondary: #f5f5f5;
  --text-primary: #333333;
  --text-secondary: #666666;
  --border-color: #e0e0e0;
  --hover-bg: #f0f0f0;
  --primary-color: #42b883;
  --primary-hover: #33a06f;
  --danger-color: #e74c3c;
  --danger-hover: #c0392b;
  --success-color: #27ae60;
  --warning-color: #f39c12;
}

[data-theme="dark"] {
  --bg-primary: #1e1e1e;
  --bg-secondary: #2d2d2d;
  --text-primary: #e0e0e0;
  --text-secondary: #a0a0a0;
  --border-color: #404040;
  --hover-bg: #3d3d3d;
  --primary-color: #42b883;
  --primary-hover: #33a06f;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  overflow: hidden;
}

/* 滚动条样式 */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: var(--bg-secondary);
}

::-webkit-scrollbar-thumb {
  background: var(--border-color);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--text-secondary);
}

/* 按钮样式 */
button {
  font-family: inherit;
}

/* 工具类 */
.text-center {
  text-align: center;
}

.mt-4 {
  margin-top: 16px;
}

.mb-4 {
  margin-bottom: 16px;
}

.p-4 {
  padding: 16px;
}

七、运行与调试

7.1 开发命令

javascript 复制代码
# 启动开发服务器
npm run dev

# 构建
npm run build

# 打包应用
npm run electron:build

# 类型检查
npm run type-check

# 代码检查
npm run lint

7.2 调试技巧

javascript 复制代码
// 在 Vue 组件中调试
console.log('Vue component log')
window.electronAPI?.system.showNotification('Debug', 'Message')

// 主进程日志
import { logger } from './utils/logger'
logger.info('Main process log')

// 使用 Vue Devtools(开发环境自动安装)
// 使用 Electron 内置 DevTools
相关推荐
M ? A2 小时前
Vue3 转 React 工具 VuReact v1.6.0 更新:useAttrs 完美兼容,修复模板迁移 / 类型错误
前端·javascript·vue.js·react.js·开源·vureact
计算机学姐2 小时前
基于SpringBoot的充电桩预约管理系统【阶梯电费+个性化推荐+数据可视化】
java·vue.js·spring boot·后端·mysql·信息可视化·mybatis
低保和光头哪个先来2 小时前
解决 ios 使用 video 全屏未铺满页面问题
前端·javascript·vue.js·ios·前端框架
MacroZheng2 小时前
全面升级!看看人家的后台管理系统,确实清新优雅!
前端·vue.js·typescript
每天吃饭的羊2 小时前
Node.js 创建可二次编辑的 HTML 文档并生成文件
开发语言·javascript·ecmascript
禅思院2 小时前
一个轻量级 Vue3 轮播组件:支持多视图、滑动距离决定切换数量,核心原理与 Swiper 对比
前端·vue.js·typescript
牛马1112 小时前
Flutter BoxDecoration border 完整用法
开发语言·前端·javascript
We་ct3 小时前
LeetCode 149. 直线上最多的点数:题解深度剖析
前端·javascript·算法·leetcode·typescript
M ? A3 小时前
Vue3 转 React:组件透传 Attributes 与 useAttrs 使用详解|VuReact 实战
前端·javascript·vue.js·经验分享·react.js·开源·vureact