Electron + Vue 3 实战:构建跨平台应用

1. 基础概念与架构

1.1 什么是 Electron

Electron 是一个使用 Web 技术(HTML、CSS、JavaScript)构建跨平台桌面应用的框架,由 GitHub 开发并维护。

1.2 核心架构

┌─────────────────────────────────────────────────────────┐
│                      Electron 应用                       │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────────┐         ┌─────────────────────┐    │
│  │   主进程 Main    │◄──────►│  渲染进程 Renderer   │    │
│  │   (Node.js)     │  IPC    │  (Chromium + Vue)   │    │
│  └────────┬────────┘         └──────────┬──────────┘    │
│           │                              │               │
│  ┌────────▼────────┐            ┌────────▼──────────┐   │
│  │  系统 API 访问   │            │   Web APIs 访问   │   │
│  │  - 窗口管理      │            │   - DOM           │   │
│  │  - 菜单栏        │            │   - Fetch/AJAX    │   │
│  │  - 托盘图标      │            │   - Canvas        │   │
│  │  - 系统通知      │            │                   │   │
│  │  - 文件系统      │            └───────────────────┘   │
│  └─────────────────┘                                    │
└─────────────────────────────────────────────────────────┘

1.3 主进程 vs 渲染进程

特性主进程 (Main Process)渲染进程 (Renderer Process)
运行环境Node.jsChromium
数量唯一多个(每个窗口一个)
职责应用生命周期、窗口管理、系统交互UI 渲染、用户交互
直接访问文件系统、操作系统 API受限(沙箱安全)
通信方式IPC(进程间通信)preload 脚本暴露 API

1.4 Electron 工作流程

1. 启动应用
   └─→ main.js 执行,创建 BrowserWindow

2. 加载页面
   └─→ 渲染进程加载 Vue 应用的 index.html

3. 用户交互
   └─→ 渲染进程 ←→ IPC ←→ 主进程

4. 应用退出
   └─→ 主进程关闭所有窗口,终止应用

2. 技术栈对比与选型

2.1 桌面框架对比

框架开发语言渲染引擎包体积性能生态
ElectronJS/TSChromium~150MB中等⭐⭐⭐⭐⭐
TauriRust+Web系统 WebView~10MB⭐⭐⭐
NW.jsJS/TSChromium~150MB中等⭐⭐⭐
PyQtPythonQt~50MB⭐⭐⭐
FlutterDartSkia~20MB⭐⭐⭐

2.2 前端框架选型

框架特点适用场景学习曲线
Vue 3简洁易用、Composition API中小型应用⭐⭐
React生态丰富、Hooks大型应用⭐⭐⭐
Angular企业级、TypeScript企业应用⭐⭐⭐⭐

2.3 推荐技术组合

组合优势适用项目
Electron + Vue 3快速开发、Vue 生态GameBox、游戏工具
Electron + React组件化强复杂应用
Tauri + Vue 3轻量高性能小工具、效率软件

3. 环境准备

3.1 系统要求

要求说明
操作系统Windows 10+ / macOS 10.15+ / Ubuntu 18.04+
内存建议 8GB+
磁盘预留 10GB+
Node.js18.x LTS 或 20.x LTS
npm/yarn/pnpm最新稳定版

3.2 安装 Node.js

# Windows: 使用 nvm-windows
# 下载: https://github.com/coreybutler/nvm-windows/releases

# Mac/Linux: 使用 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# 安装 Node.js 18 LTS
nvm install 18
nvm use 18
nvm alias default 18

# 验证安装
node --version   # v18.x.x
npm --version    # 9.x.x

3.3 安装 Electron 加速工具(可选)

# Windows 用户推荐安装 electron-builder 加速
npm install -g electron-builder

# 或使用国内镜像
npm config set electron_mirror https://npmmirror.com/mirrors/electron/

3.4 创建项目目录

# 创建项目目录
mkdir gamebox-app && cd gamebox-app

# 初始化 npm
npm init -y

4. 项目初始化

4.1 方式一:使用 electron-vite(推荐)

electron-vite 是专为 Electron + Vite 项目打造的脚手架工具。

# 创建 Electron + Vue 项目
npm create @quick-start/electron gamebox-app

# 交互式选择
? Select a framework: vue
? Select a variant: vue-ts
? Select UI framework: none

# 进入目录
cd gamebox-app
npm install

4.2 方式二:手动创建项目

# 创建目录结构
mkdir -p src/main src/renderer/src src/preload src/shared

# 目录说明
src/
├── main/           # Electron 主进程
│   ├── index.ts    # 主入口
│   └── ipc.ts      # IPC 处理
├── preload/        # 预加载脚本
│   └── index.ts
└── renderer/       # Vue 渲染进程
    ├── index.html
    └── src/
        ├── main.ts
        ├── App.vue
        ├── components/
        ├── views/
        └── stores/

4.3 完整 package.json

{
  "name": "gamebox-app",
  "version": "1.0.0",
  "description": "GameBox - 跨平台游戏盒子",
  "main": "./out/main/index.js",
  "author": "erick",
  "license": "MIT",
  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "preview": "electron-vite preview",
    "postinstall": "electron-builder install-app-deps",
    "build:win": "npm run build && electron-builder --win",
    "build:mac": "npm run build && electron-builder --mac",
    "build:linux": "npm run build && electron-builder --linux"
  },
  "dependencies": {
    "@vueuse/core": "^10.7.0",
    "electron-store": "^8.1.0",
    "pinia": "^2.1.7",
    "vue": "^3.4.0",
    "vue-router": "^4.2.5"
  },
  "devDependencies": {
    "@electron-toolkit/preload": "^3.0.0",
    "@electron-toolkit/utils": "^3.0.0",
    "@vitejs/plugin-vue": "^5.0.0",
    "electron": "^28.0.0",
    "electron-builder": "^24.9.1",
    "electron-vite": "^2.0.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vue-tsc": "^1.8.25"
  },
  "build": {
    "appId": "com.erick.gamebox",
    "productName": "GameBox",
    "directories": {
      "output": "dist"
    },
    "files": [
      "out/**/*"
    ],
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": ["x64"]
        }
      ],
      "icon": "build/icon.ico"
    },
    "mac": {
      "target": ["dmg"],
      "icon": "build/icon.icns"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true
    }
  }
}

4.4 初始化命令

# 安装依赖
npm install

# 如果安装慢,使用镜像
npm install --registry=https://registry.npmmirror.com

5. 核心配置详解

5.1 electron-vite 配置

// electron.vite.config.ts
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin()],
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'src/main/index.ts')
        }
      }
    }
  },
  preload: {
    plugins: [externalizeDepsPlugin()],
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'src/preload/index.ts')
        }
      }
    }
  },
  renderer: {
    root: resolve(__dirname, 'src/renderer'),
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'src/renderer/index.html')
        }
      }
    },
    plugins: [vue()]
  }
})

