ea4819847f
Break down templates, import, submission export, and inspiration capture into twelve tracked tasks for the daily writing workflow release. Co-authored-by: Cursor <cursoragent@cursor.com>
664 lines
20 KiB
Markdown
664 lines
20 KiB
Markdown
# 笔临 v1.1 作家日更工具包 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 交付 v1.1.0:§10 章节模板 + 快速新建、§11.1 txt/md/docx 导入、§20.2 投稿预设单章导出、§20.6 全局灵感捕获增强。
|
||
|
||
**Architecture:** `GlobalSettings` 扩展 `chapterTemplates` / `submissionPresets`;渲染进程 `chapter-template.ts` 做变量替换后复用 `chapter.create/update`;主进程 `ImportService`(mammoth/marked + `chapter-splitter`)与 `ExportService`(`submission-formatter` + clipboard);灵感模态解除视图限制且保存不跳转。
|
||
|
||
**Tech Stack:** Electron 36、electron-vite、React 18、TypeScript、node:sqlite、mammoth、marked、Zustand、i18next、Vitest、Playwright
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-07-08-bilin-v11-writer-toolkit-design.md`
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `src/shared/types.ts` | `ChapterTemplate`、`SubmissionPreset`、`Import*`、`Export*` DTO |
|
||
| `src/shared/builtin-templates.ts` | 内置模板与投稿预设常量 |
|
||
| `src/shared/ipc-channels.ts` | `import.*` / `export.*` / `IMPORT_PROGRESS` |
|
||
| `src/main/lib/chapter-splitter.ts` | txt/md/html 分章 |
|
||
| `src/main/lib/submission-formatter.ts` | HTML → 投稿纯文本 |
|
||
| `src/main/services/import.service.ts` | 读文件、预览、创建书+章 |
|
||
| `src/main/services/export.service.ts` | 格式化章节、剪贴板、存盘 |
|
||
| `src/main/ipc/handlers/import.handler.ts` | 导入 IPC + progress 事件 |
|
||
| `src/main/ipc/handlers/export.handler.ts` | 导出 IPC |
|
||
| `src/main/ipc/register.ts` | 注册 import/export |
|
||
| `src/preload/index.ts` + `electron-api.d.ts` | import / export API |
|
||
| `src/renderer/lib/chapter-template.ts` | 模板变量替换 |
|
||
| `src/renderer/components/chapter/TemplateQuickCreateModal.tsx` | 快速新建章 |
|
||
| `src/renderer/components/settings/TemplateSettingsTab.tsx` | 模板 CRUD |
|
||
| `src/renderer/components/settings/SubmissionPresetTab.tsx` | 投稿预设 CRUD |
|
||
| `src/renderer/components/import/ImportBookModal.tsx` | 导入 UI |
|
||
| `src/renderer/components/export/ExportModal.tsx` | 导出 UI |
|
||
| `src/renderer/components/inspiration/InspirationModal.tsx` | 全局灵感增强 |
|
||
| `src/renderer/components/home/HomePage.tsx` | 导入入口 |
|
||
| `src/renderer/components/layout/EditorLayout.tsx` | 快速新建/导出入口 |
|
||
| `src/renderer/App.tsx` | 全局灵感快捷键 |
|
||
| `src/renderer/stores/useSettingsStore.ts` | 新 settings 字段默认值 |
|
||
| `src/main/services/global-settings.ts` | defaults 合并 |
|
||
| `public/locales/*/translation.json` | template / import / export / submission / inspiration |
|
||
| `src/renderer/styles/layout.css` | 模板/导入/导出样式 |
|
||
| `tests/main/chapter-template.test.ts` | UT-TEMPL-* |
|
||
| `tests/main/chapter-splitter.test.ts` | UT-IMPORT-01 |
|
||
| `tests/main/submission-formatter.test.ts` | UT-EXPORT-* |
|
||
| `tests/main/import.service.test.ts` | IT-IMPORT-01 |
|
||
| `e2e/chapter-template.spec.ts` | E2E-TEMPL-01 |
|
||
| `e2e/import-book.spec.ts` | E2E-IMPORT-01 |
|
||
| `e2e/export-submission.spec.ts` | E2E-EXPORT-01 |
|
||
| `e2e/inspiration-capture.spec.ts` | E2E-INSP-01 |
|
||
|
||
---
|
||
|
||
## Task 1: 类型 + 内置常量 + GlobalSettings
|
||
|
||
**Files:**
|
||
- Modify: `src/shared/types.ts`
|
||
- Create: `src/shared/builtin-templates.ts`
|
||
- Modify: `src/main/services/global-settings.ts`
|
||
- Modify: `src/renderer/stores/useSettingsStore.ts`
|
||
|
||
- [ ] **Step 1: 扩展 types**
|
||
|
||
```typescript
|
||
export interface ChapterTemplate {
|
||
id: string
|
||
name: string
|
||
body: string
|
||
defaultStatus?: ChapterStatus
|
||
builtin?: boolean
|
||
}
|
||
|
||
export interface SubmissionPreset {
|
||
id: string
|
||
name: string
|
||
titleFormat: string
|
||
indentParagraphs: boolean
|
||
includeAuthorsNote: boolean
|
||
paragraphGap: 'single' | 'double'
|
||
}
|
||
|
||
export type ImportSplitMode = 'auto' | 'paragraph' | 'regex'
|
||
|
||
export interface ImportChapterPreview {
|
||
title: string
|
||
contentHtml: string
|
||
wordCount: number
|
||
}
|
||
|
||
export interface ImportPreviewResult {
|
||
fileName: string
|
||
fileSizeBytes: number
|
||
chapters: ImportChapterPreview[]
|
||
warnings: string[]
|
||
}
|
||
|
||
export interface ImportExecuteParams {
|
||
filePath: string
|
||
bookName: string
|
||
category: string
|
||
splitMode: ImportSplitMode
|
||
customRegex?: string
|
||
}
|
||
|
||
// GlobalSettings 追加:
|
||
chapterTemplates?: ChapterTemplate[]
|
||
submissionPresets?: SubmissionPreset[]
|
||
defaultSubmissionPresetId?: string
|
||
```
|
||
|
||
- [ ] **Step 2: builtin-templates.ts**
|
||
|
||
```typescript
|
||
import type { ChapterTemplate, SubmissionPreset } from './types'
|
||
|
||
export const BUILTIN_CHAPTER_TEMPLATES: ChapterTemplate[] = [
|
||
{
|
||
id: 'builtin-generic',
|
||
name: '通用模板',
|
||
body: '## 第{chapterNumber}章 {chapterTitle}\n\n',
|
||
builtin: true
|
||
},
|
||
{
|
||
id: 'builtin-webnovel',
|
||
name: '网文常用',
|
||
body: '## 第{chapterNumber}章 {chapterTitle}\n\n\n\n——\n作者的话:\n',
|
||
builtin: true
|
||
}
|
||
]
|
||
|
||
export const BUILTIN_SUBMISSION_PRESETS: SubmissionPreset[] = [
|
||
{
|
||
id: 'builtin-webnovel',
|
||
name: '通用网文',
|
||
titleFormat: '# 第{chapterNumber}章 {chapterTitle}',
|
||
indentParagraphs: true,
|
||
includeAuthorsNote: true,
|
||
paragraphGap: 'double'
|
||
}
|
||
]
|
||
|
||
export function mergeChapterTemplates(custom: ChapterTemplate[] = []): ChapterTemplate[] {
|
||
return [...BUILTIN_CHAPTER_TEMPLATES, ...custom.filter((t) => !t.builtin)]
|
||
}
|
||
|
||
export function mergeSubmissionPresets(custom: SubmissionPreset[] = []): SubmissionPreset[] {
|
||
return [...BUILTIN_SUBMISSION_PRESETS, ...custom.filter((p) => p.id !== 'builtin-webnovel')]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: global-settings defaults**
|
||
|
||
在 `defaults()` 追加 `chapterTemplates: []`, `submissionPresets: []`, `defaultSubmissionPresetId: 'builtin-webnovel'`。
|
||
|
||
- [ ] **Step 4: useSettingsStore 初始字段对齐**
|
||
|
||
- [ ] **Step 5: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: chapter-template 引擎 + 单元测试
|
||
|
||
**Files:**
|
||
- Create: `src/renderer/lib/chapter-template.ts`
|
||
- Create: `tests/main/chapter-template.test.ts`
|
||
|
||
- [ ] **Step 1: 实现 replaceChapterTemplate**
|
||
|
||
```typescript
|
||
export interface TemplateContext {
|
||
chapterNumber: number
|
||
chapterTitle: string
|
||
penName: string
|
||
date: string
|
||
volumeName: string
|
||
previousChapterTitle: string
|
||
outlineItem: string
|
||
characterList: string
|
||
}
|
||
|
||
const VARS = [
|
||
'chapterNumber', 'chapterTitle', 'penName', 'date',
|
||
'volumeName', 'previousChapterTitle', 'outlineItem', 'characterList'
|
||
] as const
|
||
|
||
export function replaceChapterTemplate(body: string, ctx: TemplateContext): string {
|
||
let out = body
|
||
for (const key of VARS) {
|
||
out = out.split(`{${key}}`).join(String(ctx[key]))
|
||
}
|
||
return out
|
||
}
|
||
|
||
export function plainTextToEditorHtml(text: string): string {
|
||
return text
|
||
.split(/\n{2,}/)
|
||
.map((p) => `<p>${p.replace(/\n/g, '<br>')}</p>`)
|
||
.join('')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: UT-TEMPL-01~03**
|
||
|
||
```typescript
|
||
it('UT-TEMPL-01: replaces chapterNumber and chapterTitle', () => {
|
||
const result = replaceChapterTemplate('第{chapterNumber}章 {chapterTitle}', {
|
||
chapterNumber: 5, chapterTitle: '觉醒',
|
||
penName: '', date: '', volumeName: '', previousChapterTitle: '',
|
||
outlineItem: '', characterList: ''
|
||
})
|
||
expect(result).toBe('第5章 觉醒')
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 3: 运行**
|
||
|
||
```bash
|
||
npm run test -- tests/main/chapter-template.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: TemplateQuickCreateModal + 章节侧栏入口
|
||
|
||
**Files:**
|
||
- Create: `src/renderer/components/chapter/TemplateQuickCreateModal.tsx`
|
||
- Modify: `src/renderer/components/layout/EditorLayout.tsx`
|
||
|
||
- [ ] **Step 1: TemplateQuickCreateModal**
|
||
|
||
- `data-testid="chapter-quick-new"` 打开模态
|
||
- 从 `useSettingsStore` + `mergeChapterTemplates` 展示模板卡片
|
||
- 创建流程:`chapter.create` → `replaceChapterTemplate` → `plainTextToEditorHtml` → `chapter.update({ content })`
|
||
- 若模板 `defaultStatus` 设置则 `chapter.update({ status })`
|
||
- `switchTarget` 到新章
|
||
|
||
- [ ] **Step 2: EditorLayout**
|
||
|
||
在章节面板底部保留原「+ 新章」按钮,新增「模板新建」按钮 `data-testid="chapter-quick-new"`。
|
||
|
||
- [ ] **Step 3: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: 设置页章节模板 Tab
|
||
|
||
**Files:**
|
||
- Create: `src/renderer/components/settings/TemplateSettingsTab.tsx`
|
||
- Modify: `src/renderer/components/settings/SettingsPage.tsx`
|
||
|
||
- [ ] **Step 1: TemplateSettingsTab**
|
||
|
||
- `data-testid="settings-chapter-templates"`
|
||
- 内置模板只读展示
|
||
- 自定义:`setting-template-add` 新建;编辑 name/body;删除(uuid id)
|
||
- 保存:`settings.update({ chapterTemplates: customOnly })`
|
||
|
||
- [ ] **Step 2: SettingsPage 导航**
|
||
|
||
新增 section `'templates'`,侧栏项 `settings.templates`。
|
||
|
||
- [ ] **Step 3: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: submission-formatter + 单元测试
|
||
|
||
**Files:**
|
||
- Create: `src/main/lib/submission-formatter.ts`
|
||
- Create: `tests/main/submission-formatter.test.ts`
|
||
|
||
- [ ] **Step 1: 实现**
|
||
|
||
```typescript
|
||
import { stripHtml } from '../services/word-count'
|
||
import type { SubmissionPreset } from '../../shared/types'
|
||
|
||
export interface FormatChapterInput {
|
||
chapterNumber: number
|
||
chapterTitle: string
|
||
contentHtml: string
|
||
preset: SubmissionPreset
|
||
}
|
||
|
||
export function formatChapterForSubmission(input: FormatChapterInput): string {
|
||
let body = stripHtml(input.contentHtml)
|
||
if (!input.preset.includeAuthorsNote) {
|
||
body = body.split(/\n——\n/)[0] ?? body
|
||
}
|
||
const paragraphs = body.split(/\n+/).filter((p) => p.trim())
|
||
const indented = input.preset.indentParagraphs
|
||
? paragraphs.map((p) => ` ${p.trim()}`)
|
||
: paragraphs.map((p) => p.trim())
|
||
const gap = input.preset.paragraphGap === 'double' ? '\n\n' : '\n'
|
||
const title = input.preset.titleFormat
|
||
.replace('{chapterNumber}', String(input.chapterNumber))
|
||
.replace('{chapterTitle}', input.chapterTitle)
|
||
return `${title}\n\n${indented.join(gap)}`
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: UT-EXPORT-01 / UT-EXPORT-02**
|
||
|
||
- [ ] **Step 3: 运行**
|
||
|
||
```bash
|
||
npm run test -- tests/main/submission-formatter.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: ExportService + IPC + preload
|
||
|
||
**Files:**
|
||
- Create: `src/main/services/export.service.ts`
|
||
- Create: `src/main/ipc/handlers/export.handler.ts`
|
||
- Modify: `src/shared/ipc-channels.ts`
|
||
- Modify: `src/main/ipc/register.ts`
|
||
- Modify: `src/preload/index.ts`
|
||
- Modify: `src/shared/electron-api.d.ts`
|
||
|
||
- [ ] **Step 1: ExportService**
|
||
|
||
```typescript
|
||
export class ExportService {
|
||
constructor(
|
||
private registry: BookRegistryService,
|
||
private settings: GlobalSettingsService
|
||
) {}
|
||
|
||
formatChapter(bookId: string, chapterId: string, presetId: string): string {
|
||
const db = this.registry.getDb(bookId)
|
||
const chapter = new ChapterRepository(db).get(chapterId)
|
||
if (!chapter) throw new Error('Chapter not found')
|
||
const presets = mergeSubmissionPresets(this.settings.get().submissionPresets)
|
||
const preset = presets.find((p) => p.id === presetId) ?? BUILTIN_SUBMISSION_PRESETS[0]
|
||
const chapters = new ChapterRepository(db).list().filter((c) => c.volumeId === chapter.volumeId)
|
||
const chapterNumber = chapters.findIndex((c) => c.id === chapterId) + 1
|
||
return formatChapterForSubmission({
|
||
chapterNumber, chapterTitle: chapter.title,
|
||
contentHtml: chapter.content, preset
|
||
})
|
||
}
|
||
|
||
copyToClipboard(text: string): void {
|
||
clipboard.writeText(text)
|
||
}
|
||
|
||
async saveTxt(text: string, defaultName: string): Promise<string | null> {
|
||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||
defaultPath: defaultName,
|
||
filters: [{ name: 'Text', extensions: ['txt'] }]
|
||
})
|
||
if (canceled || !filePath) return null
|
||
writeFileSync(filePath, text, 'utf8')
|
||
return filePath
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: export.handler + register + preload**
|
||
|
||
```typescript
|
||
EXPORT_FORMAT_CHAPTER: 'export:formatChapter',
|
||
EXPORT_COPY_CLIPBOARD: 'export:copyClipboard',
|
||
EXPORT_SAVE_TXT: 'export:saveTxt',
|
||
```
|
||
|
||
- [ ] **Step 3: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: ExportModal + 投稿预设设置 Tab + 快捷键
|
||
|
||
**Files:**
|
||
- Create: `src/renderer/components/export/ExportModal.tsx`
|
||
- Create: `src/renderer/components/settings/SubmissionPresetTab.tsx`
|
||
- Modify: `src/renderer/components/settings/SettingsPage.tsx`
|
||
- Modify: `src/renderer/components/layout/EditorLayout.tsx`
|
||
- Modify: `src/renderer/App.tsx`
|
||
|
||
- [ ] **Step 1: ExportModal**
|
||
|
||
- `data-testid="export-modal"`
|
||
- `export-preset-select` 下拉(mergeSubmissionPresets)
|
||
- 预览区:调用 `export.formatChapter` 显示前 500 字
|
||
- `export-copy-clipboard` / `export-save-txt`
|
||
|
||
- [ ] **Step 2: SubmissionPresetTab**
|
||
|
||
- `data-testid="settings-submission-presets"`
|
||
- CRUD 自定义预设(不可删 builtin-webnovel)
|
||
|
||
- [ ] **Step 3: EditorLayout 工具栏 + App exportBook 快捷键**
|
||
|
||
```typescript
|
||
if (action === 'exportBook') {
|
||
if (view !== 'editor' || !chapterId) return
|
||
useExportStore.getState().openModal()
|
||
return
|
||
}
|
||
```
|
||
|
||
创建 `useExportStore`(open/close,类似 useGraphStore)。
|
||
|
||
- [ ] **Step 4: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: InspirationModal 全局增强
|
||
|
||
**Files:**
|
||
- Modify: `src/renderer/components/inspiration/InspirationModal.tsx`
|
||
- Modify: `src/renderer/App.tsx`
|
||
- Modify: `src/renderer/styles/layout.css`
|
||
|
||
- [ ] **Step 1: App.tsx 移除 view 限制**
|
||
|
||
```typescript
|
||
if (action === 'captureInspiration') {
|
||
useAppStore.getState().setInspirationModalOpen(true)
|
||
return
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: InspirationModal**
|
||
|
||
- 无 `bookId` 时显示 `inspiration-book-select`(`useBookStore.books` 按 `lastOpenedAt` 排序)
|
||
- `handleSave` 删除 `switchTarget` / `setSidebarPanel`;改为 `showToast(t('inspiration.saved'))`
|
||
- 添加 `inspiration-modal--capture` class
|
||
|
||
- [ ] **Step 3: E2E 预备 build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: chapter-splitter + 单元测试
|
||
|
||
**Files:**
|
||
- Create: `src/main/lib/chapter-splitter.ts`
|
||
- Create: `tests/main/chapter-splitter.test.ts`
|
||
|
||
- [ ] **Step 1: splitPlainText**
|
||
|
||
```typescript
|
||
export function splitPlainText(
|
||
text: string,
|
||
mode: ImportSplitMode,
|
||
customRegex?: string
|
||
): Array<{ title: string; body: string }> {
|
||
// auto: 尝试 MARKDOWN_HEADING 再 CHINESE_CHAPTER
|
||
// paragraph: split \n\s*\n
|
||
// regex: new RegExp(customRegex, 'gm') 分行匹配
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: UT-IMPORT-01** — 样本文本 3 章标题
|
||
|
||
- [ ] **Step 3: 运行**
|
||
|
||
```bash
|
||
npm run test -- tests/main/chapter-splitter.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: ImportService + IPC + 依赖安装
|
||
|
||
**Files:**
|
||
- Create: `src/main/services/import.service.ts`
|
||
- Create: `src/main/ipc/handlers/import.handler.ts`
|
||
- Modify: `package.json`
|
||
- Modify: `src/main/ipc/register.ts`
|
||
- Modify: `src/preload/index.ts`
|
||
- Create: `tests/main/import.service.test.ts`
|
||
|
||
- [ ] **Step 1: 安装依赖**
|
||
|
||
```bash
|
||
npm install mammoth marked --legacy-peer-deps
|
||
npm install -D @types/marked --legacy-peer-deps
|
||
```
|
||
|
||
- [ ] **Step 2: ImportService**
|
||
|
||
```typescript
|
||
export class ImportService {
|
||
async pickFile(): Promise<{ canceled: boolean; filePath?: string }> { /* dialog */ }
|
||
|
||
async preview(filePath: string, splitMode: ImportSplitMode, customRegex?: string): Promise<ImportPreviewResult> {
|
||
const stat = statSync(filePath)
|
||
const ext = extname(filePath).toLowerCase()
|
||
let html: string
|
||
if (ext === '.docx') html = (await mammoth.convertToHtml({ path: filePath })).value
|
||
else if (ext === '.md') html = marked.parse(readFileSync(filePath, 'utf8')) as string
|
||
else html = plainTextToEditorHtml(readFileSync(filePath, 'utf8'))
|
||
const plain = stripHtml(html)
|
||
const parts = splitPlainText(plain, splitMode, customRegex)
|
||
// map to ImportChapterPreview with wordCount
|
||
}
|
||
|
||
async execute(params: ImportExecuteParams, onProgress?: (c: number, t: number) => void): Promise<string> {
|
||
const preview = await this.preview(...)
|
||
const meta = this.registry.create({ name: params.bookName, category: params.category, createSampleChapter: false })
|
||
try {
|
||
const db = this.registry.getDb(meta.id)
|
||
const volId = new VolumeRepository(db).list()[0]?.id ?? new VolumeRepository(db).create('第一卷', 0).id
|
||
for (let i = 0; i < preview.chapters.length; i++) {
|
||
const ch = preview.chapters[i]
|
||
new ChapterRepository(db).create(volId, ch.title, i, ch.contentHtml)
|
||
onProgress?.(i + 1, preview.chapters.length)
|
||
}
|
||
return meta.id
|
||
} catch (e) {
|
||
this.registry.delete(meta.id)
|
||
throw e
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: import.handler**
|
||
|
||
- `IMPORT_PICK_FILE` / `IMPORT_PREVIEW` / `IMPORT_EXECUTE`
|
||
- progress:`BrowserWindow.getAllWindows()[0]?.webContents.send(IPC.IMPORT_PROGRESS, { current, total })`
|
||
|
||
- [ ] **Step 4: IT-IMPORT-01**
|
||
|
||
- [ ] **Step 5: 运行**
|
||
|
||
```bash
|
||
npm run test -- tests/main/import.service.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: ImportBookModal + HomePage
|
||
|
||
**Files:**
|
||
- Create: `src/renderer/components/import/ImportBookModal.tsx`
|
||
- Modify: `src/renderer/components/home/HomePage.tsx`
|
||
- Modify: `src/renderer/App.tsx`(挂载 ImportBookModal)
|
||
|
||
- [ ] **Step 1: ImportBookModal**
|
||
|
||
- `data-testid="import-book-modal"`
|
||
- 浏览 → `import.pickFile`
|
||
- 分章规则 select + 条件 regex 输入
|
||
- 预览按钮 → `import.preview` 显示章数与前 5 标题
|
||
- `fileSizeBytes > 20 * 1024 * 1024` → confirm 对话框
|
||
- 执行 → 监听 `onImportProgress` → `import-progress-bar`
|
||
- 成功 → `openBook(bookId)` + 切 editor
|
||
|
||
- [ ] **Step 2: HomePage 替换 comingSoon**
|
||
|
||
```typescript
|
||
onClick={() => setImportOpen(true)}
|
||
```
|
||
|
||
- [ ] **Step 3: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: i18n + CSS + E2E + v1.1.0
|
||
|
||
**Files:**
|
||
- Modify: `public/locales/zh-CN/translation.json`
|
||
- Modify: `public/locales/en/translation.json`
|
||
- Modify: `src/renderer/styles/layout.css`
|
||
- Create: `e2e/chapter-template.spec.ts`
|
||
- Create: `e2e/import-book.spec.ts`
|
||
- Create: `e2e/export-submission.spec.ts`
|
||
- Create: `e2e/inspiration-capture.spec.ts`
|
||
- Modify: `package.json`
|
||
- Modify: `README.md`
|
||
- Modify: `src/renderer/components/settings/SettingsPage.tsx`(关于页版本)
|
||
|
||
- [ ] **Step 1: i18n 键**(spec §8 全部)
|
||
|
||
- [ ] **Step 2: CSS**
|
||
|
||
```css
|
||
.template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
|
||
.import-preview-list { font-size: 12px; max-height: 120px; overflow: auto; }
|
||
.export-preview { font-family: var(--font-mono); font-size: 11px; white-space: pre-wrap; max-height: 200px; overflow: auto; }
|
||
.inspiration-modal--capture { z-index: 10000; }
|
||
```
|
||
|
||
- [ ] **Step 3: E2E**
|
||
|
||
```typescript
|
||
// E2E-TEMPL-01: 模板新建章 → ProseMirror 含模板正文
|
||
// E2E-IMPORT-01: 导入 fixture txt → 多章节可见
|
||
// E2E-EXPORT-01: 导出复制 → 剪贴板含 # 第
|
||
// E2E-INSP-01: 主页 Ctrl+Shift+I → toast,仍在 home
|
||
```
|
||
|
||
- [ ] **Step 4: 全量验证**
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run test:e2e -- e2e/chapter-template.spec.ts e2e/import-book.spec.ts e2e/export-submission.spec.ts e2e/inspiration-capture.spec.ts
|
||
```
|
||
|
||
- [ ] **Step 5: 版本 1.1.0**
|
||
|
||
`package.json` / `README.md` / Settings 关于页 → `v1.1.0`
|
||
|
||
- [ ] **Step 6: 勾选本 plan 全部 checkbox**
|
||
|
||
---
|
||
|
||
## Spec Coverage Checklist
|
||
|
||
| Spec 要求 | Task |
|
||
|-----------|------|
|
||
| chapterTemplates + 内置模板 | Task 1, 4 |
|
||
| 模板变量 + 快速新建 | Task 2, 3 |
|
||
| submissionPresets + 格式化 | Task 1, 5, 6 |
|
||
| ExportModal + 剪贴板/txt | Task 7 |
|
||
| 全局灵感 + 不跳转 | Task 8 |
|
||
| txt/md/docx 导入 + 分章 | Task 9, 10, 11 |
|
||
| >20MB 确认 + 进度条 | Task 11 |
|
||
| i18n / E2E / v1.1.0 | Task 12 |
|
||
|
||
---
|
||
|
||
## Execution Notes
|
||
|
||
- `plainTextToEditorHtml` 与 `chapter-template.ts` 可共享;主进程导入侧可 import 同逻辑或 duplicate 最小 HTML 包装
|
||
- 导入 `.docx` 用 mammoth HTML 再 `stripHtml` 分章(章标题在纯文本层匹配)
|
||
- `export.formatChapter` 与模板 `{chapterNumber}` 均用卷内 sortOrder 计算序号
|
||
- E2E 导入使用 `tests/fixtures/import-three-chapters.txt` fixture 文件
|
||
- 用户未要求时不自动 git commit;每 Task 完成后勾选 plan checkbox
|