feat: 实现笔临 P0/P1 Electron 桌面应用 v0.1.0

交付写作核心(TipTap、自动保存、分卷分章)、Onboarding、7 主题与双语 i18n、快捷键设置;主进程使用 node:sqlite;补全 Vitest 与 Playwright E2E;统一 design/ 目录并移除 desigin 拼写错误路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 10:39:20 +08:00
parent a06038035b
commit 011bd6d4ca
70 changed files with 14245 additions and 84 deletions
+39
View File
@@ -0,0 +1,39 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { BookMeta, CreateBookParams } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { wrap } from '../result'
export function registerBookHandlers(registry: BookRegistryService): void {
ipcMain.handle(IPC.BOOK_LIST, () => wrap(() => registry.list()))
ipcMain.handle(IPC.BOOK_CREATE, (_event, params: CreateBookParams) =>
wrap(() => registry.create(params))
)
ipcMain.handle(IPC.BOOK_DELETE, (_event, { bookId }: { bookId: string }) =>
wrap(() => {
registry.delete(bookId)
})
)
ipcMain.handle(IPC.BOOK_OPEN, (_event, { bookId }: { bookId: string }) =>
wrap(() => registry.open(bookId))
)
ipcMain.handle(
IPC.BOOK_UPDATE_META,
(
_event,
{
bookId,
...patch
}: {
bookId: string
lastChapterId?: string | null
lastOpenedAt?: string
status?: BookMeta['status']
}
) => wrap(() => registry.updateMeta(bookId, patch))
)
}
+96
View File
@@ -0,0 +1,96 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { ChapterStatus } from '../../../shared/types'
import { BookRegistryService } from '../../services/book-registry'
import { wrap } from '../result'
export function registerChapterHandlers(registry: BookRegistryService): void {
ipcMain.handle(
IPC.VOLUME_CREATE,
(_event, { bookId, name }: { bookId: string; name: string }) =>
wrap(() => {
const repo = registry.getVolumeRepo(bookId)
const volumes = repo.list()
return repo.create(name, volumes.length)
})
)
ipcMain.handle(
IPC.VOLUME_UPDATE,
(
_event,
{ bookId, volumeId, name, description }: { bookId: string; volumeId: string; name?: string; description?: string }
) => wrap(() => registry.getVolumeRepo(bookId).update(volumeId, { name, description }))
)
ipcMain.handle(
IPC.VOLUME_DELETE,
(_event, { bookId, volumeId }: { bookId: string; volumeId: string }) =>
wrap(() => {
registry.getVolumeRepo(bookId).delete(volumeId)
})
)
ipcMain.handle(
IPC.CHAPTER_CREATE,
(
_event,
{ bookId, volumeId, title }: { bookId: string; volumeId: string; title: string }
) =>
wrap(() => {
const repo = registry.getChapterRepo(bookId)
const sortOrder = repo.nextSortOrder(volumeId)
return repo.create(volumeId, title, sortOrder)
})
)
ipcMain.handle(
IPC.CHAPTER_GET,
(_event, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
wrap(() => {
const ch = registry.getChapterRepo(bookId).get(chapterId)
if (!ch) throw new Error('Chapter not found')
return ch
})
)
ipcMain.handle(
IPC.CHAPTER_UPDATE,
(
_event,
{
bookId,
chapterId,
title,
content,
status,
cursorOffset
}: {
bookId: string
chapterId: string
title?: string
content?: string
status?: ChapterStatus
cursorOffset?: number
}
) =>
wrap(() => {
const chapter = registry.getChapterRepo(bookId).update(chapterId, {
title,
content,
status,
cursorOffset
})
registry.updateMeta(bookId, { lastChapterId: chapterId })
return chapter
})
)
ipcMain.handle(
IPC.CHAPTER_DELETE,
(_event, { bookId, chapterId }: { bookId: string; chapterId: string }) =>
wrap(() => {
registry.getChapterRepo(bookId).delete(chapterId)
})
)
}
+11
View File
@@ -0,0 +1,11 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import { GlobalSettingsService } from '../services/global-settings'
import { wrap } from '../result'
export function registerSettingsHandlers(settings: GlobalSettingsService): void {
ipcMain.handle(IPC.SETTINGS_GET, () => wrap(() => settings.get()))
ipcMain.handle(IPC.SETTINGS_UPDATE, (_event, partial) =>
wrap(() => settings.update(partial))
)
}
+18
View File
@@ -0,0 +1,18 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { ActionId } from '../../../shared/types'
import { ShortcutManager } from '../shortcuts/manager'
import { wrap } from '../result'
export function registerShortcutHandlers(shortcuts: ShortcutManager): void {
ipcMain.handle(IPC.SHORTCUT_GET_ALL, () => wrap(() => shortcuts.getAll()))
ipcMain.handle(
IPC.SHORTCUT_REGISTER,
(_event, { action, accelerator }: { action: ActionId; accelerator: string }) =>
wrap(() => {
const ok = shortcuts.save(action, accelerator)
if (!ok) throw new Error('快捷键注册失败,可能与其他应用冲突')
})
)
}