5.2 TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],
  "references": [
    { "path": "./tsconfig.node.json" },
    { "path": "./tsconfig.web.json" }
  ]
}
// tsconfig.node.json(主进程 + preload)
{
  "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./out",
    "baseUrl": ".",
    "paths": {
      "@main/*": ["src/main/*"],
      "@preload/*": ["src/preload/*"],
      "@shared/*": ["src/shared/*"]
    }
  },
  "include": [
    "src/main/**/*.ts",
    "src/preload/**/*.ts",
    "src/shared/**/*.ts",
    "electron.vite.config.ts"
  ]
}
// tsconfig.web.json(渲染进程)
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./out",
    "baseUrl": ".",
    "paths": {
      "@renderer/*": ["src/renderer/src/*"]
    }
  },
  "include": [
    "src/renderer/src/**/*.ts",
    "src/renderer/src/**/*.d.ts",
    "src/renderer/src/**/*.vue",
    "src/shared/**/*.ts"
  ]
}

5.3 目录别名配置

// src/renderer/src/env.d.ts
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

interface Window {
  electron: {
    ipcRenderer: {
      send: (channel: string, ...args: any[]) => void
      on: (channel: string, func: (...args: any[]) => void) => void
      invoke: (channel: string, ...args: any[]) => Promise<any>
      removeAllListeners: (channel: string) => void
    }
  }
}

6. Vue 3 组件开发

6.1 入口文件

// src/renderer/src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'

import './assets/main.css'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

6.2 App.vue 根组件

<!-- src/renderer/src/App.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { useGameStore } from './stores/game'
import { useAppStore } from './stores/app'

const gameStore = useGameStore()
const appStore = useAppStore()

onMounted(() => {
  // 初始化加载
  gameStore.fetchGames()

  // 监听 Electron 事件
  window.electron.ipcRenderer.on('game-launch', (_, gameId: string) => {
    gameStore.launchGame(gameId)
  })
})
</script>

<template>
  <div class="app-container">
    <!-- 侧边栏 -->
    <aside class="sidebar">
      <div class="logo">
        <img src="@renderer/assets/logo.png" alt="GameBox" />
        <span>GameBox</span>
      </div>

      <nav class="nav-menu">
        <router-link to="/" class="nav-item" active-class="active">
          <span class="icon">🎮</span>
          <span>全部游戏</span>
        </router-link>
        <router-link to="/categories" class="nav-item" active-class="active">
          <span class="icon">📂</span>
          <span>分类</span>
        </router-link>
        <router-link to="/favorites" class="nav-item" active-class="active">
          <span class="icon">⭐</span>
          <span>收藏</span>
        </router-link>
        <router-link to="/settings" class="nav-item" active-class="active">
          <span class="icon">⚙️</span>
          <span>设置</span>
        </router-link>
      </nav>

      <!-- 窗口控制按钮 -->
      <div class="window-controls">
        <button @click="appStore.minimize" title="最小化">─</button>
        <button @click="appStore.toggleMaximize" title="最大化">□</button>
        <button @click="appStore.close" class="close-btn" title="关闭">✕</button>
      </div>
    </aside>

    <!-- 主内容区 -->
    <main class="main-content">
      <router-view v-slot="{ Component }">
        <transition name="fade" mode="out-in">
          <component :is="Component" />
        </transition>
      </router-view>
    </main>
  </div>
</template>

<style scoped>
.app-container {
  display: flex;
  height: 100vh;
  background: #1a1a2e;
  color: #fff;
}

.sidebar {
  width: 240px;
  background: #16213e;
  display: flex;
  flex-direction: column;
  padding: 20px;
  border-right: 1px solid rgba(255,255,255,0.1);
}

.logo {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 20px;
  border-bottom: 1px solid rgba(255,255,255,0.1);
  margin-bottom: 20px;
}

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

.logo span {
  font-size: 20px;
  font-weight: bold;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.nav-menu {
  flex: 1;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  margin-bottom: 8px;
  border-radius: 8px;
  color: #a0a0a0;
  text-decoration: none;
  transition: all 0.3s;
}

.nav-item:hover {
  background: rgba(255,255,255,0.1);
  color: #fff;
}

.nav-item.active {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
}

.window-controls {
  display: flex;
  gap: 8px;
}

.window-controls button {
  flex: 1;
  padding: 8px;
  border: none;
  border-radius: 4px;
  background: rgba(255,255,255,0.1);
  color: #fff;
  cursor: pointer;
  transition: background 0.3s;
}

.window-controls button:hover {
  background: rgba(255,255,255,0.2);
}

.window-controls .close-btn:hover {
  background: #e74c3c;
}

.main-content {
  flex: 1;
  overflow-y: auto;
  padding: 24px;
}
</style>

6.3 路由配置

// src/renderer/src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@renderer/views/HomeView.vue')
  },
  {
    path: '/categories',
    name: 'categories',
    component: () => import('@renderer/views/CategoriesView.vue')
  },
  {
    path: '/categories/:id',
    name: 'category',
    component: () => import('@renderer/views/CategoryView.vue')
  },
  {
    path: '/favorites',
    name: 'favorites',
    component: () => import('@renderer/views/FavoritesView.vue')
  },
  {
    path: '/settings',
    name: 'settings',
    component: () => import('@renderer/views/SettingsView.vue')
  },
  {
    path: '/game/:id',
    name: 'game-detail',
    component: () => import('@renderer/views/GameDetailView.vue')
  }
]

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

export default router

6.4 Pinia 状态管理

// src/renderer/src/stores/game.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Game } from '@shared/types'

