细纲生成按大纲规划的每卷章数出章,大纲页可编辑计划章数
- volumes 表新增 chapter_count 列(增量迁移),应用大纲时落库保存 - 细纲生成目标章数优先级:大纲计划章数 > 卷内已有章节数 > 默认 10 - outline 路由支持手动设置 chapter_count;大纲页每卷卡片新增「计划章数」输入框
This commit is contained in:
@@ -55,7 +55,8 @@ export function migrate() {
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
sort INTEGER NOT NULL DEFAULT 0
|
||||
sort INTEGER NOT NULL DEFAULT 0,
|
||||
chapter_count INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS beats (
|
||||
@@ -126,7 +127,8 @@ export function migrate() {
|
||||
['beats', 'pov', "ALTER TABLE beats ADD COLUMN pov TEXT NOT NULL DEFAULT ''"],
|
||||
['beats', 'emotional_arc', "ALTER TABLE beats ADD COLUMN emotional_arc TEXT NOT NULL DEFAULT ''"],
|
||||
['beats', 'purpose', "ALTER TABLE beats ADD COLUMN purpose TEXT NOT NULL DEFAULT ''"],
|
||||
['beats', 'tension', 'ALTER TABLE beats ADD COLUMN tension INTEGER NOT NULL DEFAULT 0']
|
||||
['beats', 'tension', 'ALTER TABLE beats ADD COLUMN tension INTEGER NOT NULL DEFAULT 0'],
|
||||
['volumes', 'chapter_count', 'ALTER TABLE volumes ADD COLUMN chapter_count INTEGER']
|
||||
]
|
||||
for (const [table, col, ddl] of addColumns) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as { name: string }[]
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface VolumeRow {
|
||||
title: string
|
||||
summary: string
|
||||
sort: number
|
||||
chapter_count: number | null
|
||||
}
|
||||
|
||||
export interface BeatRow {
|
||||
|
||||
@@ -202,9 +202,9 @@ generateRouter.post('/generate/beats', (req, res) => {
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// 本卷目标章数:应用大纲时创建的空章节数;没有章节信息时默认 10
|
||||
// 本卷目标章数:优先用大纲规划的每卷章数,其次本卷已有章节数,都没有时默认 10
|
||||
const existing = bundle.chapters.filter((c: ChapterRow) => c.volume_id === volume.id).length
|
||||
const chapterCount = existing > 0 ? existing : 10
|
||||
const chapterCount = volume.chapter_count && volume.chapter_count > 0 ? volume.chapter_count : existing > 0 ? existing : 10
|
||||
const messages = beatsMessages(
|
||||
toProjectLike(bundle.project),
|
||||
{ title: volume.title, summary: volume.summary, chapter_count: chapterCount },
|
||||
@@ -393,7 +393,13 @@ generateRouter.post('/generate/apply/volumes', (req, res) => {
|
||||
db.prepare('DELETE FROM beats WHERE project_id = ?').run(projectId)
|
||||
db.prepare('DELETE FROM volumes WHERE project_id = ?').run(projectId)
|
||||
items.forEach((v, i) => {
|
||||
db.prepare('INSERT INTO volumes (project_id, title, summary, sort) VALUES (?,?,?,?)').run(projectId, v.title, v.summary, i + 1)
|
||||
db.prepare('INSERT INTO volumes (project_id, title, summary, sort, chapter_count) VALUES (?,?,?,?,?)').run(
|
||||
projectId,
|
||||
v.title,
|
||||
v.summary,
|
||||
i + 1,
|
||||
v.chapter_count ?? null
|
||||
)
|
||||
})
|
||||
db.exec('COMMIT')
|
||||
} catch (e) {
|
||||
|
||||
@@ -9,7 +9,8 @@ export const outlineRouter = Router()
|
||||
const volumeInput = z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
summary: z.string().max(4000).default(''),
|
||||
sort: z.number().int().default(0)
|
||||
sort: z.number().int().default(0),
|
||||
chapter_count: z.number().int().min(1).max(2000).nullable().optional()
|
||||
})
|
||||
|
||||
outlineRouter.post('/projects/:projectId/volumes', (req, res) => {
|
||||
@@ -17,8 +18,8 @@ outlineRouter.post('/projects/:projectId/volumes', (req, res) => {
|
||||
const parsed = volumeInput.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues[0].message })
|
||||
const info = db
|
||||
.prepare('INSERT INTO volumes (project_id, title, summary, sort) VALUES (?,?,?,?)')
|
||||
.run(projectId, parsed.data.title, parsed.data.summary, parsed.data.sort)
|
||||
.prepare('INSERT INTO volumes (project_id, title, summary, sort, chapter_count) VALUES (?,?,?,?,?)')
|
||||
.run(projectId, parsed.data.title, parsed.data.summary, parsed.data.sort, parsed.data.chapter_count ?? null)
|
||||
res.json(db.prepare('SELECT * FROM volumes WHERE id = ?').get(info.lastInsertRowid))
|
||||
})
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface Volume {
|
||||
title: string
|
||||
summary: string
|
||||
sort: number
|
||||
chapter_count: number | null
|
||||
}
|
||||
|
||||
export interface Beat {
|
||||
|
||||
@@ -48,8 +48,19 @@ export default function OutlineView({
|
||||
<AutoSaveText value={v.title} onSave={(t) => saveVolume(v.id, { title: t })} placeholder="卷名" bordered={false} style={{ fontWeight: 700, fontSize: 15 }} />
|
||||
</div>
|
||||
<AutoSaveText value={v.summary} onSave={(s) => saveVolume(v.id, { summary: s })} multiline minRows={2} placeholder="本卷大纲:核心冲突、关键事件、结尾钩子" style={{ color: '#555' }} />
|
||||
<div style={{ marginTop: 6, color: '#bbb', fontSize: 12 }}>
|
||||
本卷 {bundle.chapters.filter((c) => c.volume_id === v.id).length} 章(生成细纲后自动创建)
|
||||
<div style={{ marginTop: 6, color: '#bbb', fontSize: 12, display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
|
||||
<span>本卷已有 {bundle.chapters.filter((c) => c.volume_id === v.id).length} 章 · 计划章数</span>
|
||||
<AutoSaveText
|
||||
value={v.chapter_count ? String(v.chapter_count) : ''}
|
||||
onSave={(t) => {
|
||||
const n = parseInt(t, 10)
|
||||
saveVolume(v.id, { chapter_count: Number.isFinite(n) && n > 0 ? n : null })
|
||||
}}
|
||||
placeholder="自动"
|
||||
size="small"
|
||||
style={{ width: 64 }}
|
||||
/>
|
||||
<span>(生成细纲时按此数量出章;留空则按已有章节数,都没有时默认 10)</span>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user