一、回顾与概览
在上一篇中,我们完成了 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
}
}