export const useGameStore = defineStore('game', () => {
  // 状态
  const games = ref<Game[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  const favorites = ref<string[]>([])

  // 计算属性
  const totalGames = computed(() => games.value.length)
  const featuredGames = computed(() => games.value.filter(g => g.featured))
  const favoriteGames = computed(() => 
    games.value.filter(g => favorites.value.includes(g.id))
  )

  // 按分类筛选
  const getByCategory = (categoryId: string) => 
    games.value.filter(g => g.categoryId === categoryId)

  // 获取单个游戏
  const getById = (id: string) => games.value.find(g => g.id === id)

  // Actions
  async function fetchGames() {
    loading.value = true
    error.value = null

    try {
      // 从主进程获取数据(通过 IPC)
      const data = await window.electron.ipcRenderer.invoke('get-games')
      games.value = data
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }

  async function launchGame(gameId: string) {
    const game = getById(gameId)
    if (!game) return

    try {
      // 调用主进程启动游戏
      await window.electron.ipcRenderer.invoke('launch-game', game.path)
    } catch (e) {
      error.value = (e as Error).message
    }
  }

  function toggleFavorite(gameId: string) {
    const index = favorites.value.indexOf(gameId)
    if (index > -1) {
      favorites.value.splice(index, 1)
    } else {
      favorites.value.push(gameId)
    }
    // 持久化到本地存储
    saveFavorites()
  }

  function saveFavorites() {
    localStorage.setItem('gamebox-favorites', JSON.stringify(favorites.value))
  }

  function loadFavorites() {
    const stored = localStorage.getItem('gamebox-favorites')
    if (stored) {
      favorites.value = JSON.parse(stored)
    }
  }

  return {
    games,
    loading,
    error,
    favorites,
    totalGames,
    featuredGames,
    favoriteGames,
    getByCategory,
    getById,
    fetchGames,
    launchGame,
    toggleFavorite,
    loadFavorites
  }
})

6.5 游戏卡片组件

<!-- src/renderer/src/components/GameCard.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useGameStore } from '@renderer/stores/game'
import type { Game } from '@shared/types'

const props = defineProps<{
  game: Game
}>()

const emit = defineEmits<{
  (e: 'launch', game: Game): void
  (e: 'favorite', gameId: string): void
}>()

const gameStore = useGameStore()

const isFavorite = computed(() => 
  gameStore.favorites.includes(props.game.id)
)

function handleLaunch() {
  emit('launch', props.game)
}

function handleFavorite() {
  emit('favorite', props.game.id)
}
</script>

<template>
  <div class="game-card" @click="handleLaunch">
    <!-- 游戏封面 -->
    <div class="cover-wrapper">
      <img :src="game.cover" :alt="game.name" class="cover" />
      <div class="overlay">
        <button class="play-btn">▶</button>
      </div>
      <span v-if="game.featured" class="badge featured">热门</span>
      <span v-if="game.new" class="badge new">新上架</span>
    </div>

    <!-- 游戏信息 -->
    <div class="info">
      <h3 class="title">{{ game.name }}</h3>
      <p class="category">{{ game.category }}</p>
      <div class="meta">
        <span class="rating">⭐ {{ game.rating }}</span>
        <button 
          class="favorite-btn" 
          :class="{ active: isFavorite }"
          @click.stop="handleFavorite"
        >
          {{ isFavorite ? '❤️' : '🤍' }}
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.game-card {
  background: #1e1e2f;
  border-radius: 12px;
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.3s, box-shadow 0.3s;
}

.game-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0,0,0,0.3);
}

.cover-wrapper {
  position: relative;
  aspect-ratio: 16/9;
}

.cover {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.overlay {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.3s;
}

.game-card:hover .overlay {
  opacity: 1;
}

.play-btn {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: linear-gradient(135deg, #667eea, #764ba2);
  border: none;
  color: white;
  font-size: 24px;
  cursor: pointer;
  transition: transform 0.3s;
}

.play-btn:hover {
  transform: scale(1.1);
}

.badge {
  position: absolute;
  top: 8px;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: bold;
}

.badge.featured {
  left: 8px;
  background: #f39c12;
  color: #fff;
}

.badge.new {
  right: 8px;
  background: #27ae60;
  color: #fff;
}

.info {
  padding: 12px;
}

.title {
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.category {
  font-size: 12px;
  color: #888;
  margin-bottom: 8px;
}

.meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.rating {
  font-size: 14px;
  color: #f1c40f;
}

.favorite-btn {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  transition: transform 0.2s;
}

.favorite-btn:hover {
  transform: scale(1.2);
}
</style>

7. Electron 主进程

7.1 主进程入口

// src/main/index.ts
import { app, shell, BrowserWindow, ipcMain, dialog } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { setupIpcHandlers } from './ipc'

let mainWindow: BrowserWindow | null = null

function createWindow(): void {
  // 创建浏览器窗口
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 900,
    minHeight: 600,
    show: false,                    // 等待ready-to-show后再显示
    autoHideMenuBar: false,         // 显示菜单栏
    frame: true,                    // 使用系统窗口边框
    titleBarStyle: 'default',       // macOS 原生标题栏
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false,
      contextIsolation: true,        // 启用上下文隔离
      nodeIntegration: false,       // 禁用 Node.js
      webSecurity: true
    }
  })

  mainWindow.on('ready-to-show', () => {
    mainWindow?.show()
    console.log('✅ 主窗口已显示')
  })

  // 处理外部链接
  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  // 开发模式下加载本地 URL
  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  } else {
    // 生产模式下加载打包后的文件
    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  }

  // 打开 DevTools(仅开发模式)
  if (is.dev) {
    mainWindow.webContents.openDevTools()
  }
}

