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:
@@ -0,0 +1,91 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { countWords } from '../../services/word-count'
|
||||
import type { Chapter, ChapterStatus } from '../../../shared/types'
|
||||
import type { SqliteDb } from '../types'
|
||||
function mapRow(row: Record<string, unknown>): Chapter {
|
||||
return {
|
||||
id: row.id as string,
|
||||
volumeId: row.volume_id as string | null,
|
||||
title: row.title as string,
|
||||
content: row.content as string,
|
||||
status: row.status as ChapterStatus,
|
||||
wordCount: row.word_count as number,
|
||||
sortOrder: row.sort_order as number,
|
||||
cursorOffset: (row.cursor_offset as number) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
export class ChapterRepository {
|
||||
constructor(private db: SqliteDb) {}
|
||||
create(volumeId: string, title: string, sortOrder: number, content = ''): Chapter {
|
||||
const id = randomUUID()
|
||||
const wordCount = countWords(content)
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO chapters (id, volume_id, title, content, word_count, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(id, volumeId, title, content, wordCount, sortOrder)
|
||||
return this.get(id)!
|
||||
}
|
||||
|
||||
get(id: string): Chapter | null {
|
||||
const row = this.db.prepare('SELECT * FROM chapters WHERE id = ?').get(id) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
return row ? mapRow(row) : null
|
||||
}
|
||||
|
||||
list(): Chapter[] {
|
||||
return (
|
||||
this.db.prepare('SELECT * FROM chapters ORDER BY sort_order').all() as Record<string, unknown>[]
|
||||
).map(mapRow)
|
||||
}
|
||||
|
||||
listByVolume(volumeId: string): Chapter[] {
|
||||
return (
|
||||
this.db
|
||||
.prepare('SELECT * FROM chapters WHERE volume_id = ? ORDER BY sort_order')
|
||||
.all(volumeId) as Record<string, unknown>[]
|
||||
).map(mapRow)
|
||||
}
|
||||
|
||||
update(
|
||||
id: string,
|
||||
patch: Partial<{ title: string; content: string; status: ChapterStatus; cursorOffset: number }>
|
||||
): Chapter {
|
||||
const existing = this.get(id)
|
||||
if (!existing) throw new Error('Chapter not found')
|
||||
|
||||
const content = patch.content ?? existing.content
|
||||
const wordCount = countWords(content)
|
||||
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE chapters SET
|
||||
title = ?, content = ?, status = ?, cursor_offset = ?,
|
||||
word_count = ?, updated_at = datetime('now')
|
||||
WHERE id = ?`
|
||||
)
|
||||
.run(
|
||||
patch.title ?? existing.title,
|
||||
content,
|
||||
patch.status ?? existing.status,
|
||||
patch.cursorOffset ?? existing.cursorOffset,
|
||||
wordCount,
|
||||
id
|
||||
)
|
||||
return this.get(id)!
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.db.prepare('DELETE FROM chapters WHERE id = ?').run(id)
|
||||
}
|
||||
|
||||
nextSortOrder(volumeId: string): number {
|
||||
const row = this.db
|
||||
.prepare('SELECT MAX(sort_order) AS maxOrder FROM chapters WHERE volume_id = ?')
|
||||
.get(volumeId) as { maxOrder: number | null }
|
||||
return (row?.maxOrder ?? -1) + 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { Volume } from '../../../shared/types'
|
||||
import type { SqliteDb } from '../types'
|
||||
function mapRow(row: Record<string, unknown>): Volume {
|
||||
return {
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
description: row.description as string,
|
||||
sortOrder: row.sort_order as number
|
||||
}
|
||||
}
|
||||
|
||||
export class VolumeRepository {
|
||||
constructor(private db: SqliteDb) {}
|
||||
create(name: string, sortOrder: number): Volume {
|
||||
const id = randomUUID()
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO volumes (id, name, sort_order) VALUES (?, ?, ?)`
|
||||
)
|
||||
.run(id, name, sortOrder)
|
||||
return this.get(id)!
|
||||
}
|
||||
|
||||
get(id: string): Volume | null {
|
||||
const row = this.db.prepare('SELECT * FROM volumes WHERE id = ?').get(id) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
return row ? mapRow(row) : null
|
||||
}
|
||||
|
||||
list(): Volume[] {
|
||||
return (
|
||||
this.db.prepare('SELECT * FROM volumes ORDER BY sort_order').all() as Record<string, unknown>[]
|
||||
).map(mapRow)
|
||||
}
|
||||
|
||||
update(id: string, patch: { name?: string; description?: string }): Volume {
|
||||
const existing = this.get(id)
|
||||
if (!existing) throw new Error('Volume not found')
|
||||
this.db
|
||||
.prepare(
|
||||
`UPDATE volumes SET name = ?, description = ?, updated_at = datetime('now') WHERE id = ?`
|
||||
)
|
||||
.run(patch.name ?? existing.name, patch.description ?? existing.description, id)
|
||||
return this.get(id)!
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.db.prepare('DELETE FROM volumes WHERE id = ?').run(id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user