细纲生成按大纲规划的每卷章数出章,大纲页可编辑计划章数
- 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,
|
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
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 (
|
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', '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', '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', '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) {
|
for (const [table, col, ddl] of addColumns) {
|
||||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as { name: string }[]
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as unknown as { name: string }[]
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface VolumeRow {
|
|||||||
title: string
|
title: string
|
||||||
summary: string
|
summary: string
|
||||||
sort: number
|
sort: number
|
||||||
|
chapter_count: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BeatRow {
|
export interface BeatRow {
|
||||||
|
|||||||
@@ -202,9 +202,9 @@ generateRouter.post('/generate/beats', (req, res) => {
|
|||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
||||||
// 本卷目标章数:应用大纲时创建的空章节数;没有章节信息时默认 10
|
// 本卷目标章数:优先用大纲规划的每卷章数,其次本卷已有章节数,都没有时默认 10
|
||||||
const existing = bundle.chapters.filter((c: ChapterRow) => c.volume_id === volume.id).length
|
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(
|
const messages = beatsMessages(
|
||||||
toProjectLike(bundle.project),
|
toProjectLike(bundle.project),
|
||||||
{ title: volume.title, summary: volume.summary, chapter_count: chapterCount },
|
{ 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 beats WHERE project_id = ?').run(projectId)
|
||||||
db.prepare('DELETE FROM volumes WHERE project_id = ?').run(projectId)
|
db.prepare('DELETE FROM volumes WHERE project_id = ?').run(projectId)
|
||||||
items.forEach((v, i) => {
|
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')
|
db.exec('COMMIT')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export const outlineRouter = Router()
|
|||||||
const volumeInput = z.object({
|
const volumeInput = z.object({
|
||||||
title: z.string().min(1).max(100),
|
title: z.string().min(1).max(100),
|
||||||
summary: z.string().max(4000).default(''),
|
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) => {
|
outlineRouter.post('/projects/:projectId/volumes', (req, res) => {
|
||||||
@@ -17,8 +18,8 @@ outlineRouter.post('/projects/:projectId/volumes', (req, res) => {
|
|||||||
const parsed = volumeInput.safeParse(req.body)
|
const parsed = volumeInput.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues[0].message })
|
if (!parsed.success) return res.status(400).json({ error: parsed.error.issues[0].message })
|
||||||
const info = db
|
const info = db
|
||||||
.prepare('INSERT INTO volumes (project_id, title, summary, sort) VALUES (?,?,?,?)')
|
.prepare('INSERT INTO volumes (project_id, title, summary, sort, chapter_count) VALUES (?,?,?,?,?)')
|
||||||
.run(projectId, parsed.data.title, parsed.data.summary, parsed.data.sort)
|
.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))
|
res.json(db.prepare('SELECT * FROM volumes WHERE id = ?').get(info.lastInsertRowid))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface Volume {
|
|||||||
title: string
|
title: string
|
||||||
summary: string
|
summary: string
|
||||||
sort: number
|
sort: number
|
||||||
|
chapter_count: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Beat {
|
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 }} />
|
<AutoSaveText value={v.title} onSave={(t) => saveVolume(v.id, { title: t })} placeholder="卷名" bordered={false} style={{ fontWeight: 700, fontSize: 15 }} />
|
||||||
</div>
|
</div>
|
||||||
<AutoSaveText value={v.summary} onSave={(s) => saveVolume(v.id, { summary: s })} multiline minRows={2} placeholder="本卷大纲:核心冲突、关键事件、结尾钩子" style={{ color: '#555' }} />
|
<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 }}>
|
<div style={{ marginTop: 6, color: '#bbb', fontSize: 12, display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
|
||||||
本卷 {bundle.chapters.filter((c) => c.volume_id === v.id).length} 章(生成细纲后自动创建)
|
<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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user