// 应用准备就绪
app.whenReady().then(() => {
  // 设置应用 ID(Windows)
  electronApp.setAppUserModelId('com.erick.gamebox')

  // 监听窗口创建(macOS)
  app.on('browser-window-created', (_, window) => {
    optimizer.watchWindowShortcuts(window)
  })

  // 设置 IPC 处理程序
  setupIpcHandlers()

  // 创建主窗口
  createWindow()

  // macOS 激活处理
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

// 窗口全部关闭(Windows/Linux)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// 退出前清理
app.on('before-quit', () => {
  console.log('👋 应用即将退出')
})

7.2 IPC 处理程序

// src/main/ipc.ts
import { ipcMain, shell, dialog, app } from 'electron'
import { exec } from 'child_process'
import { promisify } from 'util'
import * as fs from 'fs'
import * as path from 'path'
import Store from 'electron-store'

const execAsync = promisify(exec)

// 初始化配置存储
const store = new Store({
  name: 'gamebox-config',
  defaults: {
    games: [],
    settings: {
      theme: 'dark',
      language: 'zh-CN',
      autoLaunch: false
    }
  }
})

// 游戏数据
interface Game {
  id: string
  name: string
  path: string
  cover: string
  category: string
  categoryId: string
  rating: number
  featured: boolean
  new: boolean
}

// 模拟游戏数据(后续可替换为真实后端)
const mockGames: Game[] = [
  {
    id: '1',
    name: '我的世界',
    path: 'C:\\Games\\Minecraft\\Minecraft.exe',
    cover: 'https://example.com/minecraft.jpg',
    category: '沙盒建造',
    categoryId: 'sandbox',
    rating: 4.9,
    featured: true,
    new: false
  },
  {
    id: '2',
    name: '英雄联盟',
    path: 'C:\\Games\\League\\LeagueClient.exe',
    cover: 'https://example.com/lol.jpg',
    category: 'MOBA',
    categoryId: 'moba',
    rating: 4.7,
    featured: true,
    new: false
  },
  {
    id: '3',
    name: '原神',
    path: 'C:\\Games\\Genshin\\Genshin Impact.exe',
    cover: 'https://example.com/genshin.jpg',
    category: '角色扮演',
    categoryId: 'rpg',
    rating: 4.8,
    featured: false,
    new: true
  }
]

export function setupIpcHandlers(): void {
  console.log('📡 设置 IPC 处理程序')

  // ========== 游戏相关 ==========

  // 获取游戏列表
  ipcMain.handle('get-games', async () => {
    console.log('📋 获取游戏列表')
    return mockGames
  })

  // 获取单个游戏
  ipcMain.handle('get-game', async (_, id: string) => {
    return mockGames.find(g => g.id === id) || null
  })

  // 启动游戏
  ipcMain.handle('launch-game', async (_, gamePath: string) => {
    console.log(`🎮 启动游戏: ${gamePath}`)

    try {
      // 检查文件是否存在
      if (!fs.existsSync(gamePath)) {
        throw new Error(`游戏文件不存在: ${gamePath}`)
      }

      // 使用默认程序打开(相当于双击)
      await shell.openPath(gamePath)

      return { success: true }
    } catch (error) {
      console.error('启动游戏失败:', error)
      return { success: false, error: (error as Error).message }
    }
  })

  // 打开游戏所在目录
  ipcMain.handle('open-game-folder', async (_, gamePath: string) => {
    const folder = path.dirname(gamePath)
    await shell.openPath(folder)
  })

  // ========== 文件系统相关 ==========

  // 选择文件
  ipcMain.handle('select-file', async (_, options: {
    title?: string
    filters?: { name: string; extensions: string[] }[]
  }) => {
    const result = await dialog.showOpenDialog({
      title: options.title || '选择文件',
      properties: ['openFile'],
      filters: options.filters || [
        { name: '可执行文件', extensions: ['exe', 'app', 'sh'] },
        { name: '所有文件', extensions: ['*'] }
      ]
    })

    return result.canceled ? null : result.filePaths[0]
  })

  // 选择目录
  ipcMain.handle('select-directory', async () => {
    const result = await dialog.showOpenDialog({
      title: '选择游戏目录',
      properties: ['openDirectory']
    })

    return result.canceled ? null : result.filePaths[0]
  })

  // 扫描目录下的游戏
  ipcMain.handle('scan-games', async (_, folderPath: string) => {
    console.log(`🔍 扫描目录: ${folderPath}`)

    const games: Game[] = []
    const exeExtensions = ['.exe', '.app', '.sh']

    try {
      const files = fs.readdirSync(folderPath)

      for (const file of files) {
        const ext = path.extname(file).toLowerCase()
        if (exeExtensions.includes(ext)) {
          const fullPath = path.join(folderPath, file)
          games.push({
            id: Buffer.from(fullPath).toString('base64'),
            name: path.basename(file, ext),
            path: fullPath,
            cover: '',
            category: '未分类',
            categoryId: 'uncategorized',
            rating: 0,
            featured: false,
            new: false
          })
        }
      }

      return games
    } catch (error) {
      console.error('扫描失败:', error)
      return []
    }
  })

  // ========== 系统相关 ==========

  // 获取应用版本
  ipcMain.handle('get-app-version', () => {
    return app.getVersion()
  })

  // 获取系统信息
  ipcMain.handle('get-system-info', () => {
    return {
      platform: process.platform,
      arch: process.arch,
      version: process.getSystemVersion(),
      electron: process.versions.electron,
      node: process.versions.node,
      chrome: process.versions.chrome
    }
  })

  // 打开外部链接
  ipcMain.handle('open-external', async (_, url: string) => {
    await shell.openExternal(url)
  })

  // ========== 配置相关 ==========

  // 获取配置
  ipcMain.handle('get-config', (_, key: string) => {
    return store.get(key)
  })

  // 设置配置
  ipcMain.handle('set-config', (_, key: string, value: any) => {
    store.set(key, value)
    return true
  })

  // 打开开发者工具
  ipcMain.on('toggle-devtools', (event) => {
    const webContents = event.sender
    if (webContents.isDevToolsOpened()) {
      webContents.closeDevTools()
    } else {
      webContents.openDevTools()
    }
  })
}

8. IPC 通信机制

8.1 IPC 通信模式

┌─────────────────────────────────────────────────────────────┐
│                    IPC 通信架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   主进程 (Main)              预加载 (Preload)                 │
│   ┌───────────┐              ┌─────────────┐                │
│   │ ipcMain   │◄── invoke ───│ 暴露的 API  │                │
│   │ .handle() │              │   window.   │                │
│   └───────────┘              │  electron   │                │
│        ▲                     └──────┬──────┘                │
│        │ send()                    │                        │
│        │ on()                      │                        │
│        │                           ▼                        │
│   ┌───────────┐              ┌─────────────┐                │
│   │ ipcMain   │              │  渲染进程   │                │
│   │ .on()     │◄── send ─────│  Vue App    │                │
│   └───────────┘              └─────────────┘                │
│                                                             │
│   通信方式:                                                  │
│   ┌─────────┬──────────────┬─────────────────────────────┐   │
│   │ invoke  │ 异步双向      │ await invoke('channel')   │   │
│   ├─────────┼──────────────┼─────────────────────────────┤   │
│   │ send    │ 单向(主进程) │ ipcRenderer.send()         │   │
│   ├─────────┼──────────────┼─────────────────────────────┤   │
│   │ on      │ 监听         │ ipcRenderer.on()           │   │
│   └─────────┴──────────────┴─────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

8.2 Preload 脚本

// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'

// 暴露给渲染进程的 API
contextBridge.exposeInMainWorld('electron', {
  // ========== 游戏相关 ==========
  getGames: () => ipcRenderer.invoke('get-games'),
  getGame: (id: string) => ipcRenderer.invoke('get-game', id),
  launchGame: (path: string) => ipcRenderer.invoke('launch-game', path),
  openGameFolder: (path: string) => ipcRenderer.invoke('open-game-folder', path),

  // ========== 文件系统 ==========
  selectFile: (options?: {
    title?: string
    filters?: { name: string; extensions: string[] }[]
  }) => ipcRenderer.invoke('select-file', options || {}),
  selectDirectory: () => ipcRenderer.invoke('select-directory'),
  scanGames: (folder: string) => ipcRenderer.invoke('scan-games', folder),

  // ========== 系统相关 ==========
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
  openExternal: (url: string) => ipcRenderer.invoke('open-external', url),

  // ========== 配置相关 ==========
  getConfig: (key: string) => ipcRenderer.invoke('get-config', key),
  setConfig: (key: string, value: any) => ipcRenderer.invoke('set-config', key, value),

  // ========== 事件监听 ==========
  on: (channel: string, callback: (...args: any[]) => void) => {
    // 安全检查:只允许白名单内的通道
    const validChannels = ['game-launch', 'update-available', 'notification']
    if (validChannels.includes(channel)) {
      ipcRenderer.on(channel, (_, ...args) => callback(...args))
    }
  },

  // 移除监听
  removeAllListeners: (channel: string) => {
    ipcRenderer.removeAllListeners(channel)
  }
})

// 类型声明(供 TypeScript 使用)
export interface ElectronAPI {
  getGames: () => Promise<Game[]>
  getGame: (id: string) => Promise<Game | null>
  launchGame: (path: string) => Promise<{ success: boolean; error?: string }>
  openGameFolder: (path: string) => Promise<void>
  selectFile: (options?: any) => Promise<string | null>
  selectDirectory: () => Promise<string | null>
  scanGames: (folder: string) => Promise<Game[]>
  getAppVersion: () => Promise<string>
  getSystemInfo: () => Promise<any>
  openExternal: (url: string) => Promise<void>
  getConfig: (key: string) => Promise<any>
  setConfig: (key: string, value: any) => Promise<boolean>
  on: (channel: string, callback: (...args: any[]) => void) => void
  removeAllListeners: (channel: string) => void
}

interface Game {
  id: string
  name: string
  path: string
  cover: string
  category: string
  categoryId: string
  rating: number
  featured: boolean
  new: boolean
}

8.3 渲染进程调用示例

<!-- 使用示例 -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const version = ref('')
const games = ref<any[]>([])

onMounted(async () => {
  // 方式1:invoke 调用(推荐)
  version.value = await window.electron.getAppVersion()
  games.value = await window.electron.getGames()

  // 方式2:监听事件
  window.electron.on('game-launch', (gameId: string) => {
    console.log('收到启动游戏事件:', gameId)
  })
})

onUnmounted(() => {
  // 清理监听
  window.electron.removeAllListeners('game-launch')
})

async function handleLaunchGame(path: string) {
  const result = await window.electron.launchGame(path)
  if (result.success) {
    console.log('游戏已启动')
  } else {
    console.error('启动失败:', result.error)
  }
}

async function handleSelectGame() {
  const filePath = await window.electron.selectFile({
    title: '选择游戏可执行文件',
    filters: [
      { name: '可执行文件', extensions: ['exe', 'app'] }
    ]
  })

  if (filePath) {
    console.log('选择的文件:', filePath)
  }
}
</script>

9. 系统集成

9.1 应用菜单

// src/main/menu.ts
import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron'

export function createApplicationMenu(mainWindow: BrowserWindow): Menu {
  const isMac = process.platform === 'darwin'

  const template: MenuItemConstructorOptions[] = [
    // macOS 特有应用菜单
    ...(isMac ? [{
      label: app.name,
      submenu: [
        { role: 'about' as const },
        { type: 'separator' as const },
        {
          label: '设置...',
          accelerator: 'Cmd+,',
          click: () => mainWindow.webContents.send('open-settings')
        },
        { type: 'separator' as const },
        { role: 'services' as const },
        { type: 'separator' as const },
        { role: 'hide' as const },
        { role: 'hideOthers' as const },
        { role: 'unhide' as const },
        { type: 'separator' as const },
        { role: 'quit' as const }
      ]
    }] : []),

    // 文件菜单
    {
      label: '文件',
      submenu: [
        {
          label: '添加游戏...',
          accelerator: 'CmdOrCtrl+O',
          click: () => mainWindow.webContents.send('add-game')
        },
        {
          label: '扫描游戏目录...',
          accelerator: 'CmdOrCtrl+Shift+O',
          click: () => mainWindow.webContents.send('scan-games')
        },
        { type: 'separator' },
        isMac ? { role: 'close' } : { role: 'quit' }
      ]
    },

    // 编辑菜单
    {
      label: '编辑',
      submenu: [
        { role: 'undo' },
        { role: 'redo' },
        { type: 'separator' },
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
        ...(isMac ? [
          { role: 'pasteAndMatchStyle' as const },
          { role: 'delete' as const },
          { role: 'selectAll' as const }
        ] : [
          { role: 'delete' as const },
          { type: 'separator' as const },
          { role: 'selectAll' as const }
        ])
      ]
    },

    // 视图菜单
    {
      label: '视图',
      submenu: [
        { role: 'reload' },
        { role: 'forceReload' },
        { role: 'toggleDevTools' },
        { type: 'separator' },
        { role: 'resetZoom' },
        { role: 'zoomIn' },
        { role: 'zoomOut' },
        { type: 'separator' },
        { role: 'togglefullscreen' }
      ]
    },

    // 窗口菜单
    {
      label: '窗口',
      submenu: [
        { role: 'minimize' },
        { role: 'zoom' },
        ...(isMac ? [
          { type: 'separator' as const },
          { role: 'front' as const },
          { type: 'separator' as const },
          { role: 'window' as const }
        ] : [
          { role: 'close' as const }
        ])
      ]
    },

    // 帮助菜单
    {
      label: '帮助',
      submenu: [
        {
          label: '关于 GameBox',
          click: () => mainWindow.webContents.send('show-about')
        },
        { type: 'separator' },
        {
          label: '打开日志目录',
          click: async () => {
            const { shell } = require('electron')
            await shell.openPath(app.getPath('logs'))
          }
        }
      ]
    }
  ]

  return Menu.buildFromTemplate(template)
}

9.2 系统托盘

// src/main/tray.ts
import { app, Tray, Menu, nativeImage, BrowserWindow } from 'electron'
import { join } from 'path'

let tray: Tray | null = null

export function createTray(mainWindow: BrowserWindow): Tray {
  // 创建托盘图标(需要准备图标文件)
  const iconPath = join(__dirname, '../../resources/icon.png')
  const icon = nativeImage.createFromPath(iconPath)

  // 如果图标不存在,使用空白图标
  if (icon.isEmpty()) {
    // 创建一个 16x16 的空白图标作为占位符
    const placeholder = nativeImage.createEmpty()
    tray = new Tray(placeholder)
  } else {
    tray = new Tray(icon)
  }

  // 设置托盘提示文本
  tray.setToolTip('GameBox - 游戏盒子')

  // 创建右键菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '打开 GameBox',
      click: () => {
        mainWindow.show()
        mainWindow.focus()
      }
    },
    { type: 'separator' },
    {
      label: '最近游戏',
      submenu: [
        { label: '我的世界', click: () => launchGame('minecraft') },
        { label: '英雄联盟', click: () => launchGame('lol') },
        { label: '原神', click: () => launchGame('genshin') }
      ]
    },
    { type: 'separator' },
    {
      label: '设置',
      click: () => {
        mainWindow.show()
        mainWindow.webContents.send('open-settings')
      }
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => {
        app.quit()
      }
    }
  ])

  tray.setContextMenu(contextMenu)

  // 单击托盘图标显示窗口
  tray.on('click', () => {
    if (mainWindow.isVisible()) {
      mainWindow.hide()
    } else {
      mainWindow.show()
      mainWindow.focus()
    }
  })

  return tray
}

function launchGame(gameId: string): void {
  // 通过 IPC 通知主进程启动游戏
  console.log(`从托盘启动游戏: ${gameId}`)
}

9.3 窗口管理

// src/main/window.ts
import { BrowserWindow, screen } from 'electron'

// 保存窗口状态
interface WindowState {
  x: number
  y: number
  width: number
  height: number
  isMaximized: boolean
}

export function getWindowState(): WindowState {
  const { width, height } = screen.getPrimaryDisplay().workAreaSize

  return {
    x: Math.floor((width - 1200) / 2),
    y: Math.floor((height - 800) / 2),
    width: 1200,
    height: 800,
    isMaximized: false
  }
}

export function setupWindowControls(mainWindow: BrowserWindow): void {
  // 最小化
  function minimize(): void {
    mainWindow.minimize()
  }

  // 最大化/还原
  function toggleMaximize(): void {
    if (mainWindow.isMaximized()) {
      mainWindow.unmaximize()
    } else {
      mainWindow.maximize()
    }
  }

  // 关闭
  function close(): void {
    mainWindow.close()
  }

  // 导出给渲染进程使用
  mainWindow.webContents.on('ipc-message', (_, channel, ...args) => {
    if (channel === 'window-minimize') minimize()
    if (channel === 'window-maximize') toggleMaximize()
    if (channel === 'window-close') close()
  })
}

10. 实战案例:GameBox 游戏盒子

10.1 项目结构

gamebox-app/
├── electron.vite.config.ts
├── package.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.web.json
├── resources/                  # 应用资源
│   └── icon.png               # 应用图标
├── build/                      # 构建资源
│   └── icon.ico
├── src/
│   ├── main/                  # Electron 主进程
│   │   ├── index.ts          # 主入口
│   │   ├── ipc.ts           # IPC 处理
│   │   ├── menu.ts          # 应用菜单
│   │   ├── tray.ts          # 系统托盘
│   │   └── window.ts        # 窗口管理
│   ├── preload/               # 预加载脚本
│   │   └── index.ts
│   ├── renderer/             # Vue 渲染进程
│   │   ├── index.html
│   │   └── src/
│   │       ├── main.ts
│   │       ├── App.vue
│   │       ├── router/
│   │       ├── stores/
│   │       ├── views/
│   │       ├── components/
│   │       └── assets/
│   └── shared/               # 共享类型
│       └── types.ts
└── dist/                     # 构建输出

10.2 共享类型定义

// src/shared/types.ts
export interface Game {
  id: string
  name: string
  path: string
  cover: string
  category: string
  categoryId: string
  rating: number
  featured: boolean
  new: boolean
  description?: string
  size?: string
  playTime?: number
}

export interface Category {
  id: string
  name: string
  icon: string
  count: number
}

export interface GameSettings {
  theme: 'light' | 'dark' | 'system'
  language: 'zh-CN' | 'en-US'
  autoLaunch: boolean
  minimizeToTray: boolean
  gameScanPaths: string[]
}

10.3 首页视图

<!-- src/renderer/src/views/HomeView.vue -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useGameStore } from '@renderer/stores/game'
import GameCard from '@renderer/components/GameCard.vue'

const gameStore = useGameStore()

const searchQuery = ref('')
const selectedCategory = ref('')

const categories = computed(() => {
  const cats = new Map<string, { id: string; name: string; count: number }>()
  gameStore.games.forEach(g => {
    const existing = cats.get(g.categoryId)
    cats.set(g.categoryId, {
      id: g.categoryId,
      name: g.category,
      count: (existing?.count || 0) + 1
    })
  })
  return Array.from(cats.values())
})

const filteredGames = computed(() => {
  return gameStore.games.filter(game => {
    const matchSearch = !searchQuery.value || 
      game.name.toLowerCase().includes(searchQuery.value.toLowerCase())
    const matchCategory = !selectedCategory.value || 
      game.categoryId === selectedCategory.value
    return matchSearch && matchCategory
  })
})

function handleLaunch(game: any) {
  gameStore.launchGame(game.id)
}

function handleFavorite(gameId: string) {
  gameStore.toggleFavorite(gameId)
}
</script>

<template>
  <div class="home-view">
    <!-- 搜索栏 -->
    <div class="search-bar">
      <input 
        v-model="searchQuery"
        type="text" 
        placeholder="搜索游戏..."
        class="search-input"
      />
    </div>

    <!-- 分类标签 -->
    <div class="category-tabs">
      <button 
        class="tab"
        :class="{ active: selectedCategory === '' }"
        @click="selectedCategory = ''"
      >
        全部
      </button>
      <button 
        v-for="cat in categories"
        :key="cat.id"
        class="tab"
        :class="{ active: selectedCategory === cat.id }"
        @click="selectedCategory = cat.id"
      >
        {{ cat.name }} ({{ cat.count }})
      </button>
    </div>

    <!-- 加载状态 -->
    <div v-if="gameStore.loading" class="loading">
      <div class="spinner"></div>
      <span>加载中...</span>
    </div>

    <!-- 热门游戏 -->
    <section v-if="!selectedCategory && gameStore.featuredGames.length" class="section">
      <h2 class="section-title">🔥 热门推荐</h2>
      <div class="game-grid featured">
        <GameCard 
          v-for="game in gameStore.featuredGames"
          :key="game.id"
          :game="game"
          @launch="handleLaunch"
          @favorite="handleFavorite"
        />
      </div>
    </section>

    <!-- 全部游戏 -->
    <section class="section">
      <h2 class="section-title">
        {{ selectedCategory ? categories.find(c => c.id === selectedCategory)?.name : '全部游戏' }}
        ({{ filteredGames.length }})
      </h2>

      <div v-if="filteredGames.length" class="game-grid">
        <GameCard 
          v-for="game in filteredGames"
          :key="game.id"
          :game="game"
          @launch="handleLaunch"
          @favorite="handleFavorite"
        />
      </div>

      <div v-else class="empty-state">
        <p>😢 没有找到符合条件的游戏</p>
      </div>
    </section>
  </div>
</template>

<style scoped>
.home-view {
  max-width: 1400px;
  margin: 0 auto;
}

.search-bar {
  margin-bottom: 24px;
}

.search-input {
  width: 100%;
  padding: 12px 20px;
  border: none;
  border-radius: 12px;
  background: #2a2a3e;
  color: #fff;
  font-size: 16px;
  outline: none;
  transition: box-shadow 0.3s;
}

.search-input:focus {
  box-shadow: 0 0 0 2px #667eea;
}

.category-tabs {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
  overflow-x: auto;
  padding-bottom: 8px;
}

.tab {
  padding: 8px 16px;
  border: none;
  border-radius: 20px;
  background: #2a2a3e;
  color: #a0a0a0;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.3s;
}

.tab:hover {
  background: #3a3a4e;
  color: #fff;
}

.tab.active {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
}

.section {
  margin-bottom: 32px;
}

.section-title {
  font-size: 20px;
  margin-bottom: 16px;
  color: #fff;
}

.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 20px;
}

.game-grid.featured {
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 40px;
  color: #888;
}

.spinner {
  width: 24px;
  height: 24px;
  border: 2px solid #333;
  border-top-color: #667eea;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.empty-state {
  text-align: center;
  padding: 60px;
  color: #666;
}
</style>

10.4 设置页面

<!-- src/renderer/src/views/SettingsView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAppStore } from '@renderer/stores/app'

const appStore = useAppStore()

const settings = ref({
  theme: 'dark',
  language: 'zh-CN',
  autoLaunch: false,
  minimizeToTray: true,
  gamePaths: [] as string[]
})

const appVersion = ref('')
const systemInfo = ref<any>({})

// 加载设置
onMounted(async () => {
  settings.value.theme = await window.electron.getConfig('settings.theme') || 'dark'
  settings.value.language = await window.electron.getConfig('settings.language') || 'zh-CN'
  settings.value.autoLaunch = await window.electron.getConfig('settings.autoLaunch') || false
  settings.value.minimizeToTray = await window.electron.getConfig('settings.minimizeToTray') || true
  settings.value.gamePaths = await window.electron.getConfig('settings.gamePaths') || []

  appVersion.value = await window.electron.getAppVersion()
  systemInfo.value = await window.electron.getSystemInfo()
})

// 保存设置
async function saveSetting(key: string, value: any) {
  await window.electron.setConfig(`settings.${key}`, value)
}

// 添加游戏目录
async function addGamePath() {
  const path = await window.electron.selectDirectory()
  if (path && !settings.value.gamePaths.includes(path)) {
    settings.value.gamePaths.push(path)
    await saveSetting('gamePaths', settings.value.gamePaths)
  }
}

// 移除游戏目录
async function removeGamePath(path: string) {
  settings.value.gamePaths = settings.value.gamePaths.filter(p => p !== path)
  await saveSetting('gamePaths', settings.value.gamePaths)
}
</script>

<template>
  <div class="settings-view">
    <h1 class="page-title">⚙️ 设置</h1>

    <!-- 主题设置 -->
    <section class="setting-section">
      <h2 class="section-title">外观</h2>
      <div class="setting-item">
        <div class="setting-info">
          <span class="setting-label">主题</span>
          <span class="setting-desc">选择应用的外观风格</span>
        </div>
        <select 
          v-model="settings.theme"
          @change="saveSetting('theme', settings.theme)"
          class="setting-select"
        >
          <option value="dark">深色</option>
          <option value="light">浅色</option>
          <option value="system">跟随系统</option>
        </select>
      </div>
    </section>

    <!-- 语言设置 -->
    <section class="setting-section">
      <h2 class="section-title">语言</h2>
      <div class="setting-item">
        <div class="setting-info">
          <span class="setting-label">界面语言</span>
          <span class="setting-desc">选择应用显示的语言</span>
        </div>
        <select 
          v-model="settings.language"
          @change="saveSetting('language', settings.language)"
          class="setting-select"
        >
          <option value="zh-CN">简体中文</option>
          <option value="en-US">English</option>
        </select>
      </div>
    </section>

    <!-- 行为设置 -->
    <section class="setting-section">
      <h2 class="section-title">行为</h2>
      <div class="setting-item">
        <div class="setting-info">
          <span class="setting-label">开机自启</span>
          <span class="setting-desc">登录系统时自动启动应用</span>
        </div>
        <label class="toggle">
          <input 
            type="checkbox" 
            v-model="settings.autoLaunch"
            @change="saveSetting('autoLaunch', settings.autoLaunch)"
          />
          <span class="toggle-slider"></span>
        </label>
      </div>

      <div class="setting-item">
        <div class="setting-info">
          <span class="setting-label">最小化到托盘</span>
          <span class="setting-desc">关闭按钮最小化到系统托盘</span>
        </div>
        <label class="toggle">
          <input 
            type="checkbox" 
            v-model="settings.minimizeToTray"
            @change="saveSetting('minimizeToTray', settings.minimizeToTray)"
          />
          <span class="toggle-slider"></span>
        </label>
      </div>
    </section>

    <!-- 游戏目录 -->
    <section class="setting-section">
      <h2 class="section-title">游戏目录</h2>
      <div class="setting-item">
        <div class="setting-info">
          <span class="setting-label">扫描目录</span>
          <span class="setting-desc">自动扫描以下目录查找游戏</span>
        </div>
        <button @click="addGamePath" class="btn-primary">添加目录</button>
      </div>

      <div class="path-list">
        <div v-for="path in settings.gamePaths" :key="path" class="path-item">
          <span class="path-text">{{ path }}</span>
          <button @click="removeGamePath(path)" class="btn-icon">✕</button>
        </div>
        <p v-if="!settings.gamePaths.length" class="empty-text">
          暂未添加任何游戏目录
        </p>
      </div>
    </section>

    <!-- 关于 -->
    <section class="setting-section">
      <h2 class="section-title">关于</h2>
      <div class="about-info">
        <p><strong>版本:</strong>{{ appVersion }}</p>
        <p><strong>平台:</strong>{{ systemInfo.platform }}</p>
        <p><strong>Electron:</strong>{{ systemInfo.electron }}</p>
        <p><strong>Node.js:</strong>{{ systemInfo.node }}</p>
        <p><strong>Chrome:</strong>{{ systemInfo.chrome }}</p>
      </div>
    </section>
  </div>
</template>

<style scoped>
.settings-view {
  max-width: 800px;
}

.page-title {
  font-size: 28px;
  margin-bottom: 32px;
}

.setting-section {
  background: #1e1e2f;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 20px;
}

.section-title {
  font-size: 16px;
  color: #888;
  margin-bottom: 16px;
  padding-bottom: 8px;
  border-bottom: 1px solid #333;
}

.setting-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 0;
}

.setting-info {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.setting-label {
  font-size: 16px;
  font-weight: 500;
}

.setting-desc {
  font-size: 12px;
  color: #888;
}

.setting-select {
  padding: 8px 16px;
  border: 1px solid #333;
  border-radius: 8px;
  background: #2a2a3e;
  color: #fff;
  cursor: pointer;
}

.toggle {
  position: relative;
  width: 48px;
  height: 24px;
  cursor: pointer;
}

.toggle input {
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  inset: 0;
  background: #333;
  border-radius: 24px;
  transition: background 0.3s;
}

.toggle-slider::before {
  content: '';
  position: absolute;
  width: 20px;
  height: 20px;
  left: 2px;
  bottom: 2px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.3s;
}

.toggle input:checked + .toggle-slider {
  background: #667eea;
}

.toggle input:checked + .toggle-slider::before {
  transform: translateX(24px);
}

.path-list {
  margin-top: 12px;
}

.path-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  background: #2a2a3e;
  border-radius: 8px;
  margin-bottom: 8px;
}

.path-text {
  font-size: 14px;
  color: #a0a0a0;
  word-break: break-all;
}

.btn-primary {
  padding: 8px 16px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border: none;
  border-radius: 8px;
  color: #fff;
  cursor: pointer;
  transition: opacity 0.3s;
}

.btn-primary:hover {
  opacity: 0.9;
}

.btn-icon {
  padding: 4px 8px;
  background: none;
  border: none;
  color: #888;
  cursor: pointer;
}

.btn-icon:hover {
  color: #e74c3c;
}

.empty-text {
  color: #666;
  font-size: 14px;
  text-align: center;
  padding: 20px;
}

.about-info {
  font-size: 14px;
  line-height: 2;
  color: #a0a0a0;
}
</style>

11. 打包与发布

11.1 安装图标资源

# 创建构建资源目录
mkdir -p build resources

# 准备图标
# Windows: 需要 256x256 的 .ico 文件
# macOS: 需要 512x512 和 1024x1024 的 .icns 文件
# Linux: 需要 256x256 的 .png 文件

11.2 配置打包

// package.json 添加构建配置
{
  "build": {
    "appId": "com.erick.gamebox",
    "productName": "GameBox",
    "directories": {
      "output": "dist",
      "buildResources": "build"
    },
    "files": [
      "out/**/*"
    ],
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": ["x64"]
        },
        {
          "target": "portable",
          "arch": ["x64"]
        }
      ],
      "icon": "build/icon.ico",
      "artifactName": "${productName}-${version}-${arch}.${ext}"
    },
    "mac": {
      "target": ["dmg", "zip"],
      "icon": "build/icon.icns",
      "category": "public.app-category.games"
    },
    "linux": {
      "target": ["AppImage", "deb"],
      "icon": "resources/icon.png",
      "category": "Game"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "GameBox"
    }
  }
}

11.3 构建命令

# 安装依赖
npm install

# 开发模式运行
npm run dev

# 构建(不打包)
npm run build

# 打包为 Windows 安装包
npm run build:win

# 打包为 macOS 应用
npm run build:mac

# 打包为 Linux 应用
npm run build:linux

# 同时打包多个平台(需要对应操作系统)
npm run build:win && npm run build:mac

11.4 自动更新配置

# 安装 electron-updater
npm install electron-updater --save
// src/main/update.ts
import { autoUpdater } from 'electron-updater'
import { BrowserWindow, ipcMain } from 'electron'

export function setupAutoUpdater(mainWindow: BrowserWindow): void {
  // 自动下载更新
  autoUpdater.autoDownload = false

  // 检查更新
  autoUpdater.checkForUpdates().catch(err => {
    console.error('检查更新失败:', err)
  })

  // 更新事件
  autoUpdater.on('update-available', (info) => {
    console.log('发现新版本:', info.version)
    mainWindow.webContents.send('update-available', info)
  })

  autoUpdater.on('update-downloaded', (info) => {
    console.log('更新已下载:', info.version)
    mainWindow.webContents.send('update-downloaded', info)
  })

  autoUpdater.on('error', (err) => {
    console.error('更新错误:', err)
  })

  // IPC 处理
  ipcMain.handle('check-for-updates', () => {
    return autoUpdater.checkForUpdates()
  })

  ipcMain.handle('download-update', () => {
    return autoUpdater.downloadUpdate()
  })

  ipcMain.handle('install-update', () => {
    autoUpdater.quitAndInstall()
  })
}

12. 排错指南与最佳实践

12.1 常见错误与解决方案

错误类型错误信息原因解决方案
模块找不到Cannot find module 'electron'依赖未安装npm install
构建失败Failed to run install scriptelectron-builder 问题npm install --ignore-scripts 后重新安装
预加载错误preload script failedpreload 路径错误检查 nodeIntegrationcontextIsolation
窗口空白白屏路径错误/文件未找到检查 loadFile 路径
IPC 通信失败contextBridge not definedpreload 未正确配置检查 contextBridge.exposeInMainWorld
打包后无法运行DLL 缺失缺少 Visual C++ Redistributable安装 VC++ 运行时

12.2 开发调试技巧

// main.ts - 开启详细日志
import { app } from 'electron'
import * as fs from 'fs'

// 开启日志
const logPath = `${app.getPath('userData')}/app.log`
const logStream = fs.createWriteStream(logPath, { flags: 'a' })

console.log = (...args) => {
  const msg = `[${new Date().toISOString()}] ${args.join(' ')}`
  logStream.write(msg + '\n')
  process.stdout.write(msg + '\n')
}

// 全局错误捕获
process.on('uncaughtException', (error) => {
  console.error('未捕获的异常:', error)
})

process.on('unhandledRejection', (reason) => {
  console.error('未处理的 Promise 拒绝:', reason)
})

12.3 性能优化

// 1. 禁用硬件加速(解决某些 GPU 问题)
app.disableHardwareAcceleration()

// 2. 限制并发加载
BrowserWindow.additionalFeatures('webgl', {
  maxConcurrency: 4
})

// 3. 使用 WebSecurity
webPreferences: {
  webSecurity: true,          // 生产环境开启
  allowRunningInsecureContent: false
}

// 4. 内存优化 - 禁用 PDF 查看器
app.commandLine.appendSwitch('disable-pdf-viewer')

// 5. 懒加载窗口
app.commandLine.appendSwitch('lazy-field-trial-group')

12.4 安全最佳实践

// ✅ 推荐:使用 contextIsolation
webPreferences: {
  contextIsolation: true,
  nodeIntegration: false,      // 禁用 Node.js
  sandbox: true               // 启用沙箱
}

// ✅ 推荐:使用 preload 脚本暴露 API
contextBridge.exposeInMainWorld('electron', {
  // 只暴露必要的 API
})

// ❌ 避免:在渲染进程中使用 require/import Node.js 模块
// ❌ 避免:直接暴露危险 API(如 child_process)

// ✅ 推荐:验证 IPC 通道
ipcMain.on('dangerous-action', (event) => {
  // 验证来源
  if (event.senderFrame?.origin !== 'file://') {
    return
  }
  // 执行操作
})

12.5 目录结构最佳实践

├── electron.vite.config.ts    # Vite 配置(TypeScript)
├── package.json
├── src/
│   ├── main/                  # 主进程(Node.js)
│   │   ├── index.ts          # 入口
│   │   ├── ipc.ts            # IPC 处理
│   │   ├── menu.ts           # 菜单
│   │   ├── tray.ts           # 托盘
│   │   └── window.ts         # 窗口
│   ├── preload/              # 预加载(桥接)
│   │   └── index.ts
│   ├── renderer/             # 渲染进程(Vue/React)
│   │   ├── index.html
│   │   └── src/
│   │       ├── main.ts
│   │       ├── App.vue
│   │       ├── router/
│   │       ├── stores/
│   │       ├── views/
│   │       └── components/
│   └── shared/               # 共享代码
│       └── types.ts
├── resources/                # 应用资源
│   └── icon.png
└── build/                    # 构建配置
    └── icon.ico

12.6 调试清单

# 调试命令
npm run dev                    # 开发模式
npm run build && npm run preview  # 预览构建

# VSCode 调试配置 .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Electron Main",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args": ["."],
      "outputCapture": "std"
    },
    {
      "name": "Electron Renderer",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/src/renderer"
    }
  ]
}

📚 总结

核心知识点回顾

模块核心技能
项目初始化electron-vite 脚手架、目录结构
主进程BrowserWindow、生命周期管理
预加载contextBridge、安全桥接
IPCinvoke/send/on、进程通信
Vue 集成Pinia、Vue Router、组件开发
系统集成菜单、托盘、窗口控制
打包发布electron-builder、跨平台构建

延伸学习方向

  1. Tauri – Rust 后端,更轻量的替代方案
  2. Electron Forge – 官方推荐的打包工具
  3. 性能优化 – 内存管理、启动速度优化
  4. 安全加固 – CSP、WebSecurity
  5. 自动化测试 – Spectron、Playwright
© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容